mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Reporting] Decouple screenshotting plugin from the reporting (#120110)
* Add screenshotting plugin * Move screenshotting plugin configuration options * Remove unused browser type configuration option
This commit is contained in:
parent
a74825f86f
commit
903e75ee03
178 changed files with 3775 additions and 3981 deletions
|
@ -540,6 +540,11 @@ Elastic.
|
|||
|Add tagging capability to saved objects
|
||||
|
||||
|
||||
|{kib-repo}blob/{branch}/x-pack/plugins/screenshotting/README.md[screenshotting]
|
||||
|This plugin provides functionality to take screenshots of the Kibana pages.
|
||||
It uses Chromium and Puppeteer underneath to run the browser in headless mode.
|
||||
|
||||
|
||||
|{kib-repo}blob/{branch}/x-pack/plugins/searchprofiler/README.md[searchprofiler]
|
||||
|The search profiler consumes the Profile API
|
||||
by sending a search API with profile: true enabled in the request body. The response contains
|
||||
|
|
|
@ -65,7 +65,7 @@ it('produces the right watch and ignore list', () => {
|
|||
<absolute path>/x-pack/test/plugin_functional/plugins/resolver_test/target/**,
|
||||
<absolute path>/x-pack/test/plugin_functional/plugins/resolver_test/scripts/**,
|
||||
<absolute path>/x-pack/test/plugin_functional/plugins/resolver_test/docs/**,
|
||||
<absolute path>/x-pack/plugins/reporting/chromium,
|
||||
<absolute path>/x-pack/plugins/screenshotting/chromium,
|
||||
<absolute path>/x-pack/plugins/security_solution/cypress,
|
||||
<absolute path>/x-pack/plugins/apm/scripts,
|
||||
<absolute path>/x-pack/plugins/apm/ftr_e2e,
|
||||
|
|
|
@ -56,7 +56,7 @@ export function getServerWatchPaths({ pluginPaths, pluginScanDirs }: Options) {
|
|||
/\.(md|sh|txt)$/,
|
||||
/debug\.log$/,
|
||||
...pluginInternalDirsIgnore,
|
||||
fromRoot('x-pack/plugins/reporting/chromium'),
|
||||
fromRoot('x-pack/plugins/screenshotting/chromium'),
|
||||
fromRoot('x-pack/plugins/security_solution/cypress'),
|
||||
fromRoot('x-pack/plugins/apm/scripts'),
|
||||
fromRoot('x-pack/plugins/apm/ftr_e2e'), // prevents restarts for APM cypress tests
|
||||
|
|
|
@ -117,3 +117,4 @@ pageLoadAssetSize:
|
|||
dataViewManagement: 5000
|
||||
reporting: 57003
|
||||
visTypeHeatmap: 25340
|
||||
screenshotting: 17017
|
||||
|
|
|
@ -6,10 +6,8 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { first } from 'rxjs/operators';
|
||||
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { installBrowser } from '../../../../x-pack/plugins/reporting/server/browsers/install';
|
||||
import { install } from '../../../../x-pack/plugins/screenshotting/server/utils';
|
||||
|
||||
export const InstallChromium = {
|
||||
description: 'Installing Chromium',
|
||||
|
@ -22,13 +20,23 @@ export const InstallChromium = {
|
|||
// revert after https://github.com/elastic/kibana/issues/109949
|
||||
if (target === 'darwin-arm64') continue;
|
||||
|
||||
const { binaryPath$ } = installBrowser(
|
||||
log,
|
||||
build.resolvePathForPlatform(platform, 'x-pack/plugins/reporting/chromium'),
|
||||
const logger = {
|
||||
get: log.withType.bind(log),
|
||||
debug: log.debug.bind(log),
|
||||
info: log.info.bind(log),
|
||||
warn: log.warning.bind(log),
|
||||
trace: log.verbose.bind(log),
|
||||
error: log.error.bind(log),
|
||||
fatal: log.error.bind(log),
|
||||
log: log.write.bind(log),
|
||||
};
|
||||
|
||||
await install(
|
||||
logger,
|
||||
build.resolvePathForPlatform(platform, 'x-pack/plugins/screenshotting/chromium'),
|
||||
platform.getName(),
|
||||
platform.getArchitecture()
|
||||
);
|
||||
await binaryPath$.pipe(first()).toPromise();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
@ -15,16 +15,17 @@ const log = new ToolingLog({
|
|||
});
|
||||
|
||||
describe(`enumeratePatterns`, () => {
|
||||
it(`should resolve x-pack/plugins/reporting/server/browsers/extract/unzip.ts to kibana-reporting`, () => {
|
||||
it(`should resolve x-pack/plugins/screenshotting/server/browsers/extract/unzip.ts to kibana-screenshotting`, () => {
|
||||
const actual = enumeratePatterns(REPO_ROOT)(log)(
|
||||
new Map([['x-pack/plugins/reporting', ['kibana-reporting']]])
|
||||
new Map([['x-pack/plugins/screenshotting', ['kibana-screenshotting']]])
|
||||
);
|
||||
|
||||
expect(
|
||||
actual[0].includes(
|
||||
'x-pack/plugins/reporting/server/browsers/extract/unzip.ts kibana-reporting'
|
||||
)
|
||||
).toBe(true);
|
||||
expect(actual).toHaveProperty(
|
||||
'0',
|
||||
expect.arrayContaining([
|
||||
'x-pack/plugins/screenshotting/server/browsers/extract/unzip.ts kibana-screenshotting',
|
||||
])
|
||||
);
|
||||
});
|
||||
it(`should resolve src/plugins/charts/common/static/color_maps/color_maps.ts to kibana-app`, () => {
|
||||
const actual = enumeratePatterns(REPO_ROOT)(log)(
|
||||
|
|
2
x-pack/.gitignore
vendored
2
x-pack/.gitignore
vendored
|
@ -6,7 +6,7 @@
|
|||
/test/functional/apps/**/reports/session
|
||||
/test/reporting/configs/failure_debug/
|
||||
/plugins/reporting/.chromium/
|
||||
/plugins/reporting/chromium/
|
||||
/plugins/screenshotting/chromium/
|
||||
/plugins/reporting/.phantom/
|
||||
/.aws-config.json
|
||||
/.env
|
||||
|
|
|
@ -46,6 +46,7 @@
|
|||
"xpack.reporting": ["plugins/reporting"],
|
||||
"xpack.rollupJobs": ["plugins/rollup"],
|
||||
"xpack.runtimeFields": "plugins/runtime_fields",
|
||||
"xpack.screenshotting": "plugins/screenshotting",
|
||||
"xpack.searchProfiler": "plugins/searchprofiler",
|
||||
"xpack.security": "plugins/security",
|
||||
"xpack.server": "legacy/server",
|
||||
|
|
|
@ -10,5 +10,6 @@
|
|||
},
|
||||
"description": "Example integration code for applications to feature reports.",
|
||||
"optionalPlugins": [],
|
||||
"requiredPlugins": ["reporting", "developerExamples", "navigation", "screenshotMode", "share"]
|
||||
"requiredPlugins": ["reporting", "developerExamples", "navigation", "screenshotMode", "share"],
|
||||
"requiredBundles": ["screenshotting"]
|
||||
}
|
||||
|
|
|
@ -39,7 +39,8 @@ import type {
|
|||
JobParamsPDFV2,
|
||||
JobParamsPNGV2,
|
||||
} from '../../../../plugins/reporting/public';
|
||||
import { constants, ReportingStart } from '../../../../plugins/reporting/public';
|
||||
import { LayoutTypes } from '../../../../plugins/screenshotting/public';
|
||||
import { ReportingStart } from '../../../../plugins/reporting/public';
|
||||
|
||||
import { REPORTING_EXAMPLE_LOCATOR_ID } from '../../common';
|
||||
|
||||
|
@ -87,7 +88,7 @@ export const Main = ({ basename, reporting, screenshotMode }: ReportingExampleAp
|
|||
const getPDFJobParamsDefault = (): JobAppParamsPDF => {
|
||||
return {
|
||||
layout: {
|
||||
id: constants.LAYOUT_TYPES.PRESERVE_LAYOUT,
|
||||
id: LayoutTypes.PRESERVE_LAYOUT,
|
||||
},
|
||||
relativeUrls: ['/app/reportingExample#/intended-visualization'],
|
||||
objectType: 'develeloperExample',
|
||||
|
@ -99,7 +100,7 @@ export const Main = ({ basename, reporting, screenshotMode }: ReportingExampleAp
|
|||
return {
|
||||
version: '8.0.0',
|
||||
layout: {
|
||||
id: constants.LAYOUT_TYPES.PRESERVE_LAYOUT,
|
||||
id: LayoutTypes.PRESERVE_LAYOUT,
|
||||
},
|
||||
locatorParams: [
|
||||
{ id: REPORTING_EXAMPLE_LOCATOR_ID, version: '0.5.0', params: { myTestState: {} } },
|
||||
|
@ -114,7 +115,7 @@ export const Main = ({ basename, reporting, screenshotMode }: ReportingExampleAp
|
|||
return {
|
||||
version: '8.0.0',
|
||||
layout: {
|
||||
id: constants.LAYOUT_TYPES.PRESERVE_LAYOUT,
|
||||
id: LayoutTypes.PRESERVE_LAYOUT,
|
||||
},
|
||||
locatorParams: {
|
||||
id: REPORTING_EXAMPLE_LOCATOR_ID,
|
||||
|
@ -131,7 +132,7 @@ export const Main = ({ basename, reporting, screenshotMode }: ReportingExampleAp
|
|||
return {
|
||||
version: '8.0.0',
|
||||
layout: {
|
||||
id: constants.LAYOUT_TYPES.PRESERVE_LAYOUT,
|
||||
id: LayoutTypes.PRESERVE_LAYOUT,
|
||||
},
|
||||
locatorParams: {
|
||||
id: REPORTING_EXAMPLE_LOCATOR_ID,
|
||||
|
@ -148,7 +149,7 @@ export const Main = ({ basename, reporting, screenshotMode }: ReportingExampleAp
|
|||
return {
|
||||
version: '8.0.0',
|
||||
layout: {
|
||||
id: print ? constants.LAYOUT_TYPES.PRINT : constants.LAYOUT_TYPES.PRESERVE_LAYOUT,
|
||||
id: print ? LayoutTypes.PRINT : LayoutTypes.PRESERVE_LAYOUT,
|
||||
dimensions: {
|
||||
// Magic numbers based on height of components not rendered on this screen :(
|
||||
height: 2400,
|
||||
|
|
|
@ -55,17 +55,6 @@ export const UI_SETTINGS_CSV_SEPARATOR = 'csv:separator';
|
|||
export const UI_SETTINGS_CSV_QUOTE_VALUES = 'csv:quoteValues';
|
||||
export const UI_SETTINGS_DATEFORMAT_TZ = 'dateFormat:tz';
|
||||
|
||||
export const LAYOUT_TYPES = {
|
||||
CANVAS: 'canvas',
|
||||
PRESERVE_LAYOUT: 'preserve_layout',
|
||||
PRINT: 'print',
|
||||
};
|
||||
|
||||
export const DEFAULT_VIEWPORT = {
|
||||
width: 1950,
|
||||
height: 1200,
|
||||
};
|
||||
|
||||
// Export Type Definitions
|
||||
export const CSV_REPORT_TYPE = 'CSV';
|
||||
export const CSV_JOB_TYPE = 'csv_searchsource';
|
||||
|
|
|
@ -11,7 +11,6 @@ import type { ReportMock } from './types';
|
|||
const buildMockReport = (baseObj: ReportMock) => ({
|
||||
index: '.reporting-2020.04.12',
|
||||
migration_version: '7.15.0',
|
||||
browser_type: 'chromium',
|
||||
max_attempts: 1,
|
||||
timeout: 300000,
|
||||
created_by: 'elastic',
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import type { Ensure, SerializableRecord } from '@kbn/utility-types';
|
||||
import type { LayoutParams } from './layout';
|
||||
import type { LayoutParams } from '../../../screenshotting/common';
|
||||
import { LocatorParams } from './url';
|
||||
|
||||
export type JobId = string;
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { LayoutParams } from '../layout';
|
||||
import type { LayoutParams } from '../../../../screenshotting/common';
|
||||
import type { BaseParams, BasePayload } from '../base';
|
||||
|
||||
interface BaseParamsPNG {
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import type { LocatorParams } from '../url';
|
||||
import type { LayoutParams } from '../layout';
|
||||
import type { LayoutParams } from '../../../../screenshotting/common';
|
||||
import type { BaseParams, BasePayload } from '../base';
|
||||
|
||||
// Job params: structure of incoming user request data
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { LayoutParams } from '../layout';
|
||||
import type { LayoutParams } from '../../../../screenshotting/common';
|
||||
import type { BaseParams, BasePayload } from '../base';
|
||||
|
||||
interface BaseParamsPDF {
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import type { LocatorParams } from '../url';
|
||||
import type { LayoutParams } from '../layout';
|
||||
import type { LayoutParams } from '../../../../screenshotting/common';
|
||||
import type { BaseParams, BasePayload } from '../base';
|
||||
|
||||
interface BaseParamsPDFV2 {
|
||||
|
|
|
@ -5,11 +5,9 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { Size, LayoutParams } from './layout';
|
||||
import type { JobId, BaseParams, BaseParamsV2, BasePayload, BasePayloadV2 } from './base';
|
||||
|
||||
export type { JobId, BaseParams, BaseParamsV2, BasePayload, BasePayloadV2 };
|
||||
export type { Size, LayoutParams };
|
||||
export type {
|
||||
DownloadReportFn,
|
||||
IlmPolicyMigrationStatus,
|
||||
|
@ -20,20 +18,6 @@ export type {
|
|||
} from './url';
|
||||
export * from './export_types';
|
||||
|
||||
export interface PageSizeParams {
|
||||
pageMarginTop: number;
|
||||
pageMarginBottom: number;
|
||||
pageMarginWidth: number;
|
||||
tableBorderWidth: number;
|
||||
headingHeight: number;
|
||||
subheadingHeight: number;
|
||||
}
|
||||
|
||||
export interface PdfImageSize {
|
||||
width: number;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
export interface ReportDocumentHead {
|
||||
_id: string;
|
||||
_index: string;
|
||||
|
@ -83,7 +67,6 @@ export interface ReportSource {
|
|||
*/
|
||||
kibana_name?: string; // for troubleshooting
|
||||
kibana_id?: string; // for troubleshooting
|
||||
browser_type?: string; // no longer used since chromium is the only option (used to allow phantomjs)
|
||||
timeout?: number; // for troubleshooting: the actual comparison uses the config setting xpack.reporting.queue.timeout
|
||||
max_attempts?: number; // for troubleshooting: the actual comparison uses the config setting xpack.reporting.capture.maxAttempts
|
||||
started_at?: string; // timestamp in UTC
|
||||
|
|
|
@ -1,24 +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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { Ensure, SerializableRecord } from '@kbn/utility-types';
|
||||
|
||||
export type Size = Ensure<
|
||||
{
|
||||
width: number;
|
||||
height: number;
|
||||
},
|
||||
SerializableRecord
|
||||
>;
|
||||
|
||||
export type LayoutParams = Ensure<
|
||||
{
|
||||
id: string;
|
||||
dimensions?: Size;
|
||||
},
|
||||
SerializableRecord
|
||||
>;
|
|
@ -18,6 +18,7 @@
|
|||
"uiActions",
|
||||
"taskManager",
|
||||
"embeddable",
|
||||
"screenshotting",
|
||||
"screenshotMode",
|
||||
"share",
|
||||
"features"
|
||||
|
|
|
@ -51,7 +51,6 @@ export class Job {
|
|||
public timeout: ReportSource['timeout'];
|
||||
public kibana_name: ReportSource['kibana_name'];
|
||||
public kibana_id: ReportSource['kibana_id'];
|
||||
public browser_type: ReportSource['browser_type'];
|
||||
|
||||
public size?: ReportOutput['size'];
|
||||
public content_type?: TaskRunResult['content_type'];
|
||||
|
@ -80,7 +79,6 @@ export class Job {
|
|||
this.timeout = report.timeout;
|
||||
this.kibana_name = report.kibana_name;
|
||||
this.kibana_id = report.kibana_id;
|
||||
this.browser_type = report.browser_type;
|
||||
this.browserTimezone = report.payload.browserTimezone;
|
||||
this.size = report.output?.size;
|
||||
this.content_type = report.output?.content_type;
|
||||
|
|
|
@ -141,12 +141,6 @@ export const ReportInfoFlyoutContent: FunctionComponent<Props> = ({ info }) => {
|
|||
}),
|
||||
description: info.layout?.id || UNKNOWN,
|
||||
},
|
||||
{
|
||||
title: i18n.translate('xpack.reporting.listing.infoPanel.browserTypeInfo', {
|
||||
defaultMessage: 'Browser type',
|
||||
}),
|
||||
description: info.browser_type || NA,
|
||||
},
|
||||
];
|
||||
|
||||
const warnings = info.getWarnings();
|
||||
|
|
|
@ -18,6 +18,7 @@ import {
|
|||
Plugin,
|
||||
PluginInitializerContext,
|
||||
} from 'src/core/public';
|
||||
import type { ScreenshottingSetup } from '../../screenshotting/public';
|
||||
import { CONTEXT_MENU_TRIGGER } from '../../../../src/plugins/embeddable/public';
|
||||
import {
|
||||
FeatureCatalogueCategory,
|
||||
|
@ -73,6 +74,7 @@ export interface ReportingPublicPluginSetupDendencies {
|
|||
management: ManagementSetup;
|
||||
licensing: LicensingPluginSetup;
|
||||
uiActions: UiActionsSetup;
|
||||
screenshotting: ScreenshottingSetup;
|
||||
share: SharePluginSetup;
|
||||
}
|
||||
|
||||
|
@ -145,6 +147,7 @@ export class ReportingPublicPlugin
|
|||
home,
|
||||
management,
|
||||
licensing: { license$ }, // FIXME: 'license$' is deprecated
|
||||
screenshotting,
|
||||
share,
|
||||
uiActions,
|
||||
} = setupDeps;
|
||||
|
@ -203,7 +206,7 @@ export class ReportingPublicPlugin
|
|||
id: 'reportingRedirect',
|
||||
mount: async (params) => {
|
||||
const { mountRedirectApp } = await import('./redirect');
|
||||
return mountRedirectApp({ ...params, share, apiClient });
|
||||
return mountRedirectApp({ ...params, apiClient, screenshotting, share });
|
||||
},
|
||||
title: 'Reporting redirect app',
|
||||
searchable: false,
|
||||
|
|
|
@ -10,6 +10,7 @@ import React from 'react';
|
|||
import { EuiErrorBoundary } from '@elastic/eui';
|
||||
|
||||
import type { AppMountParameters } from 'kibana/public';
|
||||
import type { ScreenshottingSetup } from '../../../screenshotting/public';
|
||||
import type { SharePluginSetup } from '../shared_imports';
|
||||
import type { ReportingAPIClient } from '../lib/reporting_api_client';
|
||||
|
||||
|
@ -17,13 +18,25 @@ import { RedirectApp } from './redirect_app';
|
|||
|
||||
interface MountParams extends AppMountParameters {
|
||||
apiClient: ReportingAPIClient;
|
||||
screenshotting: ScreenshottingSetup;
|
||||
share: SharePluginSetup;
|
||||
}
|
||||
|
||||
export const mountRedirectApp = ({ element, apiClient, history, share }: MountParams) => {
|
||||
export const mountRedirectApp = ({
|
||||
element,
|
||||
apiClient,
|
||||
history,
|
||||
screenshotting,
|
||||
share,
|
||||
}: MountParams) => {
|
||||
render(
|
||||
<EuiErrorBoundary>
|
||||
<RedirectApp apiClient={apiClient} history={history} share={share} />
|
||||
<RedirectApp
|
||||
apiClient={apiClient}
|
||||
history={history}
|
||||
screenshotting={screenshotting}
|
||||
share={share}
|
||||
/>
|
||||
</EuiErrorBoundary>,
|
||||
element
|
||||
);
|
||||
|
|
|
@ -12,6 +12,7 @@ import { i18n } from '@kbn/i18n';
|
|||
import { EuiCallOut, EuiCodeBlock } from '@elastic/eui';
|
||||
|
||||
import type { ScopedHistory } from 'src/core/public';
|
||||
import type { ScreenshottingSetup } from '../../../screenshotting/public';
|
||||
|
||||
import { REPORTING_REDIRECT_LOCATOR_STORE_KEY } from '../../common/constants';
|
||||
import { LocatorParams } from '../../common/types';
|
||||
|
@ -24,6 +25,7 @@ import './redirect_app.scss';
|
|||
interface Props {
|
||||
apiClient: ReportingAPIClient;
|
||||
history: ScopedHistory;
|
||||
screenshotting: ScreenshottingSetup;
|
||||
share: SharePluginSetup;
|
||||
}
|
||||
|
||||
|
@ -39,7 +41,9 @@ const i18nTexts = {
|
|||
),
|
||||
};
|
||||
|
||||
export const RedirectApp: FunctionComponent<Props> = ({ share, apiClient }) => {
|
||||
type ReportingContext = Record<string, LocatorParams>;
|
||||
|
||||
export const RedirectApp: FunctionComponent<Props> = ({ apiClient, screenshotting, share }) => {
|
||||
const [error, setError] = useState<undefined | Error>();
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -53,9 +57,8 @@ export const RedirectApp: FunctionComponent<Props> = ({ share, apiClient }) => {
|
|||
const result = await apiClient.getInfo(jobId as string);
|
||||
locatorParams = result?.locatorParams?.[0];
|
||||
} else {
|
||||
locatorParams = (window as unknown as Record<string, LocatorParams>)[
|
||||
REPORTING_REDIRECT_LOCATOR_STORE_KEY
|
||||
];
|
||||
locatorParams =
|
||||
screenshotting.getContext<ReportingContext>()?.[REPORTING_REDIRECT_LOCATOR_STORE_KEY];
|
||||
}
|
||||
|
||||
if (!locatorParams) {
|
||||
|
@ -70,7 +73,7 @@ export const RedirectApp: FunctionComponent<Props> = ({ share, apiClient }) => {
|
|||
throw e;
|
||||
}
|
||||
})();
|
||||
}, [share, apiClient]);
|
||||
}, [apiClient, screenshotting, share]);
|
||||
|
||||
return (
|
||||
<div className="reportingRedirectApp__interstitialPage">
|
||||
|
|
|
@ -8,8 +8,8 @@
|
|||
import * as Rx from 'rxjs';
|
||||
import type { IUiSettingsClient, ToastsSetup } from 'src/core/public';
|
||||
import { CoreStart } from 'src/core/public';
|
||||
import type { LayoutParams } from '../../../screenshotting/common';
|
||||
import type { LicensingPluginSetup } from '../../../licensing/public';
|
||||
import type { LayoutParams } from '../../common/types';
|
||||
import type { ReportingAPIClient } from '../lib/reporting_api_client';
|
||||
|
||||
export interface ExportPanelShareOpts {
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import { EuiFormRow, EuiSwitch, EuiSwitchEvent } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import React, { Component } from 'react';
|
||||
import { LayoutParams } from '../../common/types';
|
||||
import type { LayoutParams } from '../../../screenshotting/common';
|
||||
import { ReportingPanelContent, ReportingPanelProps } from './reporting_panel_content';
|
||||
|
||||
export interface Props extends ReportingPanelProps {
|
||||
|
@ -103,7 +103,7 @@ export class ScreenCapturePanelContent extends Component<Props, State> {
|
|||
this.setState({ useCanvasLayout: evt.target.checked, usePrintLayout: false });
|
||||
};
|
||||
|
||||
private getLayout = (): Required<LayoutParams> => {
|
||||
private getLayout = (): LayoutParams => {
|
||||
const { layout: outerLayout } = this.props.getJobParams();
|
||||
|
||||
let dimensions = outerLayout?.dimensions;
|
||||
|
|
|
@ -1,76 +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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import puppeteer from 'puppeteer';
|
||||
import * as Rx from 'rxjs';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { HeadlessChromiumDriverFactory } from '.';
|
||||
import type { ReportingCore } from '../../..';
|
||||
import {
|
||||
createMockConfigSchema,
|
||||
createMockLevelLogger,
|
||||
createMockReportingCore,
|
||||
} from '../../../test_helpers';
|
||||
|
||||
jest.mock('puppeteer');
|
||||
|
||||
const mock = (browserDriverFactory: HeadlessChromiumDriverFactory) => {
|
||||
browserDriverFactory.getBrowserLogger = jest.fn(() => new Rx.Observable());
|
||||
browserDriverFactory.getProcessLogger = jest.fn(() => new Rx.Observable());
|
||||
browserDriverFactory.getPageExit = jest.fn(() => new Rx.Observable());
|
||||
return browserDriverFactory;
|
||||
};
|
||||
|
||||
describe('class HeadlessChromiumDriverFactory', () => {
|
||||
let reporting: ReportingCore;
|
||||
const logger = createMockLevelLogger();
|
||||
const path = 'path/to/headless_shell';
|
||||
|
||||
beforeEach(async () => {
|
||||
(puppeteer as jest.Mocked<typeof puppeteer>).launch.mockResolvedValue({
|
||||
newPage: jest.fn().mockResolvedValue({
|
||||
target: jest.fn(() => ({
|
||||
createCDPSession: jest.fn().mockResolvedValue({
|
||||
send: jest.fn(),
|
||||
}),
|
||||
})),
|
||||
emulateTimezone: jest.fn(),
|
||||
setDefaultTimeout: jest.fn(),
|
||||
}),
|
||||
close: jest.fn(),
|
||||
process: jest.fn(),
|
||||
} as unknown as puppeteer.Browser);
|
||||
|
||||
reporting = await createMockReportingCore(
|
||||
createMockConfigSchema({
|
||||
capture: {
|
||||
browser: { chromium: { proxy: {} } },
|
||||
timeouts: { openUrl: 50000 },
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('createPage returns browser driver and process exit observable', async () => {
|
||||
const factory = mock(new HeadlessChromiumDriverFactory(reporting, path, logger));
|
||||
const utils = await factory.createPage({}).pipe(take(1)).toPromise();
|
||||
expect(utils).toHaveProperty('driver');
|
||||
expect(utils).toHaveProperty('exit$');
|
||||
});
|
||||
|
||||
it('createPage rejects if Puppeteer launch fails', async () => {
|
||||
(puppeteer as jest.Mocked<typeof puppeteer>).launch.mockRejectedValue(
|
||||
`Puppeteer Launch mock fail.`
|
||||
);
|
||||
const factory = mock(new HeadlessChromiumDriverFactory(reporting, path, logger));
|
||||
expect(() =>
|
||||
factory.createPage({}).pipe(take(1)).toPromise()
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"Error spawning Chromium browser! Puppeteer Launch mock fail."`
|
||||
);
|
||||
});
|
||||
});
|
|
@ -1,268 +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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { getDataPath } from '@kbn/utils';
|
||||
import del from 'del';
|
||||
import apm from 'elastic-apm-node';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import puppeteer from 'puppeteer';
|
||||
import * as Rx from 'rxjs';
|
||||
import { InnerSubscriber } from 'rxjs/internal/InnerSubscriber';
|
||||
import { ignoreElements, map, mergeMap, tap } from 'rxjs/operators';
|
||||
import { getChromiumDisconnectedError } from '../';
|
||||
import { ReportingCore } from '../../..';
|
||||
import { durationToNumber } from '../../../../common/schema_utils';
|
||||
import { CaptureConfig } from '../../../../server/types';
|
||||
import { LevelLogger } from '../../../lib';
|
||||
import { safeChildProcess } from '../../safe_child_process';
|
||||
import { HeadlessChromiumDriver } from '../driver';
|
||||
import { args } from './args';
|
||||
import { getMetrics } from './metrics';
|
||||
|
||||
type BrowserConfig = CaptureConfig['browser']['chromium'];
|
||||
|
||||
export class HeadlessChromiumDriverFactory {
|
||||
private binaryPath: string;
|
||||
private captureConfig: CaptureConfig;
|
||||
private browserConfig: BrowserConfig;
|
||||
private userDataDir: string;
|
||||
private getChromiumArgs: () => string[];
|
||||
private core: ReportingCore;
|
||||
|
||||
constructor(core: ReportingCore, binaryPath: string, private logger: LevelLogger) {
|
||||
this.core = core;
|
||||
this.binaryPath = binaryPath;
|
||||
const config = core.getConfig();
|
||||
this.captureConfig = config.get('capture');
|
||||
this.browserConfig = this.captureConfig.browser.chromium;
|
||||
|
||||
if (this.browserConfig.disableSandbox) {
|
||||
logger.warning(`Enabling the Chromium sandbox provides an additional layer of protection.`);
|
||||
}
|
||||
|
||||
this.userDataDir = fs.mkdtempSync(path.join(getDataPath(), 'chromium-'));
|
||||
this.getChromiumArgs = () =>
|
||||
args({
|
||||
userDataDir: this.userDataDir,
|
||||
disableSandbox: this.browserConfig.disableSandbox,
|
||||
proxy: this.browserConfig.proxy,
|
||||
});
|
||||
}
|
||||
|
||||
type = 'chromium';
|
||||
|
||||
/*
|
||||
* Return an observable to objects which will drive screenshot capture for a page
|
||||
*/
|
||||
createPage(
|
||||
{ browserTimezone }: { browserTimezone?: string },
|
||||
pLogger = this.logger
|
||||
): Rx.Observable<{ driver: HeadlessChromiumDriver; exit$: Rx.Observable<never> }> {
|
||||
// FIXME: 'create' is deprecated
|
||||
return Rx.Observable.create(async (observer: InnerSubscriber<unknown, unknown>) => {
|
||||
const logger = pLogger.clone(['browser-driver']);
|
||||
logger.info(`Creating browser page driver`);
|
||||
|
||||
const chromiumArgs = this.getChromiumArgs();
|
||||
logger.debug(`Chromium launch args set to: ${chromiumArgs}`);
|
||||
|
||||
let browser: puppeteer.Browser | null = null;
|
||||
|
||||
try {
|
||||
browser = await puppeteer.launch({
|
||||
pipe: !this.browserConfig.inspect,
|
||||
userDataDir: this.userDataDir,
|
||||
executablePath: this.binaryPath,
|
||||
ignoreHTTPSErrors: true,
|
||||
handleSIGHUP: false,
|
||||
args: chromiumArgs,
|
||||
env: {
|
||||
TZ: browserTimezone,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
observer.error(new Error(`Error spawning Chromium browser! ${err}`));
|
||||
return;
|
||||
}
|
||||
|
||||
const page = await browser.newPage();
|
||||
const devTools = await page.target().createCDPSession();
|
||||
|
||||
await devTools.send('Performance.enable', { timeDomain: 'timeTicks' });
|
||||
const startMetrics = await devTools.send('Performance.getMetrics');
|
||||
|
||||
// Log version info for debugging / maintenance
|
||||
const versionInfo = await devTools.send('Browser.getVersion');
|
||||
logger.debug(`Browser version: ${JSON.stringify(versionInfo)}`);
|
||||
|
||||
await page.emulateTimezone(browserTimezone);
|
||||
|
||||
// Set the default timeout for all navigation methods to the openUrl timeout
|
||||
// All waitFor methods have their own timeout config passed in to them
|
||||
page.setDefaultTimeout(durationToNumber(this.captureConfig.timeouts.openUrl));
|
||||
|
||||
logger.debug(`Browser page driver created`);
|
||||
|
||||
const childProcess = {
|
||||
async kill() {
|
||||
try {
|
||||
if (devTools && startMetrics) {
|
||||
const endMetrics = await devTools.send('Performance.getMetrics');
|
||||
const { cpu, cpuInPercentage, memory, memoryInMegabytes } = getMetrics(
|
||||
startMetrics,
|
||||
endMetrics
|
||||
);
|
||||
|
||||
apm.currentTransaction?.setLabel('cpu', cpu, false);
|
||||
apm.currentTransaction?.setLabel('memory', memory, false);
|
||||
logger.debug(
|
||||
`Chromium consumed CPU ${cpuInPercentage}% Memory ${memoryInMegabytes}MB`
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
}
|
||||
|
||||
try {
|
||||
await browser?.close();
|
||||
} catch (err) {
|
||||
// do not throw
|
||||
logger.error(err);
|
||||
}
|
||||
},
|
||||
};
|
||||
const { terminate$ } = safeChildProcess(logger, childProcess);
|
||||
|
||||
// this is adding unsubscribe logic to our observer
|
||||
// so that if our observer unsubscribes, we terminate our child-process
|
||||
observer.add(() => {
|
||||
logger.debug(`The browser process observer has unsubscribed. Closing the browser...`);
|
||||
childProcess.kill(); // ignore async
|
||||
});
|
||||
|
||||
// make the observer subscribe to terminate$
|
||||
observer.add(
|
||||
terminate$
|
||||
.pipe(
|
||||
tap((signal) => {
|
||||
logger.debug(`Termination signal received: ${signal}`);
|
||||
}),
|
||||
ignoreElements()
|
||||
)
|
||||
.subscribe(observer)
|
||||
);
|
||||
|
||||
// taps the browser log streams and combine them to Kibana logs
|
||||
this.getBrowserLogger(page, logger).subscribe();
|
||||
this.getProcessLogger(browser, logger).subscribe();
|
||||
|
||||
// HeadlessChromiumDriver: object to "drive" a browser page
|
||||
const driver = new HeadlessChromiumDriver(this.core, page, {
|
||||
inspect: !!this.browserConfig.inspect,
|
||||
networkPolicy: this.captureConfig.networkPolicy,
|
||||
});
|
||||
|
||||
// Rx.Observable<never>: stream to interrupt page capture
|
||||
const exit$ = this.getPageExit(browser, page);
|
||||
|
||||
observer.next({ driver, exit$ });
|
||||
|
||||
// unsubscribe logic makes a best-effort attempt to delete the user data directory used by chromium
|
||||
observer.add(() => {
|
||||
const userDataDir = this.userDataDir;
|
||||
logger.debug(`deleting chromium user data directory at [${userDataDir}]`);
|
||||
// the unsubscribe function isn't `async` so we're going to make our best effort at
|
||||
// deleting the userDataDir and if it fails log an error.
|
||||
del(userDataDir, { force: true }).catch((error) => {
|
||||
logger.error(`error deleting user data directory at [${userDataDir}]!`);
|
||||
logger.error(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getBrowserLogger(page: puppeteer.Page, logger: LevelLogger): Rx.Observable<void> {
|
||||
const consoleMessages$ = Rx.fromEvent<puppeteer.ConsoleMessage>(page, 'console').pipe(
|
||||
map((line) => {
|
||||
const formatLine = () => `{ text: "${line.text()?.trim()}", url: ${line.location()?.url} }`;
|
||||
|
||||
if (line.type() === 'error') {
|
||||
logger.error(`Error in browser console: ${formatLine()}`, ['headless-browser-console']);
|
||||
} else {
|
||||
logger.debug(`Message in browser console: ${formatLine()}`, [
|
||||
`headless-browser-console:${line.type()}`,
|
||||
]);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const uncaughtExceptionPageError$ = Rx.fromEvent<Error>(page, 'pageerror').pipe(
|
||||
map((err) => {
|
||||
logger.warning(
|
||||
i18n.translate('xpack.reporting.browsers.chromium.pageErrorDetected', {
|
||||
defaultMessage: `Reporting encountered an uncaught error on the page that will be ignored: {err}`,
|
||||
values: { err: err.toString() },
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
const pageRequestFailed$ = Rx.fromEvent<puppeteer.HTTPRequest>(page, 'requestfailed').pipe(
|
||||
map((req) => {
|
||||
const failure = req.failure && req.failure();
|
||||
if (failure) {
|
||||
logger.warning(
|
||||
`Request to [${req.url()}] failed! [${failure.errorText}]. This error will be ignored.`
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
return Rx.merge(consoleMessages$, uncaughtExceptionPageError$, pageRequestFailed$);
|
||||
}
|
||||
|
||||
getProcessLogger(browser: puppeteer.Browser, logger: LevelLogger): Rx.Observable<void> {
|
||||
const childProcess = browser.process();
|
||||
// NOTE: The browser driver can not observe stdout and stderr of the child process
|
||||
// Puppeteer doesn't give a handle to the original ChildProcess object
|
||||
// See https://github.com/GoogleChrome/puppeteer/issues/1292#issuecomment-521470627
|
||||
|
||||
if (childProcess == null) {
|
||||
throw new TypeError('childProcess is null or undefined!');
|
||||
}
|
||||
|
||||
// just log closing of the process
|
||||
const processClose$ = Rx.fromEvent<void>(childProcess, 'close').pipe(
|
||||
tap(() => {
|
||||
logger.debug('child process closed', ['headless-browser-process']);
|
||||
})
|
||||
);
|
||||
|
||||
return processClose$; // ideally, this would also merge with observers for stdout and stderr
|
||||
}
|
||||
|
||||
getPageExit(browser: puppeteer.Browser, page: puppeteer.Page) {
|
||||
const pageError$ = Rx.fromEvent<Error>(page, 'error').pipe(
|
||||
mergeMap((err) => {
|
||||
return Rx.throwError(
|
||||
i18n.translate('xpack.reporting.browsers.chromium.errorDetected', {
|
||||
defaultMessage: 'Reporting encountered an error: {err}',
|
||||
values: { err: err.toString() },
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
const browserDisconnect$ = Rx.fromEvent(browser, 'disconnected').pipe(
|
||||
mergeMap(() => Rx.throwError(getChromiumDisconnectedError()))
|
||||
);
|
||||
|
||||
return Rx.merge(pageError$, browserDisconnect$);
|
||||
}
|
||||
}
|
|
@ -1,144 +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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { spawn } from 'child_process';
|
||||
import del from 'del';
|
||||
import { mkdtempSync } from 'fs';
|
||||
import { uniq } from 'lodash';
|
||||
import os from 'os';
|
||||
import { join } from 'path';
|
||||
import { createInterface } from 'readline';
|
||||
import { getDataPath } from '@kbn/utils';
|
||||
import { fromEvent, merge, of, timer } from 'rxjs';
|
||||
import { catchError, map, reduce, takeUntil, tap } from 'rxjs/operators';
|
||||
import { ReportingCore } from '../../../';
|
||||
import { LevelLogger } from '../../../lib';
|
||||
import { ChromiumArchivePaths } from '../paths';
|
||||
import { args } from './args';
|
||||
|
||||
const paths = new ChromiumArchivePaths();
|
||||
const browserLaunchTimeToWait = 5 * 1000;
|
||||
|
||||
// Default args used by pptr
|
||||
// https://github.com/puppeteer/puppeteer/blob/13ea347/src/node/Launcher.ts#L168
|
||||
const defaultArgs = [
|
||||
'--disable-background-networking',
|
||||
'--enable-features=NetworkService,NetworkServiceInProcess',
|
||||
'--disable-background-timer-throttling',
|
||||
'--disable-backgrounding-occluded-windows',
|
||||
'--disable-breakpad',
|
||||
'--disable-client-side-phishing-detection',
|
||||
'--disable-component-extensions-with-background-pages',
|
||||
'--disable-default-apps',
|
||||
'--disable-dev-shm-usage',
|
||||
'--disable-extensions',
|
||||
'--disable-features=TranslateUI',
|
||||
'--disable-hang-monitor',
|
||||
'--disable-ipc-flooding-protection',
|
||||
'--disable-popup-blocking',
|
||||
'--disable-prompt-on-repost',
|
||||
'--disable-renderer-backgrounding',
|
||||
'--disable-sync',
|
||||
'--force-color-profile=srgb',
|
||||
'--metrics-recording-only',
|
||||
'--no-first-run',
|
||||
'--enable-automation',
|
||||
'--password-store=basic',
|
||||
'--use-mock-keychain',
|
||||
'--remote-debugging-port=0',
|
||||
'--headless',
|
||||
];
|
||||
|
||||
export const browserStartLogs = (
|
||||
core: ReportingCore,
|
||||
logger: LevelLogger,
|
||||
overrideFlags: string[] = []
|
||||
) => {
|
||||
const config = core.getConfig();
|
||||
const proxy = config.get('capture', 'browser', 'chromium', 'proxy');
|
||||
const disableSandbox = config.get('capture', 'browser', 'chromium', 'disableSandbox');
|
||||
const userDataDir = mkdtempSync(join(getDataPath(), 'chromium-'));
|
||||
|
||||
const platform = process.platform;
|
||||
const architecture = os.arch();
|
||||
const pkg = paths.find(platform, architecture);
|
||||
if (!pkg) {
|
||||
throw new Error(`Unsupported platform: ${platform}-${architecture}`);
|
||||
}
|
||||
const binaryPath = paths.getBinaryPath(pkg);
|
||||
|
||||
const kbnArgs = args({
|
||||
userDataDir,
|
||||
disableSandbox,
|
||||
proxy,
|
||||
});
|
||||
const finalArgs = uniq([...defaultArgs, ...kbnArgs, ...overrideFlags]);
|
||||
|
||||
// On non-windows platforms, `detached: true` makes child process a
|
||||
// leader of a new process group, making it possible to kill child
|
||||
// process tree with `.kill(-pid)` command. @see
|
||||
// https://nodejs.org/api/child_process.html#child_process_options_detached
|
||||
const browserProcess = spawn(binaryPath, finalArgs, {
|
||||
detached: process.platform !== 'win32',
|
||||
});
|
||||
|
||||
const rl = createInterface({ input: browserProcess.stderr });
|
||||
|
||||
const exit$ = fromEvent(browserProcess, 'exit').pipe(
|
||||
map((code) => {
|
||||
logger.error(`Browser exited abnormally, received code: ${code}`);
|
||||
return i18n.translate('xpack.reporting.diagnostic.browserCrashed', {
|
||||
defaultMessage: `Browser exited abnormally during startup`,
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
const error$ = fromEvent(browserProcess, 'error').pipe(
|
||||
map((err) => {
|
||||
logger.error(`Browser process threw an error on startup`);
|
||||
logger.error(err as string | Error);
|
||||
return i18n.translate('xpack.reporting.diagnostic.browserErrored', {
|
||||
defaultMessage: `Browser process threw an error on startup`,
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
const browserProcessLogger = logger.clone(['chromium-stderr']);
|
||||
const log$ = fromEvent(rl, 'line').pipe(
|
||||
tap((message: unknown) => {
|
||||
if (typeof message === 'string') {
|
||||
browserProcessLogger.info(message);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Collect all events (exit, error and on log-lines), but let chromium keep spitting out
|
||||
// logs as sometimes it's "bind" successfully for remote connections, but later emit
|
||||
// a log indicative of an issue (for example, no default font found).
|
||||
return merge(exit$, error$, log$).pipe(
|
||||
takeUntil(timer(browserLaunchTimeToWait)),
|
||||
reduce((acc, curr) => `${acc}${curr}\n`, ''),
|
||||
tap(() => {
|
||||
if (browserProcess && browserProcess.pid && !browserProcess.killed) {
|
||||
browserProcess.kill('SIGKILL');
|
||||
logger.info(`Successfully sent 'SIGKILL' to browser process (PID: ${browserProcess.pid})`);
|
||||
}
|
||||
browserProcess.removeAllListeners();
|
||||
rl.removeAllListeners();
|
||||
rl.close();
|
||||
del(userDataDir, { force: true }).catch((error) => {
|
||||
logger.error(`Error deleting user data directory at [${userDataDir}]!`);
|
||||
logger.error(error);
|
||||
});
|
||||
}),
|
||||
catchError((error) => {
|
||||
logger.error(error);
|
||||
return of(error);
|
||||
})
|
||||
);
|
||||
};
|
|
@ -1,36 +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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { BrowserDownload } from '../';
|
||||
import { ReportingCore } from '../../../server';
|
||||
import { LevelLogger } from '../../lib';
|
||||
import { HeadlessChromiumDriverFactory } from './driver_factory';
|
||||
import { ChromiumArchivePaths } from './paths';
|
||||
|
||||
export const chromium: BrowserDownload = {
|
||||
paths: new ChromiumArchivePaths(),
|
||||
createDriverFactory: (core: ReportingCore, binaryPath: string, logger: LevelLogger) =>
|
||||
new HeadlessChromiumDriverFactory(core, binaryPath, logger),
|
||||
};
|
||||
|
||||
export const getChromiumDisconnectedError = () =>
|
||||
new Error(
|
||||
i18n.translate('xpack.reporting.screencapture.browserWasClosed', {
|
||||
defaultMessage: 'Browser was closed unexpectedly! Check the server logs for more info.',
|
||||
})
|
||||
);
|
||||
|
||||
export const getDisallowedOutgoingUrlError = (interceptedUrl: string) =>
|
||||
new Error(
|
||||
i18n.translate('xpack.reporting.chromiumDriver.disallowedOutgoingUrl', {
|
||||
defaultMessage: `Received disallowed outgoing URL: "{interceptedUrl}". Failing the request and closing the browser.`,
|
||||
values: { interceptedUrl },
|
||||
})
|
||||
);
|
||||
|
||||
export { ChromiumArchivePaths };
|
|
@ -1,72 +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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { createHash } from 'crypto';
|
||||
import del from 'del';
|
||||
import { readFileSync } from 'fs';
|
||||
import { resolve as resolvePath } from 'path';
|
||||
import { Readable } from 'stream';
|
||||
import { LevelLogger } from '../../lib';
|
||||
import { download } from './download';
|
||||
|
||||
const TEMP_DIR = resolvePath(__dirname, '__tmp__');
|
||||
const TEMP_FILE = resolvePath(TEMP_DIR, 'foo/bar/download');
|
||||
|
||||
class ReadableOf extends Readable {
|
||||
constructor(private readonly responseBody: string) {
|
||||
super();
|
||||
}
|
||||
|
||||
_read() {
|
||||
this.push(this.responseBody);
|
||||
this.push(null);
|
||||
}
|
||||
}
|
||||
|
||||
jest.mock('axios');
|
||||
const request: jest.Mock = jest.requireMock('axios').request;
|
||||
|
||||
const mockLogger = {
|
||||
error: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
info: jest.fn(),
|
||||
} as unknown as LevelLogger;
|
||||
|
||||
test('downloads the url to the path', async () => {
|
||||
const BODY = 'abdcefg';
|
||||
request.mockImplementationOnce(async () => {
|
||||
return {
|
||||
data: new ReadableOf(BODY),
|
||||
};
|
||||
});
|
||||
|
||||
await download('url', TEMP_FILE, mockLogger);
|
||||
expect(readFileSync(TEMP_FILE, 'utf8')).toEqual(BODY);
|
||||
});
|
||||
|
||||
test('returns the md5 hex hash of the http body', async () => {
|
||||
const BODY = 'foobar';
|
||||
const HASH = createHash('md5').update(BODY).digest('hex');
|
||||
request.mockImplementationOnce(async () => {
|
||||
return {
|
||||
data: new ReadableOf(BODY),
|
||||
};
|
||||
});
|
||||
|
||||
const returned = await download('url', TEMP_FILE, mockLogger);
|
||||
expect(returned).toEqual(HASH);
|
||||
});
|
||||
|
||||
test('throws if request emits an error', async () => {
|
||||
request.mockImplementationOnce(async () => {
|
||||
throw new Error('foo');
|
||||
});
|
||||
|
||||
return expect(download('url', TEMP_FILE, mockLogger)).rejects.toThrow('foo');
|
||||
});
|
||||
|
||||
afterEach(async () => await del(TEMP_DIR));
|
|
@ -1,120 +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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import mockFs from 'mock-fs';
|
||||
import { existsSync, readdirSync } from 'fs';
|
||||
import { chromium } from '../chromium';
|
||||
import { download } from './download';
|
||||
import { md5 } from './checksum';
|
||||
import { ensureBrowserDownloaded } from './ensure_downloaded';
|
||||
import { LevelLogger } from '../../lib';
|
||||
|
||||
jest.mock('./checksum');
|
||||
jest.mock('./download');
|
||||
|
||||
// https://github.com/elastic/kibana/issues/115881
|
||||
describe.skip('ensureBrowserDownloaded', () => {
|
||||
let logger: jest.Mocked<LevelLogger>;
|
||||
|
||||
beforeEach(() => {
|
||||
logger = {
|
||||
debug: jest.fn(),
|
||||
error: jest.fn(),
|
||||
warning: jest.fn(),
|
||||
} as unknown as typeof logger;
|
||||
|
||||
(md5 as jest.MockedFunction<typeof md5>).mockImplementation(
|
||||
async (packagePath) =>
|
||||
chromium.paths.packages.find(
|
||||
(packageInfo) => chromium.paths.resolvePath(packageInfo) === packagePath
|
||||
)?.archiveChecksum ?? 'some-md5'
|
||||
);
|
||||
|
||||
(download as jest.MockedFunction<typeof download>).mockImplementation(
|
||||
async (_url, packagePath) =>
|
||||
chromium.paths.packages.find(
|
||||
(packageInfo) => chromium.paths.resolvePath(packageInfo) === packagePath
|
||||
)?.archiveChecksum ?? 'some-md5'
|
||||
);
|
||||
|
||||
mockFs();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should remove unexpected files', async () => {
|
||||
const unexpectedPath1 = `${chromium.paths.archivesPath}/unexpected1`;
|
||||
const unexpectedPath2 = `${chromium.paths.archivesPath}/unexpected2`;
|
||||
|
||||
mockFs({
|
||||
[unexpectedPath1]: 'test',
|
||||
[unexpectedPath2]: 'test',
|
||||
});
|
||||
|
||||
await ensureBrowserDownloaded(logger);
|
||||
|
||||
expect(existsSync(unexpectedPath1)).toBe(false);
|
||||
expect(existsSync(unexpectedPath2)).toBe(false);
|
||||
});
|
||||
|
||||
it('should reject when download fails', async () => {
|
||||
(download as jest.MockedFunction<typeof download>).mockRejectedValueOnce(
|
||||
new Error('some error')
|
||||
);
|
||||
|
||||
await expect(ensureBrowserDownloaded(logger)).rejects.toBeInstanceOf(Error);
|
||||
});
|
||||
|
||||
it('should reject when downloaded md5 hash is different', async () => {
|
||||
(download as jest.MockedFunction<typeof download>).mockResolvedValue('random-md5');
|
||||
|
||||
await expect(ensureBrowserDownloaded(logger)).rejects.toBeInstanceOf(Error);
|
||||
});
|
||||
|
||||
describe('when archives are already present', () => {
|
||||
beforeEach(() => {
|
||||
mockFs(
|
||||
Object.fromEntries(
|
||||
chromium.paths.packages.map((packageInfo) => [
|
||||
chromium.paths.resolvePath(packageInfo),
|
||||
'',
|
||||
])
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
it('should not download again', async () => {
|
||||
await ensureBrowserDownloaded(logger);
|
||||
|
||||
expect(download).not.toHaveBeenCalled();
|
||||
const paths = [
|
||||
readdirSync(path.resolve(chromium.paths.archivesPath + '/x64')),
|
||||
readdirSync(path.resolve(chromium.paths.archivesPath + '/arm64')),
|
||||
];
|
||||
|
||||
expect(paths).toEqual([
|
||||
expect.arrayContaining([
|
||||
'chrome-win.zip',
|
||||
'chromium-70f5d88-linux_x64.zip',
|
||||
'chromium-d163fd7-darwin_x64.zip',
|
||||
]),
|
||||
expect.arrayContaining(['chromium-70f5d88-linux_arm64.zip']),
|
||||
]);
|
||||
});
|
||||
|
||||
it('should download again if md5 hash different', async () => {
|
||||
(md5 as jest.MockedFunction<typeof md5>).mockResolvedValueOnce('random-md5');
|
||||
await ensureBrowserDownloaded(logger);
|
||||
|
||||
expect(download).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,101 +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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { existsSync } from 'fs';
|
||||
import del from 'del';
|
||||
import { BrowserDownload, chromium } from '../';
|
||||
import { GenericLevelLogger } from '../../lib/level_logger';
|
||||
import { md5 } from './checksum';
|
||||
import { download } from './download';
|
||||
|
||||
/**
|
||||
* Check for the downloaded archive of each requested browser type and
|
||||
* download them if they are missing or their checksum is invalid
|
||||
*/
|
||||
export async function ensureBrowserDownloaded(logger: GenericLevelLogger) {
|
||||
await ensureDownloaded([chromium], logger);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the unexpected files in the browsers archivesPath
|
||||
* and ensures that all packages/archives are downloaded and
|
||||
* that their checksums match the declared value
|
||||
*/
|
||||
async function ensureDownloaded(browsers: BrowserDownload[], logger: GenericLevelLogger) {
|
||||
await Promise.all(
|
||||
browsers.map(async ({ paths: pSet }) => {
|
||||
const removedFiles = await del(`${pSet.archivesPath}/**/*`, {
|
||||
force: true,
|
||||
onlyFiles: true,
|
||||
ignore: pSet.getAllArchiveFilenames(),
|
||||
});
|
||||
|
||||
removedFiles.forEach((path) => {
|
||||
logger.warning(`Deleting unexpected file ${path}`);
|
||||
});
|
||||
|
||||
const invalidChecksums: string[] = [];
|
||||
await Promise.all(
|
||||
pSet.packages.map(async (p) => {
|
||||
const { archiveFilename, archiveChecksum } = p;
|
||||
if (archiveFilename && archiveChecksum) {
|
||||
const path = pSet.resolvePath(p);
|
||||
const pathExists = existsSync(path);
|
||||
|
||||
let foundChecksum: string;
|
||||
try {
|
||||
foundChecksum = await md5(path).catch();
|
||||
} catch {
|
||||
foundChecksum = 'MISSING';
|
||||
}
|
||||
|
||||
if (pathExists && foundChecksum === archiveChecksum) {
|
||||
logger.debug(`Browser archive for ${p.platform}/${p.architecture} found in ${path} `);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!pathExists) {
|
||||
logger.warning(
|
||||
`Browser archive for ${p.platform}/${p.architecture} not found in ${path}.`
|
||||
);
|
||||
}
|
||||
if (foundChecksum !== archiveChecksum) {
|
||||
logger.warning(
|
||||
`Browser archive checksum for ${p.platform}/${p.architecture} ` +
|
||||
`is ${foundChecksum} but ${archiveChecksum} was expected.`
|
||||
);
|
||||
}
|
||||
|
||||
const url = pSet.getDownloadUrl(p);
|
||||
try {
|
||||
const downloadedChecksum = await download(url, path, logger);
|
||||
if (downloadedChecksum !== archiveChecksum) {
|
||||
logger.warning(
|
||||
`Invalid checksum for ${p.platform}/${p.architecture}: ` +
|
||||
`expected ${archiveChecksum} got ${downloadedChecksum}`
|
||||
);
|
||||
invalidChecksums.push(`${url} => ${path}`);
|
||||
}
|
||||
} catch (err) {
|
||||
throw new Error(`Failed to download ${url}: ${err}`);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
if (invalidChecksums.length) {
|
||||
const err = new Error(
|
||||
`Error downloading browsers, checksums incorrect for:\n - ${invalidChecksums.join(
|
||||
'\n - '
|
||||
)}`
|
||||
);
|
||||
logger.error(err);
|
||||
throw err;
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
|
@ -1,8 +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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export { ensureBrowserDownloaded } from './ensure_downloaded';
|
|
@ -1,35 +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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { first } from 'rxjs/operators';
|
||||
import { ReportingCore } from '../';
|
||||
import { LevelLogger } from '../lib';
|
||||
import { chromium, ChromiumArchivePaths } from './chromium';
|
||||
import { HeadlessChromiumDriverFactory } from './chromium/driver_factory';
|
||||
import { installBrowser } from './install';
|
||||
|
||||
export { chromium } from './chromium';
|
||||
export { HeadlessChromiumDriver } from './chromium/driver';
|
||||
export { HeadlessChromiumDriverFactory } from './chromium/driver_factory';
|
||||
|
||||
type CreateDriverFactory = (
|
||||
core: ReportingCore,
|
||||
binaryPath: string,
|
||||
logger: LevelLogger
|
||||
) => HeadlessChromiumDriverFactory;
|
||||
|
||||
export interface BrowserDownload {
|
||||
createDriverFactory: CreateDriverFactory;
|
||||
paths: ChromiumArchivePaths;
|
||||
}
|
||||
|
||||
export const initializeBrowserDriverFactory = async (core: ReportingCore, logger: LevelLogger) => {
|
||||
const chromiumLogger = logger.clone(['chromium']);
|
||||
const { binaryPath$ } = installBrowser(chromiumLogger);
|
||||
const binaryPath = await binaryPath$.pipe(first()).toPromise();
|
||||
return chromium.createDriverFactory(core, binaryPath, chromiumLogger);
|
||||
};
|
|
@ -1,72 +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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import del from 'del';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import * as Rx from 'rxjs';
|
||||
import { GenericLevelLogger } from '../lib/level_logger';
|
||||
import { ChromiumArchivePaths } from './chromium';
|
||||
import { ensureBrowserDownloaded } from './download';
|
||||
import { md5 } from './download/checksum';
|
||||
import { extract } from './extract';
|
||||
|
||||
/**
|
||||
* "install" a browser by type into installs path by extracting the downloaded
|
||||
* archive. If there is an error extracting the archive an `ExtractError` is thrown
|
||||
*/
|
||||
export function installBrowser(
|
||||
logger: GenericLevelLogger,
|
||||
chromiumPath: string = path.resolve(__dirname, '../../chromium'),
|
||||
platform: string = process.platform,
|
||||
architecture: string = os.arch()
|
||||
): { binaryPath$: Rx.Subject<string> } {
|
||||
const binaryPath$ = new Rx.Subject<string>();
|
||||
|
||||
const paths = new ChromiumArchivePaths();
|
||||
const pkg = paths.find(platform, architecture);
|
||||
|
||||
if (!pkg) {
|
||||
throw new Error(`Unsupported platform: ${platform}-${architecture}`);
|
||||
}
|
||||
|
||||
const backgroundInstall = async () => {
|
||||
const binaryPath = paths.getBinaryPath(pkg);
|
||||
const binaryChecksum = await md5(binaryPath).catch(() => '');
|
||||
|
||||
if (binaryChecksum !== pkg.binaryChecksum) {
|
||||
logger.warning(
|
||||
`Found browser binary checksum for ${pkg.platform}/${pkg.architecture} ` +
|
||||
`is ${binaryChecksum} but ${pkg.binaryChecksum} was expected. Re-installing...`
|
||||
);
|
||||
try {
|
||||
await del(chromiumPath);
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
}
|
||||
|
||||
try {
|
||||
await ensureBrowserDownloaded(logger);
|
||||
const archive = path.join(paths.archivesPath, pkg.architecture, pkg.archiveFilename);
|
||||
logger.info(`Extracting [${archive}] to [${chromiumPath}]`);
|
||||
await extract(archive, chromiumPath);
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`Browser executable: ${binaryPath}`);
|
||||
|
||||
binaryPath$.next(binaryPath); // subscribers wait for download and extract to complete
|
||||
};
|
||||
|
||||
backgroundInstall();
|
||||
|
||||
return {
|
||||
binaryPath$,
|
||||
};
|
||||
}
|
|
@ -3,52 +3,8 @@
|
|||
exports[`Reporting Config Schema context {"dev":false,"dist":false} produces correct config 1`] = `
|
||||
Object {
|
||||
"capture": Object {
|
||||
"browser": Object {
|
||||
"autoDownload": true,
|
||||
"chromium": Object {
|
||||
"proxy": Object {
|
||||
"enabled": false,
|
||||
},
|
||||
},
|
||||
"type": "chromium",
|
||||
},
|
||||
"loadDelay": "PT3S",
|
||||
"maxAttempts": 1,
|
||||
"networkPolicy": Object {
|
||||
"enabled": true,
|
||||
"rules": Array [
|
||||
Object {
|
||||
"allow": true,
|
||||
"host": undefined,
|
||||
"protocol": "http:",
|
||||
},
|
||||
Object {
|
||||
"allow": true,
|
||||
"host": undefined,
|
||||
"protocol": "https:",
|
||||
},
|
||||
Object {
|
||||
"allow": true,
|
||||
"host": undefined,
|
||||
"protocol": "ws:",
|
||||
},
|
||||
Object {
|
||||
"allow": true,
|
||||
"host": undefined,
|
||||
"protocol": "wss:",
|
||||
},
|
||||
Object {
|
||||
"allow": true,
|
||||
"host": undefined,
|
||||
"protocol": "data:",
|
||||
},
|
||||
Object {
|
||||
"allow": false,
|
||||
"host": undefined,
|
||||
"protocol": undefined,
|
||||
},
|
||||
],
|
||||
},
|
||||
"timeouts": Object {
|
||||
"openUrl": "PT1M",
|
||||
"renderComplete": "PT30S",
|
||||
|
@ -101,53 +57,8 @@ Object {
|
|||
exports[`Reporting Config Schema context {"dev":false,"dist":true} produces correct config 1`] = `
|
||||
Object {
|
||||
"capture": Object {
|
||||
"browser": Object {
|
||||
"autoDownload": false,
|
||||
"chromium": Object {
|
||||
"inspect": false,
|
||||
"proxy": Object {
|
||||
"enabled": false,
|
||||
},
|
||||
},
|
||||
"type": "chromium",
|
||||
},
|
||||
"loadDelay": "PT3S",
|
||||
"maxAttempts": 3,
|
||||
"networkPolicy": Object {
|
||||
"enabled": true,
|
||||
"rules": Array [
|
||||
Object {
|
||||
"allow": true,
|
||||
"host": undefined,
|
||||
"protocol": "http:",
|
||||
},
|
||||
Object {
|
||||
"allow": true,
|
||||
"host": undefined,
|
||||
"protocol": "https:",
|
||||
},
|
||||
Object {
|
||||
"allow": true,
|
||||
"host": undefined,
|
||||
"protocol": "ws:",
|
||||
},
|
||||
Object {
|
||||
"allow": true,
|
||||
"host": undefined,
|
||||
"protocol": "wss:",
|
||||
},
|
||||
Object {
|
||||
"allow": true,
|
||||
"host": undefined,
|
||||
"protocol": "data:",
|
||||
},
|
||||
Object {
|
||||
"allow": false,
|
||||
"host": undefined,
|
||||
"protocol": undefined,
|
||||
},
|
||||
],
|
||||
},
|
||||
"timeouts": Object {
|
||||
"openUrl": "PT1M",
|
||||
"renderComplete": "PT30S",
|
||||
|
|
|
@ -77,13 +77,6 @@ describe('Reporting server createConfig$', () => {
|
|||
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"capture": Object {
|
||||
"browser": Object {
|
||||
"chromium": Object {
|
||||
"disableSandbox": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
"csv": Object {},
|
||||
"encryptionKey": "iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii",
|
||||
"index": ".reporting",
|
||||
|
@ -106,47 +99,6 @@ describe('Reporting server createConfig$', () => {
|
|||
expect(mockLogger.warn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('uses user-provided disableSandbox: false', async () => {
|
||||
mockInitContext = coreMock.createPluginInitializerContext(
|
||||
createMockConfigSchema({
|
||||
encryptionKey: '888888888888888888888888888888888',
|
||||
capture: { browser: { chromium: { disableSandbox: false } } },
|
||||
})
|
||||
);
|
||||
const mockConfig$ = createMockConfig(mockInitContext);
|
||||
const result = await createConfig$(mockCoreSetup, mockConfig$, mockLogger).toPromise();
|
||||
|
||||
expect(result.capture.browser.chromium).toMatchObject({ disableSandbox: false });
|
||||
expect(mockLogger.warn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('uses user-provided disableSandbox: true', async () => {
|
||||
mockInitContext = coreMock.createPluginInitializerContext(
|
||||
createMockConfigSchema({
|
||||
encryptionKey: '888888888888888888888888888888888',
|
||||
capture: { browser: { chromium: { disableSandbox: true } } },
|
||||
})
|
||||
);
|
||||
const mockConfig$ = createMockConfig(mockInitContext);
|
||||
const result = await createConfig$(mockCoreSetup, mockConfig$, mockLogger).toPromise();
|
||||
|
||||
expect(result.capture.browser.chromium).toMatchObject({ disableSandbox: true });
|
||||
expect(mockLogger.warn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('provides a default for disableSandbox', async () => {
|
||||
mockInitContext = coreMock.createPluginInitializerContext(
|
||||
createMockConfigSchema({
|
||||
encryptionKey: '888888888888888888888888888888888',
|
||||
})
|
||||
);
|
||||
const mockConfig$ = createMockConfig(mockInitContext);
|
||||
const result = await createConfig$(mockCoreSetup, mockConfig$, mockLogger).toPromise();
|
||||
|
||||
expect(result.capture.browser.chromium).toMatchObject({ disableSandbox: expect.any(Boolean) });
|
||||
expect(mockLogger.warn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each(['0', '0.0', '0.0.0', '0.0.0.0', '0000:0000:0000:0000:0000:0000:0000:0000', '::'])(
|
||||
`apply failover logic when hostname is given as "%s"`,
|
||||
async (hostname) => {
|
||||
|
|
|
@ -7,17 +7,15 @@
|
|||
|
||||
import crypto from 'crypto';
|
||||
import ipaddr from 'ipaddr.js';
|
||||
import { sum, upperFirst } from 'lodash';
|
||||
import { sum } from 'lodash';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map, mergeMap } from 'rxjs/operators';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { CoreSetup } from 'src/core/server';
|
||||
import { LevelLogger } from '../lib';
|
||||
import { getDefaultChromiumSandboxDisabled } from './default_chromium_sandbox_disabled';
|
||||
import { ReportingConfigType } from './schema';
|
||||
|
||||
/*
|
||||
* Set up dynamic config defaults
|
||||
* - xpack.capture.browser.chromium.disableSandbox
|
||||
* - xpack.kibanaServer
|
||||
* - xpack.reporting.encryptionKey
|
||||
*/
|
||||
|
@ -71,41 +69,6 @@ export function createConfig$(
|
|||
protocol: kibanaServerProtocol,
|
||||
},
|
||||
};
|
||||
}),
|
||||
mergeMap(async (config) => {
|
||||
if (config.capture.browser.chromium.disableSandbox != null) {
|
||||
// disableSandbox was set by user
|
||||
return { ...config };
|
||||
}
|
||||
|
||||
// disableSandbox was not set by user, apply default for OS
|
||||
const { os, disableSandbox } = await getDefaultChromiumSandboxDisabled();
|
||||
const osName = [os.os, os.dist, os.release].filter(Boolean).map(upperFirst).join(' ');
|
||||
|
||||
logger.debug(`Running on OS: '{osName}'`);
|
||||
|
||||
if (disableSandbox === true) {
|
||||
logger.warn(
|
||||
`Chromium sandbox provides an additional layer of protection, but is not supported for ${osName} OS.` +
|
||||
` Automatically setting 'xpack.reporting.capture.browser.chromium.disableSandbox: true'.`
|
||||
);
|
||||
} else {
|
||||
logger.info(
|
||||
`Chromium sandbox provides an additional layer of protection, and is supported for ${osName} OS.` +
|
||||
` Automatically enabling Chromium sandbox.`
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
...config,
|
||||
capture: {
|
||||
...config.capture,
|
||||
browser: {
|
||||
...config.capture.browser,
|
||||
chromium: { ...config.capture.browser.chromium, disableSandbox },
|
||||
},
|
||||
},
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,39 +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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
jest.mock('getos', () => {
|
||||
return jest.fn();
|
||||
});
|
||||
|
||||
import { getDefaultChromiumSandboxDisabled } from './default_chromium_sandbox_disabled';
|
||||
import getos from 'getos';
|
||||
|
||||
interface TestObject {
|
||||
os: string;
|
||||
dist?: string;
|
||||
release?: string;
|
||||
}
|
||||
|
||||
function defaultTest(os: TestObject, expectedDefault: boolean) {
|
||||
test(`${expectedDefault ? 'disabled' : 'enabled'} on ${JSON.stringify(os)}`, async () => {
|
||||
(getos as jest.Mock).mockImplementation((cb) => cb(null, os));
|
||||
const actualDefault = await getDefaultChromiumSandboxDisabled();
|
||||
expect(actualDefault.disableSandbox).toBe(expectedDefault);
|
||||
});
|
||||
}
|
||||
|
||||
defaultTest({ os: 'win32' }, false);
|
||||
defaultTest({ os: 'darwin' }, false);
|
||||
defaultTest({ os: 'linux', dist: 'Centos', release: '7.0' }, true);
|
||||
defaultTest({ os: 'linux', dist: 'Red Hat Linux', release: '7.0' }, true);
|
||||
defaultTest({ os: 'linux', dist: 'Ubuntu Linux', release: '14.04' }, false);
|
||||
defaultTest({ os: 'linux', dist: 'Ubuntu Linux', release: '16.04' }, false);
|
||||
defaultTest({ os: 'linux', dist: 'SUSE Linux', release: '11' }, false);
|
||||
defaultTest({ os: 'linux', dist: 'SUSE Linux', release: '12' }, false);
|
||||
defaultTest({ os: 'linux', dist: 'SUSE Linux', release: '42.0' }, false);
|
||||
defaultTest({ os: 'linux', dist: 'Debian', release: '8' }, true);
|
||||
defaultTest({ os: 'linux', dist: 'Debian', release: '9' }, true);
|
|
@ -19,6 +19,7 @@ export const config: PluginConfigDescriptor<ReportingConfigType> = {
|
|||
schema: ConfigSchema,
|
||||
deprecations: ({ unused }) => [
|
||||
unused('capture.browser.chromium.maxScreenshotDimension', { level: 'warning' }), // unused since 7.8
|
||||
unused('capture.browser.type'),
|
||||
unused('poll.jobCompletionNotifier.intervalErrorMultiplier', { level: 'warning' }), // unused since 7.10
|
||||
unused('poll.jobsRefresh.intervalErrorMultiplier', { level: 'warning' }), // unused since 7.10
|
||||
unused('capture.viewport', { level: 'warning' }), // deprecated as unused since 7.16
|
||||
|
@ -72,7 +73,6 @@ export const config: PluginConfigDescriptor<ReportingConfigType> = {
|
|||
capture: {
|
||||
maxAttempts: true,
|
||||
timeouts: { openUrl: true, renderComplete: true, waitForElements: true },
|
||||
networkPolicy: false, // show as [redacted]
|
||||
zoom: true,
|
||||
},
|
||||
csv: { maxSizeBytes: true, scroll: { size: true, duration: true } },
|
||||
|
|
|
@ -55,47 +55,12 @@ describe('Reporting Config Schema', () => {
|
|||
).toBe('qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq');
|
||||
|
||||
expect(ConfigSchema.validate({ encryptionKey: 'weaksauce' }).encryptionKey).toBe('weaksauce');
|
||||
|
||||
// disableSandbox
|
||||
expect(
|
||||
ConfigSchema.validate({ capture: { browser: { chromium: { disableSandbox: true } } } })
|
||||
.capture.browser.chromium
|
||||
).toMatchObject({ disableSandbox: true, proxy: { enabled: false } });
|
||||
|
||||
// kibanaServer
|
||||
expect(
|
||||
ConfigSchema.validate({ kibanaServer: { hostname: 'Frodo' } }).kibanaServer
|
||||
).toMatchObject({ hostname: 'Frodo' });
|
||||
});
|
||||
|
||||
it('allows setting a wildcard for chrome proxy bypass', () => {
|
||||
expect(
|
||||
ConfigSchema.validate({
|
||||
capture: {
|
||||
browser: {
|
||||
chromium: {
|
||||
proxy: {
|
||||
enabled: true,
|
||||
server: 'http://example.com:8080',
|
||||
bypass: ['*.example.com', '*bar.example.com', 'bats.example.com'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}).capture.browser.chromium.proxy
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"bypass": Array [
|
||||
"*.example.com",
|
||||
"*bar.example.com",
|
||||
"bats.example.com",
|
||||
],
|
||||
"enabled": true,
|
||||
"server": "http://example.com:8080",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it.each(['0', '0.0', '0.0.0'])(
|
||||
`fails to validate "kibanaServer.hostname" with an invalid hostname: "%s"`,
|
||||
(address) => {
|
||||
|
|
|
@ -46,20 +46,6 @@ const QueueSchema = schema.object({
|
|||
}),
|
||||
});
|
||||
|
||||
const RulesSchema = schema.object({
|
||||
allow: schema.boolean(),
|
||||
host: schema.maybe(schema.string()),
|
||||
protocol: schema.maybe(
|
||||
schema.string({
|
||||
validate(value) {
|
||||
if (!/:$/.test(value)) {
|
||||
return 'must end in colon';
|
||||
}
|
||||
},
|
||||
})
|
||||
),
|
||||
});
|
||||
|
||||
const CaptureSchema = schema.object({
|
||||
timeouts: schema.object({
|
||||
openUrl: schema.oneOf([schema.number(), schema.duration()], {
|
||||
|
@ -72,56 +58,10 @@ const CaptureSchema = schema.object({
|
|||
defaultValue: moment.duration({ seconds: 30 }),
|
||||
}),
|
||||
}),
|
||||
networkPolicy: schema.object({
|
||||
enabled: schema.boolean({ defaultValue: true }),
|
||||
rules: schema.arrayOf(RulesSchema, {
|
||||
defaultValue: [
|
||||
{ host: undefined, allow: true, protocol: 'http:' },
|
||||
{ host: undefined, allow: true, protocol: 'https:' },
|
||||
{ host: undefined, allow: true, protocol: 'ws:' },
|
||||
{ host: undefined, allow: true, protocol: 'wss:' },
|
||||
{ host: undefined, allow: true, protocol: 'data:' },
|
||||
{ host: undefined, allow: false, protocol: undefined }, // Default action is to deny!
|
||||
],
|
||||
}),
|
||||
}),
|
||||
zoom: schema.number({ defaultValue: 2 }),
|
||||
loadDelay: schema.oneOf([schema.number(), schema.duration()], {
|
||||
defaultValue: moment.duration({ seconds: 3 }),
|
||||
}),
|
||||
browser: schema.object({
|
||||
autoDownload: schema.conditional(
|
||||
schema.contextRef('dist'),
|
||||
true,
|
||||
schema.boolean({ defaultValue: false }),
|
||||
schema.boolean({ defaultValue: true })
|
||||
),
|
||||
chromium: schema.object({
|
||||
inspect: schema.conditional(
|
||||
schema.contextRef('dist'),
|
||||
true,
|
||||
schema.boolean({ defaultValue: false }),
|
||||
schema.maybe(schema.never())
|
||||
),
|
||||
disableSandbox: schema.maybe(schema.boolean()), // default value is dynamic in createConfig$
|
||||
proxy: schema.object({
|
||||
enabled: schema.boolean({ defaultValue: false }),
|
||||
server: schema.conditional(
|
||||
schema.siblingRef('enabled'),
|
||||
true,
|
||||
schema.uri({ scheme: ['http', 'https'] }),
|
||||
schema.maybe(schema.never())
|
||||
),
|
||||
bypass: schema.conditional(
|
||||
schema.siblingRef('enabled'),
|
||||
true,
|
||||
schema.arrayOf(schema.string()),
|
||||
schema.maybe(schema.never())
|
||||
),
|
||||
}),
|
||||
}),
|
||||
type: schema.string({ defaultValue: 'chromium' }),
|
||||
}),
|
||||
maxAttempts: schema.conditional(
|
||||
schema.contextRef('dist'),
|
||||
true,
|
||||
|
|
|
@ -7,8 +7,8 @@
|
|||
|
||||
import Hapi from '@hapi/hapi';
|
||||
import * as Rx from 'rxjs';
|
||||
import { filter, first, map, take } from 'rxjs/operators';
|
||||
import { ScreenshotModePluginSetup } from 'src/plugins/screenshot_mode/server';
|
||||
import { filter, first, map, switchMap, take } from 'rxjs/operators';
|
||||
import type { ScreenshottingStart, ScreenshotResult } from '../../screenshotting/server';
|
||||
import {
|
||||
BasePath,
|
||||
IClusterClient,
|
||||
|
@ -28,13 +28,14 @@ import { SecurityPluginSetup } from '../../security/server';
|
|||
import { DEFAULT_SPACE_ID } from '../../spaces/common/constants';
|
||||
import { SpacesPluginSetup } from '../../spaces/server';
|
||||
import { TaskManagerSetupContract, TaskManagerStartContract } from '../../task_manager/server';
|
||||
import { REPORTING_REDIRECT_LOCATOR_STORE_KEY } from '../common/constants';
|
||||
import { durationToNumber } from '../common/schema_utils';
|
||||
import { ReportingConfig, ReportingSetup } from './';
|
||||
import { HeadlessChromiumDriverFactory } from './browsers/chromium/driver_factory';
|
||||
import { ReportingConfigType } from './config';
|
||||
import { checkLicense, getExportTypesRegistry, LevelLogger } from './lib';
|
||||
import { ReportingStore } from './lib/store';
|
||||
import { ExecuteReportTask, MonitorReportsTask, ReportTaskParams } from './lib/tasks';
|
||||
import { ReportingPluginRouter } from './types';
|
||||
import { ReportingPluginRouter, ScreenshotOptions } from './types';
|
||||
|
||||
export interface ReportingInternalSetup {
|
||||
basePath: Pick<BasePath, 'set'>;
|
||||
|
@ -44,13 +45,11 @@ export interface ReportingInternalSetup {
|
|||
security?: SecurityPluginSetup;
|
||||
spaces?: SpacesPluginSetup;
|
||||
taskManager: TaskManagerSetupContract;
|
||||
screenshotMode: ScreenshotModePluginSetup;
|
||||
logger: LevelLogger;
|
||||
status: StatusServiceSetup;
|
||||
}
|
||||
|
||||
export interface ReportingInternalStart {
|
||||
browserDriverFactory: HeadlessChromiumDriverFactory;
|
||||
store: ReportingStore;
|
||||
savedObjects: SavedObjectsServiceStart;
|
||||
uiSettings: UiSettingsServiceStart;
|
||||
|
@ -58,6 +57,7 @@ export interface ReportingInternalStart {
|
|||
data: DataPluginStart;
|
||||
taskManager: TaskManagerStartContract;
|
||||
logger: LevelLogger;
|
||||
screenshotting: ScreenshottingStart;
|
||||
}
|
||||
|
||||
export class ReportingCore {
|
||||
|
@ -253,18 +253,6 @@ export class ReportingCore {
|
|||
.toPromise();
|
||||
}
|
||||
|
||||
private getScreenshotModeDep() {
|
||||
return this.getPluginSetupDeps().screenshotMode;
|
||||
}
|
||||
|
||||
public getEnableScreenshotMode() {
|
||||
return this.getScreenshotModeDep().setScreenshotModeEnabled;
|
||||
}
|
||||
|
||||
public getSetScreenshotLayout() {
|
||||
return this.getScreenshotModeDep().setScreenshotLayout;
|
||||
}
|
||||
|
||||
/*
|
||||
* Gives synchronous access to the setupDeps
|
||||
*/
|
||||
|
@ -350,6 +338,35 @@ export class ReportingCore {
|
|||
return startDeps.esClient;
|
||||
}
|
||||
|
||||
public getScreenshots(options: ScreenshotOptions): Rx.Observable<ScreenshotResult> {
|
||||
return Rx.defer(() => this.getPluginStartDeps()).pipe(
|
||||
switchMap(({ screenshotting }) => {
|
||||
const config = this.getConfig();
|
||||
return screenshotting.getScreenshots({
|
||||
...options,
|
||||
|
||||
timeouts: {
|
||||
loadDelay: durationToNumber(config.get('capture', 'loadDelay')),
|
||||
openUrl: durationToNumber(config.get('capture', 'timeouts', 'openUrl')),
|
||||
waitForElements: durationToNumber(config.get('capture', 'timeouts', 'waitForElements')),
|
||||
renderComplete: durationToNumber(config.get('capture', 'timeouts', 'renderComplete')),
|
||||
},
|
||||
|
||||
layout: {
|
||||
zoom: config.get('capture', 'zoom'),
|
||||
...options.layout,
|
||||
},
|
||||
|
||||
urls: options.urls.map((url) =>
|
||||
typeof url === 'string'
|
||||
? url
|
||||
: [url[0], { [REPORTING_REDIRECT_LOCATOR_STORE_KEY]: url[1] }]
|
||||
),
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public trackReport(reportId: string) {
|
||||
this.executing.add(reportId);
|
||||
}
|
||||
|
|
|
@ -8,70 +8,60 @@
|
|||
import apm from 'elastic-apm-node';
|
||||
import * as Rx from 'rxjs';
|
||||
import { finalize, map, tap } from 'rxjs/operators';
|
||||
import { LayoutTypes } from '../../../../screenshotting/common';
|
||||
import { REPORTING_TRANSACTION_TYPE } from '../../../common/constants';
|
||||
import { ReportingCore } from '../../';
|
||||
import { UrlOrUrlLocatorTuple } from '../../../common/types';
|
||||
import { ScreenshotOptions } from '../../types';
|
||||
import { LevelLogger } from '../../lib';
|
||||
import { LayoutParams, LayoutSelectorDictionary, PreserveLayout } from '../../lib/layouts';
|
||||
import { getScreenshots$, ScreenshotResults } from '../../lib/screenshots';
|
||||
import { ConditionalHeaders } from '../common';
|
||||
|
||||
export async function generatePngObservableFactory(reporting: ReportingCore) {
|
||||
const config = reporting.getConfig();
|
||||
const captureConfig = config.get('capture');
|
||||
const { browserDriverFactory } = await reporting.getPluginStartDeps();
|
||||
|
||||
return function generatePngObservable(
|
||||
logger: LevelLogger,
|
||||
urlOrUrlLocatorTuple: UrlOrUrlLocatorTuple,
|
||||
browserTimezone: string | undefined,
|
||||
conditionalHeaders: ConditionalHeaders,
|
||||
layoutParams: LayoutParams & { selectors?: Partial<LayoutSelectorDictionary> }
|
||||
): Rx.Observable<{ buffer: Buffer; warnings: string[] }> {
|
||||
const apmTrans = apm.startTransaction('generate-png', REPORTING_TRANSACTION_TYPE);
|
||||
const apmLayout = apmTrans?.startSpan('create-layout', 'setup');
|
||||
if (!layoutParams || !layoutParams.dimensions) {
|
||||
throw new Error(`LayoutParams.Dimensions is undefined.`);
|
||||
}
|
||||
const layout = new PreserveLayout(layoutParams.dimensions, layoutParams.selectors);
|
||||
|
||||
if (apmLayout) apmLayout.end();
|
||||
|
||||
const apmScreenshots = apmTrans?.startSpan('screenshots-pipeline', 'setup');
|
||||
let apmBuffer: typeof apm.currentSpan;
|
||||
const screenshots$ = getScreenshots$(captureConfig, browserDriverFactory, {
|
||||
logger,
|
||||
urlsOrUrlLocatorTuples: [urlOrUrlLocatorTuple],
|
||||
conditionalHeaders,
|
||||
layout,
|
||||
browserTimezone,
|
||||
}).pipe(
|
||||
tap(() => {
|
||||
apmScreenshots?.end();
|
||||
apmBuffer = apmTrans?.startSpan('get-buffer', 'output') ?? null;
|
||||
}),
|
||||
map((results: ScreenshotResults[]) => ({
|
||||
buffer: results[0].screenshots[0].data,
|
||||
warnings: results.reduce((found, current) => {
|
||||
if (current.error) {
|
||||
found.push(current.error.message);
|
||||
}
|
||||
if (current.renderErrors) {
|
||||
found.push(...current.renderErrors);
|
||||
}
|
||||
return found;
|
||||
}, [] as string[]),
|
||||
})),
|
||||
tap(({ buffer }) => {
|
||||
logger.debug(`PNG buffer byte length: ${buffer.byteLength}`);
|
||||
apmTrans?.setLabel('byte-length', buffer.byteLength, false);
|
||||
}),
|
||||
finalize(() => {
|
||||
apmBuffer?.end();
|
||||
apmTrans?.end();
|
||||
})
|
||||
);
|
||||
|
||||
return screenshots$;
|
||||
export function generatePngObservable(
|
||||
reporting: ReportingCore,
|
||||
logger: LevelLogger,
|
||||
options: ScreenshotOptions
|
||||
): Rx.Observable<{ buffer: Buffer; warnings: string[] }> {
|
||||
const apmTrans = apm.startTransaction('generate-png', REPORTING_TRANSACTION_TYPE);
|
||||
const apmLayout = apmTrans?.startSpan('create-layout', 'setup');
|
||||
if (!options.layout.dimensions) {
|
||||
throw new Error(`LayoutParams.Dimensions is undefined.`);
|
||||
}
|
||||
const layout = {
|
||||
id: LayoutTypes.PRESERVE_LAYOUT,
|
||||
...options.layout,
|
||||
};
|
||||
|
||||
apmLayout?.end();
|
||||
|
||||
const apmScreenshots = apmTrans?.startSpan('screenshots-pipeline', 'setup');
|
||||
let apmBuffer: typeof apm.currentSpan;
|
||||
|
||||
return reporting.getScreenshots({ ...options, layout }).pipe(
|
||||
tap(({ metrics$ }) => {
|
||||
metrics$.subscribe(({ cpu, memory }) => {
|
||||
apmTrans?.setLabel('cpu', cpu, false);
|
||||
apmTrans?.setLabel('memory', memory, false);
|
||||
});
|
||||
apmScreenshots?.end();
|
||||
apmBuffer = apmTrans?.startSpan('get-buffer', 'output') ?? null;
|
||||
}),
|
||||
map(({ results }) => ({
|
||||
buffer: results[0].screenshots[0].data,
|
||||
warnings: results.reduce((found, current) => {
|
||||
if (current.error) {
|
||||
found.push(current.error.message);
|
||||
}
|
||||
if (current.renderErrors) {
|
||||
found.push(...current.renderErrors);
|
||||
}
|
||||
return found;
|
||||
}, [] as string[]),
|
||||
})),
|
||||
tap(({ buffer }) => {
|
||||
logger.debug(`PNG buffer byte length: ${buffer.byteLength}`);
|
||||
apmTrans?.setLabel('byte-length', buffer.byteLength, false);
|
||||
}),
|
||||
finalize(() => {
|
||||
apmBuffer?.end();
|
||||
apmTrans?.end();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ export { getConditionalHeaders } from './get_conditional_headers';
|
|||
export { getFullUrls } from './get_full_urls';
|
||||
export { omitBlockedHeaders } from './omit_blocked_headers';
|
||||
export { validateUrls } from './validate_urls';
|
||||
export { generatePngObservableFactory } from './generate_png';
|
||||
export { generatePngObservable } from './generate_png';
|
||||
export { getCustomLogo } from './get_custom_logo';
|
||||
|
||||
export interface TimeRangeParams {
|
||||
|
|
|
@ -13,12 +13,12 @@ import {
|
|||
StyleDictionary,
|
||||
TDocumentDefinitions,
|
||||
} from 'pdfmake/interfaces';
|
||||
import { LayoutInstance } from '../../../lib/layouts';
|
||||
import type { Layout } from '../../../../../screenshotting/server';
|
||||
import { REPORTING_TABLE_LAYOUT } from './get_doc_options';
|
||||
import { getFont } from './get_font';
|
||||
|
||||
export function getTemplate(
|
||||
layout: LayoutInstance,
|
||||
layout: Layout,
|
||||
logo: string | undefined,
|
||||
title: string,
|
||||
tableBorderWidth: number,
|
||||
|
|
|
@ -5,8 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { PreserveLayout, PrintLayout } from '../../../lib/layouts';
|
||||
import { createMockConfig, createMockConfigSchema } from '../../../test_helpers';
|
||||
import { createMockLayout } from '../../../../../screenshotting/server/layouts/mock';
|
||||
import { PdfMaker } from './';
|
||||
|
||||
const imageBase64 = Buffer.from(
|
||||
|
@ -16,66 +15,22 @@ const imageBase64 = Buffer.from(
|
|||
|
||||
// FLAKY: https://github.com/elastic/kibana/issues/118484
|
||||
describe.skip('PdfMaker', () => {
|
||||
it('makes PDF using PrintLayout mode', async () => {
|
||||
const config = createMockConfig(createMockConfigSchema());
|
||||
const layout = new PrintLayout(config.get('capture'));
|
||||
const pdf = new PdfMaker(layout, undefined);
|
||||
let layout: ReturnType<typeof createMockLayout>;
|
||||
let pdf: PdfMaker;
|
||||
|
||||
expect(pdf.setTitle('the best PDF in the world')).toBe(undefined);
|
||||
expect([
|
||||
pdf.addImage(imageBase64, { title: 'first viz', description: '☃️' }),
|
||||
pdf.addImage(imageBase64, { title: 'second viz', description: '❄️' }),
|
||||
]).toEqual([undefined, undefined]);
|
||||
|
||||
const { _layout: testLayout, _title: testTitle } = pdf as unknown as {
|
||||
_layout: object;
|
||||
_title: string;
|
||||
};
|
||||
expect(testLayout).toMatchObject({
|
||||
captureConfig: { browser: { chromium: { disableSandbox: true } } }, // NOTE: irrelevant data?
|
||||
groupCount: 2,
|
||||
id: 'print',
|
||||
selectors: {
|
||||
itemsCountAttribute: 'data-shared-items-count',
|
||||
renderComplete: '[data-shared-item]',
|
||||
screenshot: '[data-shared-item]',
|
||||
timefilterDurationAttribute: 'data-shared-timefilter-duration',
|
||||
},
|
||||
});
|
||||
expect(testTitle).toBe('the best PDF in the world');
|
||||
|
||||
// generate buffer
|
||||
pdf.generate();
|
||||
const result = await pdf.getBuffer();
|
||||
expect(Buffer.isBuffer(result)).toBe(true);
|
||||
beforeEach(() => {
|
||||
layout = createMockLayout();
|
||||
pdf = new PdfMaker(layout, undefined);
|
||||
});
|
||||
|
||||
it('makes PDF using PreserveLayout mode', async () => {
|
||||
const layout = new PreserveLayout({ width: 400, height: 300 });
|
||||
const pdf = new PdfMaker(layout, undefined);
|
||||
describe('getBuffer', () => {
|
||||
it('should generate PDF buffer', async () => {
|
||||
pdf.setTitle('the best PDF in the world');
|
||||
pdf.addImage(imageBase64, { title: 'first viz', description: '☃️' });
|
||||
pdf.addImage(imageBase64, { title: 'second viz', description: '❄️' });
|
||||
pdf.generate();
|
||||
|
||||
expect(pdf.setTitle('the finest PDF in the world')).toBe(undefined);
|
||||
expect(pdf.addImage(imageBase64, { title: 'cool times', description: '☃️' })).toBe(undefined);
|
||||
|
||||
const { _layout: testLayout, _title: testTitle } = pdf as unknown as {
|
||||
_layout: object;
|
||||
_title: string;
|
||||
};
|
||||
expect(testLayout).toMatchObject({
|
||||
groupCount: 1,
|
||||
id: 'preserve_layout',
|
||||
selectors: {
|
||||
itemsCountAttribute: 'data-shared-items-count',
|
||||
renderComplete: '[data-shared-item]',
|
||||
screenshot: '[data-shared-items-container]',
|
||||
timefilterDurationAttribute: 'data-shared-timefilter-duration',
|
||||
},
|
||||
await expect(pdf.getBuffer()).resolves.toBeInstanceOf(Buffer);
|
||||
});
|
||||
expect(testTitle).toBe('the finest PDF in the world');
|
||||
|
||||
// generate buffer
|
||||
pdf.generate();
|
||||
const result = await pdf.getBuffer();
|
||||
expect(Buffer.isBuffer(result)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -12,7 +12,7 @@ import _ from 'lodash';
|
|||
import path from 'path';
|
||||
import Printer from 'pdfmake';
|
||||
import { Content, ContentImage, ContentText } from 'pdfmake/interfaces';
|
||||
import { LayoutInstance } from '../../../lib/layouts';
|
||||
import type { Layout } from '../../../../../screenshotting/server';
|
||||
import { getDocOptions, REPORTING_TABLE_LAYOUT } from './get_doc_options';
|
||||
import { getFont } from './get_font';
|
||||
import { getTemplate } from './get_template';
|
||||
|
@ -21,14 +21,14 @@ const assetPath = path.resolve(__dirname, '..', '..', 'common', 'assets');
|
|||
const tableBorderWidth = 1;
|
||||
|
||||
export class PdfMaker {
|
||||
private _layout: LayoutInstance;
|
||||
private _layout: Layout;
|
||||
private _logo: string | undefined;
|
||||
private _title: string;
|
||||
private _content: Content[];
|
||||
private _printer: Printer;
|
||||
private _pdfDoc: PDFKit.PDFDocument | undefined;
|
||||
|
||||
constructor(layout: LayoutInstance, logo: string | undefined) {
|
||||
constructor(layout: Layout, logo: string | undefined) {
|
||||
const fontPath = (filename: string) => path.resolve(assetPath, 'fonts', filename);
|
||||
const fonts = {
|
||||
Roboto: {
|
||||
|
|
|
@ -15,11 +15,11 @@ import {
|
|||
createMockConfigSchema,
|
||||
createMockReportingCore,
|
||||
} from '../../../test_helpers';
|
||||
import { generatePngObservableFactory } from '../../common';
|
||||
import { generatePngObservable } from '../../common';
|
||||
import { TaskPayloadPNG } from '../types';
|
||||
import { runTaskFnFactory } from './';
|
||||
|
||||
jest.mock('../../common/generate_png', () => ({ generatePngObservableFactory: jest.fn() }));
|
||||
jest.mock('../../common/generate_png');
|
||||
|
||||
let content: string;
|
||||
let mockReporting: ReportingCore;
|
||||
|
@ -61,16 +61,13 @@ beforeEach(async () => {
|
|||
|
||||
mockReporting = await createMockReportingCore(mockReportingConfig);
|
||||
mockReporting.setConfig(createMockConfig(mockReportingConfig));
|
||||
|
||||
(generatePngObservableFactory as jest.Mock).mockReturnValue(jest.fn());
|
||||
});
|
||||
|
||||
afterEach(() => (generatePngObservableFactory as jest.Mock).mockReset());
|
||||
afterEach(() => (generatePngObservable as jest.Mock).mockReset());
|
||||
|
||||
test(`passes browserTimezone to generatePng`, async () => {
|
||||
const encryptedHeaders = await encryptHeaders({});
|
||||
const generatePngObservable = (await generatePngObservableFactory(mockReporting)) as jest.Mock;
|
||||
generatePngObservable.mockReturnValue(Rx.of({ buffer: Buffer.from('') }));
|
||||
(generatePngObservable as jest.Mock).mockReturnValue(Rx.of({ buffer: Buffer.from('') }));
|
||||
|
||||
const runTask = await runTaskFnFactory(mockReporting, getMockLogger());
|
||||
const browserTimezone = 'UTC';
|
||||
|
@ -85,42 +82,24 @@ test(`passes browserTimezone to generatePng`, async () => {
|
|||
stream
|
||||
);
|
||||
|
||||
expect(generatePngObservable.mock.calls).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
LevelLogger {
|
||||
"_logger": Object {
|
||||
"get": [MockFunction],
|
||||
},
|
||||
"_tags": Array [
|
||||
"PNG",
|
||||
"execute",
|
||||
"pngJobId",
|
||||
],
|
||||
"warning": [Function],
|
||||
},
|
||||
"localhost:80undefined/app/kibana#/something",
|
||||
"UTC",
|
||||
Object {
|
||||
"conditions": Object {
|
||||
"basePath": undefined,
|
||||
"hostname": "localhost",
|
||||
"port": 80,
|
||||
"protocol": undefined,
|
||||
},
|
||||
"headers": Object {},
|
||||
},
|
||||
undefined,
|
||||
],
|
||||
]
|
||||
`);
|
||||
expect(generatePngObservable).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
expect.objectContaining({
|
||||
urls: ['localhost:80undefined/app/kibana#/something'],
|
||||
browserTimezone: 'UTC',
|
||||
conditionalHeaders: expect.objectContaining({
|
||||
conditions: expect.any(Object),
|
||||
headers: {},
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test(`returns content_type of application/png`, async () => {
|
||||
const runTask = await runTaskFnFactory(mockReporting, getMockLogger());
|
||||
const encryptedHeaders = await encryptHeaders({});
|
||||
|
||||
const generatePngObservable = await generatePngObservableFactory(mockReporting);
|
||||
(generatePngObservable as jest.Mock).mockReturnValue(Rx.of({ buffer: Buffer.from('foo') }));
|
||||
|
||||
const { content_type: contentType } = await runTask(
|
||||
|
@ -134,7 +113,6 @@ test(`returns content_type of application/png`, async () => {
|
|||
|
||||
test(`returns content of generatePng`, async () => {
|
||||
const testContent = 'raw string from get_screenhots';
|
||||
const generatePngObservable = await generatePngObservableFactory(mockReporting);
|
||||
(generatePngObservable as jest.Mock).mockReturnValue(Rx.of({ buffer: Buffer.from(testContent) }));
|
||||
|
||||
const runTask = await runTaskFnFactory(mockReporting, getMockLogger());
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import apm from 'elastic-apm-node';
|
||||
import * as Rx from 'rxjs';
|
||||
import { catchError, finalize, map, mergeMap, takeUntil, tap } from 'rxjs/operators';
|
||||
import { finalize, map, mergeMap, takeUntil, tap } from 'rxjs/operators';
|
||||
import { PNG_JOB_TYPE, REPORTING_TRANSACTION_TYPE } from '../../../../common/constants';
|
||||
import { TaskRunResult } from '../../../lib/tasks';
|
||||
import { RunTaskFn, RunTaskFnFactory } from '../../../types';
|
||||
|
@ -16,7 +16,7 @@ import {
|
|||
getConditionalHeaders,
|
||||
getFullUrls,
|
||||
omitBlockedHeaders,
|
||||
generatePngObservableFactory,
|
||||
generatePngObservable,
|
||||
} from '../../common';
|
||||
import { TaskPayloadPNG } from '../types';
|
||||
|
||||
|
@ -25,40 +25,35 @@ export const runTaskFnFactory: RunTaskFnFactory<RunTaskFn<TaskPayloadPNG>> =
|
|||
const config = reporting.getConfig();
|
||||
const encryptionKey = config.get('encryptionKey');
|
||||
|
||||
return async function runTask(jobId, job, cancellationToken, stream) {
|
||||
return function runTask(jobId, job, cancellationToken, stream) {
|
||||
const apmTrans = apm.startTransaction('execute-job-png', REPORTING_TRANSACTION_TYPE);
|
||||
const apmGetAssets = apmTrans?.startSpan('get-assets', 'setup');
|
||||
let apmGeneratePng: { end: () => void } | null | undefined;
|
||||
|
||||
const generatePngObservable = await generatePngObservableFactory(reporting);
|
||||
const jobLogger = parentLogger.clone([PNG_JOB_TYPE, 'execute', jobId]);
|
||||
const process$: Rx.Observable<TaskRunResult> = Rx.of(1).pipe(
|
||||
mergeMap(() => decryptJobHeaders(encryptionKey, job.headers, jobLogger)),
|
||||
map((decryptedHeaders) => omitBlockedHeaders(decryptedHeaders)),
|
||||
map((filteredHeaders) => getConditionalHeaders(config, filteredHeaders)),
|
||||
mergeMap((conditionalHeaders) => {
|
||||
const urls = getFullUrls(config, job);
|
||||
const hashUrl = urls[0];
|
||||
if (apmGetAssets) apmGetAssets.end();
|
||||
const [url] = getFullUrls(config, job);
|
||||
|
||||
apmGetAssets?.end();
|
||||
apmGeneratePng = apmTrans?.startSpan('generate-png-pipeline', 'execute');
|
||||
return generatePngObservable(
|
||||
jobLogger,
|
||||
hashUrl,
|
||||
job.browserTimezone,
|
||||
|
||||
return generatePngObservable(reporting, jobLogger, {
|
||||
conditionalHeaders,
|
||||
job.layout
|
||||
);
|
||||
urls: [url],
|
||||
browserTimezone: job.browserTimezone,
|
||||
layout: job.layout,
|
||||
});
|
||||
}),
|
||||
tap(({ buffer }) => stream.write(buffer)),
|
||||
map(({ warnings }) => ({
|
||||
content_type: 'image/png',
|
||||
warnings,
|
||||
})),
|
||||
catchError((err) => {
|
||||
jobLogger.error(err);
|
||||
return Rx.throwError(err);
|
||||
}),
|
||||
tap({ error: (error) => jobLogger.error(error) }),
|
||||
finalize(() => apmGeneratePng?.end())
|
||||
);
|
||||
|
||||
|
|
|
@ -16,11 +16,11 @@ import {
|
|||
createMockConfigSchema,
|
||||
createMockReportingCore,
|
||||
} from '../../test_helpers';
|
||||
import { generatePngObservableFactory } from '../common';
|
||||
import { generatePngObservable } from '../common';
|
||||
import { runTaskFnFactory } from './execute_job';
|
||||
import { TaskPayloadPNGV2 } from './types';
|
||||
|
||||
jest.mock('../common/generate_png', () => ({ generatePngObservableFactory: jest.fn() }));
|
||||
jest.mock('../common/generate_png');
|
||||
|
||||
let content: string;
|
||||
let mockReporting: ReportingCore;
|
||||
|
@ -62,16 +62,13 @@ beforeEach(async () => {
|
|||
|
||||
mockReporting = await createMockReportingCore(mockReportingConfig);
|
||||
mockReporting.setConfig(createMockConfig(mockReportingConfig));
|
||||
|
||||
(generatePngObservableFactory as jest.Mock).mockReturnValue(jest.fn());
|
||||
});
|
||||
|
||||
afterEach(() => (generatePngObservableFactory as jest.Mock).mockReset());
|
||||
afterEach(() => (generatePngObservable as jest.Mock).mockReset());
|
||||
|
||||
test(`passes browserTimezone to generatePng`, async () => {
|
||||
const encryptedHeaders = await encryptHeaders({});
|
||||
const generatePngObservable = (await generatePngObservableFactory(mockReporting)) as jest.Mock;
|
||||
generatePngObservable.mockReturnValue(Rx.of({ buffer: Buffer.from('') }));
|
||||
(generatePngObservable as jest.Mock).mockReturnValue(Rx.of({ buffer: Buffer.from('') }));
|
||||
|
||||
const runTask = await runTaskFnFactory(mockReporting, getMockLogger());
|
||||
const browserTimezone = 'UTC';
|
||||
|
@ -87,49 +84,29 @@ test(`passes browserTimezone to generatePng`, async () => {
|
|||
stream
|
||||
);
|
||||
|
||||
expect(generatePngObservable.mock.calls).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
LevelLogger {
|
||||
"_logger": Object {
|
||||
"get": [MockFunction],
|
||||
},
|
||||
"_tags": Array [
|
||||
"PNGV2",
|
||||
"execute",
|
||||
"pngJobId",
|
||||
],
|
||||
"warning": [Function],
|
||||
},
|
||||
Array [
|
||||
"localhost:80undefined/app/reportingRedirect?forceNow=test",
|
||||
Object {
|
||||
"id": "test",
|
||||
"params": Object {},
|
||||
"version": "test",
|
||||
},
|
||||
expect(generatePngObservable).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
expect.objectContaining({
|
||||
urls: [
|
||||
[
|
||||
'localhost:80undefined/app/reportingRedirect?forceNow=test',
|
||||
{ id: 'test', params: {}, version: 'test' },
|
||||
],
|
||||
"UTC",
|
||||
Object {
|
||||
"conditions": Object {
|
||||
"basePath": undefined,
|
||||
"hostname": "localhost",
|
||||
"port": 80,
|
||||
"protocol": undefined,
|
||||
},
|
||||
"headers": Object {},
|
||||
},
|
||||
undefined,
|
||||
],
|
||||
]
|
||||
`);
|
||||
browserTimezone: 'UTC',
|
||||
conditionalHeaders: expect.objectContaining({
|
||||
conditions: expect.any(Object),
|
||||
headers: {},
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test(`returns content_type of application/png`, async () => {
|
||||
const runTask = await runTaskFnFactory(mockReporting, getMockLogger());
|
||||
const encryptedHeaders = await encryptHeaders({});
|
||||
|
||||
const generatePngObservable = await generatePngObservableFactory(mockReporting);
|
||||
(generatePngObservable as jest.Mock).mockReturnValue(Rx.of({ buffer: Buffer.from('foo') }));
|
||||
|
||||
const { content_type: contentType } = await runTask(
|
||||
|
@ -146,7 +123,6 @@ test(`returns content_type of application/png`, async () => {
|
|||
|
||||
test(`returns content of generatePng getBuffer base64 encoded`, async () => {
|
||||
const testContent = 'raw string from get_screenhots';
|
||||
const generatePngObservable = await generatePngObservableFactory(mockReporting);
|
||||
(generatePngObservable as jest.Mock).mockReturnValue(Rx.of({ buffer: Buffer.from(testContent) }));
|
||||
|
||||
const runTask = await runTaskFnFactory(mockReporting, getMockLogger());
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import apm from 'elastic-apm-node';
|
||||
import * as Rx from 'rxjs';
|
||||
import { catchError, finalize, map, mergeMap, takeUntil, tap } from 'rxjs/operators';
|
||||
import { finalize, map, mergeMap, takeUntil, tap } from 'rxjs/operators';
|
||||
import { PNG_JOB_TYPE_V2, REPORTING_TRANSACTION_TYPE } from '../../../common/constants';
|
||||
import { TaskRunResult } from '../../lib/tasks';
|
||||
import { RunTaskFn, RunTaskFnFactory } from '../../types';
|
||||
|
@ -15,7 +15,7 @@ import {
|
|||
decryptJobHeaders,
|
||||
getConditionalHeaders,
|
||||
omitBlockedHeaders,
|
||||
generatePngObservableFactory,
|
||||
generatePngObservable,
|
||||
} from '../common';
|
||||
import { getFullRedirectAppUrl } from '../common/v2/get_full_redirect_app_url';
|
||||
import { TaskPayloadPNGV2 } from './types';
|
||||
|
@ -25,12 +25,11 @@ export const runTaskFnFactory: RunTaskFnFactory<RunTaskFn<TaskPayloadPNGV2>> =
|
|||
const config = reporting.getConfig();
|
||||
const encryptionKey = config.get('encryptionKey');
|
||||
|
||||
return async function runTask(jobId, job, cancellationToken, stream) {
|
||||
return function runTask(jobId, job, cancellationToken, stream) {
|
||||
const apmTrans = apm.startTransaction('execute-job-png-v2', REPORTING_TRANSACTION_TYPE);
|
||||
const apmGetAssets = apmTrans?.startSpan('get-assets', 'setup');
|
||||
let apmGeneratePng: { end: () => void } | null | undefined;
|
||||
|
||||
const generatePngObservable = await generatePngObservableFactory(reporting);
|
||||
const jobLogger = parentLogger.clone([PNG_JOB_TYPE_V2, 'execute', jobId]);
|
||||
const process$: Rx.Observable<TaskRunResult> = Rx.of(1).pipe(
|
||||
mergeMap(() => decryptJobHeaders(encryptionKey, job.headers, jobLogger)),
|
||||
|
@ -41,25 +40,21 @@ export const runTaskFnFactory: RunTaskFnFactory<RunTaskFn<TaskPayloadPNGV2>> =
|
|||
const [locatorParams] = job.locatorParams;
|
||||
|
||||
apmGetAssets?.end();
|
||||
|
||||
apmGeneratePng = apmTrans?.startSpan('generate-png-pipeline', 'execute');
|
||||
return generatePngObservable(
|
||||
jobLogger,
|
||||
[url, locatorParams],
|
||||
job.browserTimezone,
|
||||
|
||||
return generatePngObservable(reporting, jobLogger, {
|
||||
conditionalHeaders,
|
||||
job.layout
|
||||
);
|
||||
browserTimezone: job.browserTimezone,
|
||||
layout: job.layout,
|
||||
urls: [[url, locatorParams]],
|
||||
});
|
||||
}),
|
||||
tap(({ buffer }) => stream.write(buffer)),
|
||||
map(({ warnings }) => ({
|
||||
content_type: 'image/png',
|
||||
warnings,
|
||||
})),
|
||||
catchError((err) => {
|
||||
jobLogger.error(err);
|
||||
return Rx.throwError(err);
|
||||
}),
|
||||
tap({ error: (error) => jobLogger.error(error) }),
|
||||
finalize(() => apmGeneratePng?.end())
|
||||
);
|
||||
|
||||
|
|
|
@ -5,18 +5,18 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
jest.mock('../lib/generate_pdf', () => ({ generatePdfObservableFactory: jest.fn() }));
|
||||
|
||||
import * as Rx from 'rxjs';
|
||||
import { Writable } from 'stream';
|
||||
import { ReportingCore } from '../../../';
|
||||
import { CancellationToken } from '../../../../common';
|
||||
import { cryptoFactory, LevelLogger } from '../../../lib';
|
||||
import { createMockConfigSchema, createMockReportingCore } from '../../../test_helpers';
|
||||
import { generatePdfObservableFactory } from '../lib/generate_pdf';
|
||||
import { generatePdfObservable } from '../lib/generate_pdf';
|
||||
import { TaskPayloadPDF } from '../types';
|
||||
import { runTaskFnFactory } from './';
|
||||
|
||||
jest.mock('../lib/generate_pdf');
|
||||
|
||||
let content: string;
|
||||
let mockReporting: ReportingCore;
|
||||
let stream: jest.Mocked<Writable>;
|
||||
|
@ -56,16 +56,13 @@ beforeEach(async () => {
|
|||
};
|
||||
const mockSchema = createMockConfigSchema(reportingConfig);
|
||||
mockReporting = await createMockReportingCore(mockSchema);
|
||||
|
||||
(generatePdfObservableFactory as jest.Mock).mockReturnValue(jest.fn());
|
||||
});
|
||||
|
||||
afterEach(() => (generatePdfObservableFactory as jest.Mock).mockReset());
|
||||
afterEach(() => (generatePdfObservable as jest.Mock).mockReset());
|
||||
|
||||
test(`passes browserTimezone to generatePdf`, async () => {
|
||||
const encryptedHeaders = await encryptHeaders({});
|
||||
const generatePdfObservable = (await generatePdfObservableFactory(mockReporting)) as jest.Mock;
|
||||
generatePdfObservable.mockReturnValue(Rx.of({ buffer: Buffer.from('') }));
|
||||
(generatePdfObservable as jest.Mock).mockReturnValue(Rx.of({ buffer: Buffer.from('') }));
|
||||
|
||||
const runTask = runTaskFnFactory(mockReporting, getMockLogger());
|
||||
const browserTimezone = 'UTC';
|
||||
|
@ -81,8 +78,13 @@ test(`passes browserTimezone to generatePdf`, async () => {
|
|||
stream
|
||||
);
|
||||
|
||||
const tzParam = generatePdfObservable.mock.calls[0][3];
|
||||
expect(tzParam).toBe('UTC');
|
||||
expect(generatePdfObservable).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
expect.objectContaining({ browserTimezone: 'UTC' }),
|
||||
undefined
|
||||
);
|
||||
});
|
||||
|
||||
test(`returns content_type of application/pdf`, async () => {
|
||||
|
@ -90,7 +92,6 @@ test(`returns content_type of application/pdf`, async () => {
|
|||
const runTask = runTaskFnFactory(mockReporting, logger);
|
||||
const encryptedHeaders = await encryptHeaders({});
|
||||
|
||||
const generatePdfObservable = await generatePdfObservableFactory(mockReporting);
|
||||
(generatePdfObservable as jest.Mock).mockReturnValue(Rx.of({ buffer: Buffer.from('') }));
|
||||
|
||||
const { content_type: contentType } = await runTask(
|
||||
|
@ -104,7 +105,6 @@ test(`returns content_type of application/pdf`, async () => {
|
|||
|
||||
test(`returns content of generatePdf getBuffer base64 encoded`, async () => {
|
||||
const testContent = 'test content';
|
||||
const generatePdfObservable = await generatePdfObservableFactory(mockReporting);
|
||||
(generatePdfObservable as jest.Mock).mockReturnValue(Rx.of({ buffer: Buffer.from(testContent) }));
|
||||
|
||||
const runTask = runTaskFnFactory(mockReporting, getMockLogger());
|
||||
|
|
|
@ -18,7 +18,7 @@ import {
|
|||
omitBlockedHeaders,
|
||||
getCustomLogo,
|
||||
} from '../../common';
|
||||
import { generatePdfObservableFactory } from '../lib/generate_pdf';
|
||||
import { generatePdfObservable } from '../lib/generate_pdf';
|
||||
import { TaskPayloadPDF } from '../types';
|
||||
|
||||
export const runTaskFnFactory: RunTaskFnFactory<RunTaskFn<TaskPayloadPDF>> =
|
||||
|
@ -32,8 +32,6 @@ export const runTaskFnFactory: RunTaskFnFactory<RunTaskFn<TaskPayloadPDF>> =
|
|||
const apmGetAssets = apmTrans?.startSpan('get-assets', 'setup');
|
||||
let apmGeneratePdf: { end: () => void } | null | undefined;
|
||||
|
||||
const generatePdfObservable = await generatePdfObservableFactory(reporting);
|
||||
|
||||
const process$: Rx.Observable<TaskRunResult> = Rx.of(1).pipe(
|
||||
mergeMap(() => decryptJobHeaders(encryptionKey, job.headers, jobLogger)),
|
||||
map((decryptedHeaders) => omitBlockedHeaders(decryptedHeaders)),
|
||||
|
@ -49,12 +47,15 @@ export const runTaskFnFactory: RunTaskFnFactory<RunTaskFn<TaskPayloadPDF>> =
|
|||
|
||||
apmGeneratePdf = apmTrans?.startSpan('generate-pdf-pipeline', 'execute');
|
||||
return generatePdfObservable(
|
||||
reporting,
|
||||
jobLogger,
|
||||
title,
|
||||
urls,
|
||||
browserTimezone,
|
||||
conditionalHeaders,
|
||||
layout,
|
||||
{
|
||||
urls,
|
||||
browserTimezone,
|
||||
conditionalHeaders,
|
||||
layout,
|
||||
},
|
||||
logo
|
||||
);
|
||||
}),
|
||||
|
|
|
@ -8,16 +8,15 @@
|
|||
import { groupBy } from 'lodash';
|
||||
import * as Rx from 'rxjs';
|
||||
import { mergeMap } from 'rxjs/operators';
|
||||
import { ScreenshotResult } from '../../../../../screenshotting/server';
|
||||
import { ReportingCore } from '../../../';
|
||||
import { LevelLogger } from '../../../lib';
|
||||
import { createLayout, LayoutParams } from '../../../lib/layouts';
|
||||
import { getScreenshots$, ScreenshotResults } from '../../../lib/screenshots';
|
||||
import { ConditionalHeaders } from '../../common';
|
||||
import { ScreenshotOptions } from '../../../types';
|
||||
import { PdfMaker } from '../../common/pdf';
|
||||
import { getTracker } from './tracker';
|
||||
|
||||
const getTimeRange = (urlScreenshots: ScreenshotResults[]) => {
|
||||
const grouped = groupBy(urlScreenshots.map((u) => u.timeRange));
|
||||
const getTimeRange = (urlScreenshots: ScreenshotResult['results']) => {
|
||||
const grouped = groupBy(urlScreenshots.map(({ timeRange }) => timeRange));
|
||||
const values = Object.values(grouped);
|
||||
if (values.length === 1) {
|
||||
return values[0][0];
|
||||
|
@ -26,97 +25,80 @@ const getTimeRange = (urlScreenshots: ScreenshotResults[]) => {
|
|||
return null;
|
||||
};
|
||||
|
||||
export async function generatePdfObservableFactory(reporting: ReportingCore) {
|
||||
const config = reporting.getConfig();
|
||||
const captureConfig = config.get('capture');
|
||||
const { browserDriverFactory } = await reporting.getPluginStartDeps();
|
||||
export function generatePdfObservable(
|
||||
reporting: ReportingCore,
|
||||
logger: LevelLogger,
|
||||
title: string,
|
||||
options: ScreenshotOptions,
|
||||
logo?: string
|
||||
): Rx.Observable<{ buffer: Buffer | null; warnings: string[] }> {
|
||||
const tracker = getTracker();
|
||||
tracker.startScreenshots();
|
||||
|
||||
return function generatePdfObservable(
|
||||
logger: LevelLogger,
|
||||
title: string,
|
||||
urls: string[],
|
||||
browserTimezone: string | undefined,
|
||||
conditionalHeaders: ConditionalHeaders,
|
||||
layoutParams: LayoutParams,
|
||||
logo?: string
|
||||
): Rx.Observable<{ buffer: Buffer | null; warnings: string[] }> {
|
||||
const tracker = getTracker();
|
||||
tracker.startLayout();
|
||||
return reporting.getScreenshots(options).pipe(
|
||||
mergeMap(async ({ layout, metrics$, results }) => {
|
||||
metrics$.subscribe(({ cpu, memory }) => {
|
||||
tracker.setCpuUsage(cpu);
|
||||
tracker.setMemoryUsage(memory);
|
||||
});
|
||||
tracker.endScreenshots();
|
||||
tracker.startSetup();
|
||||
|
||||
const layout = createLayout(captureConfig, layoutParams);
|
||||
logger.debug(`Layout: width=${layout.width} height=${layout.height}`);
|
||||
tracker.endLayout();
|
||||
const pdfOutput = new PdfMaker(layout, logo);
|
||||
if (title) {
|
||||
const timeRange = getTimeRange(results);
|
||||
title += timeRange ? ` - ${timeRange}` : '';
|
||||
pdfOutput.setTitle(title);
|
||||
}
|
||||
tracker.endSetup();
|
||||
|
||||
tracker.startScreenshots();
|
||||
const screenshots$ = getScreenshots$(captureConfig, browserDriverFactory, {
|
||||
logger,
|
||||
urlsOrUrlLocatorTuples: urls,
|
||||
conditionalHeaders,
|
||||
layout,
|
||||
browserTimezone,
|
||||
}).pipe(
|
||||
mergeMap(async (results: ScreenshotResults[]) => {
|
||||
tracker.endScreenshots();
|
||||
|
||||
tracker.startSetup();
|
||||
const pdfOutput = new PdfMaker(layout, logo);
|
||||
if (title) {
|
||||
const timeRange = getTimeRange(results);
|
||||
title += timeRange ? ` - ${timeRange}` : '';
|
||||
pdfOutput.setTitle(title);
|
||||
}
|
||||
tracker.endSetup();
|
||||
|
||||
results.forEach((r) => {
|
||||
r.screenshots.forEach((screenshot) => {
|
||||
logger.debug(`Adding image to PDF. Image size: ${screenshot.data.byteLength}`); // prettier-ignore
|
||||
tracker.startAddImage();
|
||||
tracker.endAddImage();
|
||||
pdfOutput.addImage(screenshot.data, {
|
||||
title: screenshot.title ?? undefined,
|
||||
description: screenshot.description ?? undefined,
|
||||
});
|
||||
results.forEach((r) => {
|
||||
r.screenshots.forEach((screenshot) => {
|
||||
logger.debug(`Adding image to PDF. Image size: ${screenshot.data.byteLength}`); // prettier-ignore
|
||||
tracker.startAddImage();
|
||||
tracker.endAddImage();
|
||||
pdfOutput.addImage(screenshot.data, {
|
||||
title: screenshot.title ?? undefined,
|
||||
description: screenshot.description ?? undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
let buffer: Buffer | null = null;
|
||||
try {
|
||||
tracker.startCompile();
|
||||
logger.debug(`Compiling PDF using "${layout.id}" layout...`);
|
||||
pdfOutput.generate();
|
||||
tracker.endCompile();
|
||||
let buffer: Buffer | null = null;
|
||||
try {
|
||||
tracker.startCompile();
|
||||
logger.debug(`Compiling PDF using "${layout.id}" layout...`);
|
||||
pdfOutput.generate();
|
||||
tracker.endCompile();
|
||||
|
||||
tracker.startGetBuffer();
|
||||
logger.debug(`Generating PDF Buffer...`);
|
||||
buffer = await pdfOutput.getBuffer();
|
||||
tracker.startGetBuffer();
|
||||
logger.debug(`Generating PDF Buffer...`);
|
||||
buffer = await pdfOutput.getBuffer();
|
||||
|
||||
const byteLength = buffer?.byteLength ?? 0;
|
||||
logger.debug(`PDF buffer byte length: ${byteLength}`);
|
||||
tracker.setByteLength(byteLength);
|
||||
const byteLength = buffer?.byteLength ?? 0;
|
||||
logger.debug(`PDF buffer byte length: ${byteLength}`);
|
||||
tracker.setByteLength(byteLength);
|
||||
|
||||
tracker.endGetBuffer();
|
||||
} catch (err) {
|
||||
logger.error(`Could not generate the PDF buffer!`);
|
||||
logger.error(err);
|
||||
}
|
||||
tracker.endGetBuffer();
|
||||
} catch (err) {
|
||||
logger.error(`Could not generate the PDF buffer!`);
|
||||
logger.error(err);
|
||||
}
|
||||
|
||||
tracker.end();
|
||||
tracker.end();
|
||||
|
||||
return {
|
||||
buffer,
|
||||
warnings: results.reduce((found, current) => {
|
||||
if (current.error) {
|
||||
found.push(current.error.message);
|
||||
}
|
||||
if (current.renderErrors) {
|
||||
found.push(...current.renderErrors);
|
||||
}
|
||||
return found;
|
||||
}, [] as string[]),
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return screenshots$;
|
||||
};
|
||||
return {
|
||||
buffer,
|
||||
warnings: results.reduce((found, current) => {
|
||||
if (current.error) {
|
||||
found.push(current.error.message);
|
||||
}
|
||||
if (current.renderErrors) {
|
||||
found.push(...current.renderErrors);
|
||||
}
|
||||
return found;
|
||||
}, [] as string[]),
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
|
@ -10,8 +10,8 @@ import { REPORTING_TRANSACTION_TYPE } from '../../../../common/constants';
|
|||
|
||||
interface PdfTracker {
|
||||
setByteLength: (byteLength: number) => void;
|
||||
startLayout: () => void;
|
||||
endLayout: () => void;
|
||||
setCpuUsage: (cpu: number) => void;
|
||||
setMemoryUsage: (memory: number) => void;
|
||||
startScreenshots: () => void;
|
||||
endScreenshots: () => void;
|
||||
startSetup: () => void;
|
||||
|
@ -35,7 +35,6 @@ interface ApmSpan {
|
|||
export function getTracker(): PdfTracker {
|
||||
const apmTrans = apm.startTransaction('generate-pdf', REPORTING_TRANSACTION_TYPE);
|
||||
|
||||
let apmLayout: ApmSpan | null = null;
|
||||
let apmScreenshots: ApmSpan | null = null;
|
||||
let apmSetup: ApmSpan | null = null;
|
||||
let apmAddImage: ApmSpan | null = null;
|
||||
|
@ -43,12 +42,6 @@ export function getTracker(): PdfTracker {
|
|||
let apmGetBuffer: ApmSpan | null = null;
|
||||
|
||||
return {
|
||||
startLayout() {
|
||||
apmLayout = apmTrans?.startSpan('create-layout', SPANTYPE_SETUP) || null;
|
||||
},
|
||||
endLayout() {
|
||||
if (apmLayout) apmLayout.end();
|
||||
},
|
||||
startScreenshots() {
|
||||
apmScreenshots = apmTrans?.startSpan('screenshots-pipeline', SPANTYPE_SETUP) || null;
|
||||
},
|
||||
|
@ -82,6 +75,12 @@ export function getTracker(): PdfTracker {
|
|||
setByteLength(byteLength: number) {
|
||||
apmTrans?.setLabel('byte-length', byteLength, false);
|
||||
},
|
||||
setCpuUsage(cpu: number) {
|
||||
apmTrans?.setLabel('cpu', cpu, false);
|
||||
},
|
||||
setMemoryUsage(memory: number) {
|
||||
apmTrans?.setLabel('memory', memory, false);
|
||||
},
|
||||
end() {
|
||||
if (apmTrans) apmTrans.end();
|
||||
},
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
jest.mock('./lib/generate_pdf', () => ({ generatePdfObservableFactory: jest.fn() }));
|
||||
jest.mock('./lib/generate_pdf');
|
||||
|
||||
import * as Rx from 'rxjs';
|
||||
import { Writable } from 'stream';
|
||||
|
@ -15,7 +15,7 @@ import { LocatorParams } from '../../../common/types';
|
|||
import { cryptoFactory, LevelLogger } from '../../lib';
|
||||
import { createMockConfigSchema, createMockReportingCore } from '../../test_helpers';
|
||||
import { runTaskFnFactory } from './execute_job';
|
||||
import { generatePdfObservableFactory } from './lib/generate_pdf';
|
||||
import { generatePdfObservable } from './lib/generate_pdf';
|
||||
import { TaskPayloadPDFV2 } from './types';
|
||||
|
||||
let content: string;
|
||||
|
@ -61,16 +61,13 @@ beforeEach(async () => {
|
|||
};
|
||||
const mockSchema = createMockConfigSchema(reportingConfig);
|
||||
mockReporting = await createMockReportingCore(mockSchema);
|
||||
|
||||
(generatePdfObservableFactory as jest.Mock).mockReturnValue(jest.fn());
|
||||
});
|
||||
|
||||
afterEach(() => (generatePdfObservableFactory as jest.Mock).mockReset());
|
||||
afterEach(() => (generatePdfObservable as jest.Mock).mockReset());
|
||||
|
||||
test(`passes browserTimezone to generatePdf`, async () => {
|
||||
const encryptedHeaders = await encryptHeaders({});
|
||||
const generatePdfObservable = (await generatePdfObservableFactory(mockReporting)) as jest.Mock;
|
||||
generatePdfObservable.mockReturnValue(Rx.of(Buffer.from('')));
|
||||
(generatePdfObservable as jest.Mock).mockReturnValue(Rx.of(Buffer.from('')));
|
||||
|
||||
const runTask = runTaskFnFactory(mockReporting, getMockLogger());
|
||||
const browserTimezone = 'UTC';
|
||||
|
@ -87,8 +84,15 @@ test(`passes browserTimezone to generatePdf`, async () => {
|
|||
stream
|
||||
);
|
||||
|
||||
const tzParam = generatePdfObservable.mock.calls[0][4];
|
||||
expect(tzParam).toBe('UTC');
|
||||
expect(generatePdfObservable).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
expect.objectContaining({ browserTimezone: 'UTC' }),
|
||||
undefined
|
||||
);
|
||||
});
|
||||
|
||||
test(`returns content_type of application/pdf`, async () => {
|
||||
|
@ -96,7 +100,6 @@ test(`returns content_type of application/pdf`, async () => {
|
|||
const runTask = runTaskFnFactory(mockReporting, logger);
|
||||
const encryptedHeaders = await encryptHeaders({});
|
||||
|
||||
const generatePdfObservable = await generatePdfObservableFactory(mockReporting);
|
||||
(generatePdfObservable as jest.Mock).mockReturnValue(Rx.of({ buffer: Buffer.from('') }));
|
||||
|
||||
const { content_type: contentType } = await runTask(
|
||||
|
@ -110,7 +113,6 @@ test(`returns content_type of application/pdf`, async () => {
|
|||
|
||||
test(`returns content of generatePdf getBuffer base64 encoded`, async () => {
|
||||
const testContent = 'test content';
|
||||
const generatePdfObservable = await generatePdfObservableFactory(mockReporting);
|
||||
(generatePdfObservable as jest.Mock).mockReturnValue(Rx.of({ buffer: Buffer.from(testContent) }));
|
||||
|
||||
const runTask = runTaskFnFactory(mockReporting, getMockLogger());
|
||||
|
|
|
@ -17,7 +17,7 @@ import {
|
|||
omitBlockedHeaders,
|
||||
getCustomLogo,
|
||||
} from '../common';
|
||||
import { generatePdfObservableFactory } from './lib/generate_pdf';
|
||||
import { generatePdfObservable } from './lib/generate_pdf';
|
||||
import { TaskPayloadPDFV2 } from './types';
|
||||
|
||||
export const runTaskFnFactory: RunTaskFnFactory<RunTaskFn<TaskPayloadPDFV2>> =
|
||||
|
@ -31,8 +31,6 @@ export const runTaskFnFactory: RunTaskFnFactory<RunTaskFn<TaskPayloadPDFV2>> =
|
|||
const apmGetAssets = apmTrans?.startSpan('get-assets', 'setup');
|
||||
let apmGeneratePdf: { end: () => void } | null | undefined;
|
||||
|
||||
const generatePdfObservable = await generatePdfObservableFactory(reporting);
|
||||
|
||||
const process$: Rx.Observable<TaskRunResult> = Rx.of(1).pipe(
|
||||
mergeMap(() => decryptJobHeaders(encryptionKey, job.headers, jobLogger)),
|
||||
map((decryptedHeaders) => omitBlockedHeaders(decryptedHeaders)),
|
||||
|
@ -46,13 +44,16 @@ export const runTaskFnFactory: RunTaskFnFactory<RunTaskFn<TaskPayloadPDFV2>> =
|
|||
|
||||
apmGeneratePdf = apmTrans?.startSpan('generate-pdf-pipeline', 'execute');
|
||||
return generatePdfObservable(
|
||||
reporting,
|
||||
jobLogger,
|
||||
job,
|
||||
title,
|
||||
locatorParams,
|
||||
browserTimezone,
|
||||
conditionalHeaders,
|
||||
layout,
|
||||
{
|
||||
browserTimezone,
|
||||
conditionalHeaders,
|
||||
layout,
|
||||
},
|
||||
logo
|
||||
);
|
||||
}),
|
||||
|
|
|
@ -5,21 +5,20 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { groupBy, zip } from 'lodash';
|
||||
import { groupBy } from 'lodash';
|
||||
import * as Rx from 'rxjs';
|
||||
import { mergeMap } from 'rxjs/operators';
|
||||
import { ReportingCore } from '../../../';
|
||||
import { LocatorParams, UrlOrUrlLocatorTuple } from '../../../../common/types';
|
||||
import { LevelLogger } from '../../../lib';
|
||||
import { createLayout, LayoutParams } from '../../../lib/layouts';
|
||||
import { getScreenshots$, ScreenshotResults } from '../../../lib/screenshots';
|
||||
import { ConditionalHeaders } from '../../common';
|
||||
import { ScreenshotResult } from '../../../../../screenshotting/server';
|
||||
import { ScreenshotOptions } from '../../../types';
|
||||
import { PdfMaker } from '../../common/pdf';
|
||||
import { getFullRedirectAppUrl } from '../../common/v2/get_full_redirect_app_url';
|
||||
import type { TaskPayloadPDFV2 } from '../types';
|
||||
import { getTracker } from './tracker';
|
||||
|
||||
const getTimeRange = (urlScreenshots: ScreenshotResults[]) => {
|
||||
const getTimeRange = (urlScreenshots: ScreenshotResult['results']) => {
|
||||
const grouped = groupBy(urlScreenshots.map((u) => u.timeRange));
|
||||
const values = Object.values(grouped);
|
||||
if (values.length === 1) {
|
||||
|
@ -29,106 +28,92 @@ const getTimeRange = (urlScreenshots: ScreenshotResults[]) => {
|
|||
return null;
|
||||
};
|
||||
|
||||
export async function generatePdfObservableFactory(reporting: ReportingCore) {
|
||||
const config = reporting.getConfig();
|
||||
const captureConfig = config.get('capture');
|
||||
const { browserDriverFactory } = await reporting.getPluginStartDeps();
|
||||
export function generatePdfObservable(
|
||||
reporting: ReportingCore,
|
||||
logger: LevelLogger,
|
||||
job: TaskPayloadPDFV2,
|
||||
title: string,
|
||||
locatorParams: LocatorParams[],
|
||||
options: Omit<ScreenshotOptions, 'urls'>,
|
||||
logo?: string
|
||||
): Rx.Observable<{ buffer: Buffer | null; warnings: string[] }> {
|
||||
const tracker = getTracker();
|
||||
tracker.startScreenshots();
|
||||
|
||||
return function generatePdfObservable(
|
||||
logger: LevelLogger,
|
||||
job: TaskPayloadPDFV2,
|
||||
title: string,
|
||||
locatorParams: LocatorParams[],
|
||||
browserTimezone: string | undefined,
|
||||
conditionalHeaders: ConditionalHeaders,
|
||||
layoutParams: LayoutParams,
|
||||
logo?: string
|
||||
): Rx.Observable<{ buffer: Buffer | null; warnings: string[] }> {
|
||||
const tracker = getTracker();
|
||||
tracker.startLayout();
|
||||
/**
|
||||
* For each locator we get the relative URL to the redirect app
|
||||
*/
|
||||
const urls = locatorParams.map((locator) => [
|
||||
getFullRedirectAppUrl(reporting.getConfig(), job.spaceId, job.forceNow),
|
||||
locator,
|
||||
]) as UrlOrUrlLocatorTuple[];
|
||||
|
||||
const layout = createLayout(captureConfig, layoutParams);
|
||||
logger.debug(`Layout: width=${layout.width} height=${layout.height}`);
|
||||
tracker.endLayout();
|
||||
const screenshots$ = reporting.getScreenshots({ ...options, urls }).pipe(
|
||||
mergeMap(async ({ layout, metrics$, results }) => {
|
||||
metrics$.subscribe(({ cpu, memory }) => {
|
||||
tracker.setCpuUsage(cpu);
|
||||
tracker.setMemoryUsage(memory);
|
||||
});
|
||||
tracker.endScreenshots();
|
||||
tracker.startSetup();
|
||||
|
||||
tracker.startScreenshots();
|
||||
const pdfOutput = new PdfMaker(layout, logo);
|
||||
if (title) {
|
||||
const timeRange = getTimeRange(results);
|
||||
title += timeRange ? ` - ${timeRange}` : '';
|
||||
pdfOutput.setTitle(title);
|
||||
}
|
||||
tracker.endSetup();
|
||||
|
||||
/**
|
||||
* For each locator we get the relative URL to the redirect app
|
||||
*/
|
||||
const urls = locatorParams.map(() =>
|
||||
getFullRedirectAppUrl(reporting.getConfig(), job.spaceId, job.forceNow)
|
||||
);
|
||||
|
||||
const screenshots$ = getScreenshots$(captureConfig, browserDriverFactory, {
|
||||
logger,
|
||||
urlsOrUrlLocatorTuples: zip(urls, locatorParams) as UrlOrUrlLocatorTuple[],
|
||||
conditionalHeaders,
|
||||
layout,
|
||||
browserTimezone,
|
||||
}).pipe(
|
||||
mergeMap(async (results: ScreenshotResults[]) => {
|
||||
tracker.endScreenshots();
|
||||
|
||||
tracker.startSetup();
|
||||
const pdfOutput = new PdfMaker(layout, logo);
|
||||
if (title) {
|
||||
const timeRange = getTimeRange(results);
|
||||
title += timeRange ? ` - ${timeRange}` : '';
|
||||
pdfOutput.setTitle(title);
|
||||
}
|
||||
tracker.endSetup();
|
||||
|
||||
results.forEach((r) => {
|
||||
r.screenshots.forEach((screenshot) => {
|
||||
logger.debug(`Adding image to PDF. Image base64 size: ${screenshot.data.byteLength}`); // prettier-ignore
|
||||
tracker.startAddImage();
|
||||
tracker.endAddImage();
|
||||
pdfOutput.addImage(screenshot.data, {
|
||||
title: screenshot.title ?? undefined,
|
||||
description: screenshot.description ?? undefined,
|
||||
});
|
||||
results.forEach((r) => {
|
||||
r.screenshots.forEach((screenshot) => {
|
||||
logger.debug(`Adding image to PDF. Image base64 size: ${screenshot.data.byteLength}`); // prettier-ignore
|
||||
tracker.startAddImage();
|
||||
tracker.endAddImage();
|
||||
pdfOutput.addImage(screenshot.data, {
|
||||
title: screenshot.title ?? undefined,
|
||||
description: screenshot.description ?? undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
let buffer: Buffer | null = null;
|
||||
try {
|
||||
tracker.startCompile();
|
||||
logger.debug(`Compiling PDF using "${layout.id}" layout...`);
|
||||
pdfOutput.generate();
|
||||
tracker.endCompile();
|
||||
let buffer: Buffer | null = null;
|
||||
try {
|
||||
tracker.startCompile();
|
||||
logger.debug(`Compiling PDF using "${layout.id}" layout...`);
|
||||
pdfOutput.generate();
|
||||
tracker.endCompile();
|
||||
|
||||
tracker.startGetBuffer();
|
||||
logger.debug(`Generating PDF Buffer...`);
|
||||
buffer = await pdfOutput.getBuffer();
|
||||
tracker.startGetBuffer();
|
||||
logger.debug(`Generating PDF Buffer...`);
|
||||
buffer = await pdfOutput.getBuffer();
|
||||
|
||||
const byteLength = buffer?.byteLength ?? 0;
|
||||
logger.debug(`PDF buffer byte length: ${byteLength}`);
|
||||
tracker.setByteLength(byteLength);
|
||||
const byteLength = buffer?.byteLength ?? 0;
|
||||
logger.debug(`PDF buffer byte length: ${byteLength}`);
|
||||
tracker.setByteLength(byteLength);
|
||||
|
||||
tracker.endGetBuffer();
|
||||
} catch (err) {
|
||||
logger.error(`Could not generate the PDF buffer!`);
|
||||
logger.error(err);
|
||||
}
|
||||
tracker.endGetBuffer();
|
||||
} catch (err) {
|
||||
logger.error(`Could not generate the PDF buffer!`);
|
||||
logger.error(err);
|
||||
}
|
||||
|
||||
tracker.end();
|
||||
tracker.end();
|
||||
|
||||
return {
|
||||
buffer,
|
||||
warnings: results.reduce((found, current) => {
|
||||
if (current.error) {
|
||||
found.push(current.error.message);
|
||||
}
|
||||
if (current.renderErrors) {
|
||||
found.push(...current.renderErrors);
|
||||
}
|
||||
return found;
|
||||
}, [] as string[]),
|
||||
};
|
||||
})
|
||||
);
|
||||
return {
|
||||
buffer,
|
||||
warnings: results.reduce((found, current) => {
|
||||
if (current.error) {
|
||||
found.push(current.error.message);
|
||||
}
|
||||
if (current.renderErrors) {
|
||||
found.push(...current.renderErrors);
|
||||
}
|
||||
return found;
|
||||
}, [] as string[]),
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return screenshots$;
|
||||
};
|
||||
return screenshots$;
|
||||
}
|
||||
|
|
|
@ -10,8 +10,8 @@ import { REPORTING_TRANSACTION_TYPE } from '../../../../common/constants';
|
|||
|
||||
interface PdfTracker {
|
||||
setByteLength: (byteLength: number) => void;
|
||||
startLayout: () => void;
|
||||
endLayout: () => void;
|
||||
setCpuUsage: (cpu: number) => void;
|
||||
setMemoryUsage: (memory: number) => void;
|
||||
startScreenshots: () => void;
|
||||
endScreenshots: () => void;
|
||||
startSetup: () => void;
|
||||
|
@ -35,7 +35,6 @@ interface ApmSpan {
|
|||
export function getTracker(): PdfTracker {
|
||||
const apmTrans = apm.startTransaction('generate-pdf', REPORTING_TRANSACTION_TYPE);
|
||||
|
||||
let apmLayout: ApmSpan | null = null;
|
||||
let apmScreenshots: ApmSpan | null = null;
|
||||
let apmSetup: ApmSpan | null = null;
|
||||
let apmAddImage: ApmSpan | null = null;
|
||||
|
@ -43,12 +42,6 @@ export function getTracker(): PdfTracker {
|
|||
let apmGetBuffer: ApmSpan | null = null;
|
||||
|
||||
return {
|
||||
startLayout() {
|
||||
apmLayout = apmTrans?.startSpan('create-layout', SPANTYPE_SETUP) || null;
|
||||
},
|
||||
endLayout() {
|
||||
if (apmLayout) apmLayout.end();
|
||||
},
|
||||
startScreenshots() {
|
||||
apmScreenshots = apmTrans?.startSpan('screenshots-pipeline', SPANTYPE_SETUP) || null;
|
||||
},
|
||||
|
@ -82,6 +75,12 @@ export function getTracker(): PdfTracker {
|
|||
setByteLength(byteLength: number) {
|
||||
apmTrans?.setLabel('byte-length', byteLength, false);
|
||||
},
|
||||
setCpuUsage(cpu: number) {
|
||||
apmTrans?.setLabel('cpu', cpu, false);
|
||||
},
|
||||
setMemoryUsage(memory: number) {
|
||||
apmTrans?.setLabel('memory', memory, false);
|
||||
},
|
||||
end() {
|
||||
if (apmTrans) apmTrans.end();
|
||||
},
|
||||
|
|
|
@ -1,29 +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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { LAYOUT_TYPES } from '../../../common/constants';
|
||||
import { CaptureConfig } from '../../types';
|
||||
import { LayoutInstance, LayoutParams, LayoutTypes } from './';
|
||||
import { CanvasLayout } from './canvas_layout';
|
||||
import { PreserveLayout } from './preserve_layout';
|
||||
import { PrintLayout } from './print_layout';
|
||||
|
||||
export function createLayout(
|
||||
captureConfig: CaptureConfig,
|
||||
layoutParams?: LayoutParams
|
||||
): LayoutInstance {
|
||||
if (layoutParams && layoutParams.dimensions && layoutParams.id === LAYOUT_TYPES.PRESERVE_LAYOUT) {
|
||||
return new PreserveLayout(layoutParams.dimensions);
|
||||
}
|
||||
|
||||
if (layoutParams && layoutParams.dimensions && layoutParams.id === LayoutTypes.CANVAS) {
|
||||
return new CanvasLayout(layoutParams.dimensions);
|
||||
}
|
||||
|
||||
// layoutParams is optional as PrintLayout doesn't use it
|
||||
return new PrintLayout(captureConfig);
|
||||
}
|
|
@ -1,51 +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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { LevelLogger } from '../';
|
||||
import { Size } from '../../../common/types';
|
||||
import { HeadlessChromiumDriver } from '../../browsers';
|
||||
import type { Layout } from './layout';
|
||||
|
||||
export interface LayoutSelectorDictionary {
|
||||
screenshot: string;
|
||||
renderComplete: string;
|
||||
renderError: string;
|
||||
renderErrorAttribute: string;
|
||||
itemsCountAttribute: string;
|
||||
timefilterDurationAttribute: string;
|
||||
}
|
||||
|
||||
export type { LayoutParams, PageSizeParams, PdfImageSize, Size } from '../../../common/types';
|
||||
export { CanvasLayout } from './canvas_layout';
|
||||
export { createLayout } from './create_layout';
|
||||
export type { Layout } from './layout';
|
||||
export { PreserveLayout } from './preserve_layout';
|
||||
export { PrintLayout } from './print_layout';
|
||||
|
||||
export const LayoutTypes = {
|
||||
PRESERVE_LAYOUT: 'preserve_layout',
|
||||
PRINT: 'print',
|
||||
CANVAS: 'canvas', // no margins or branding in the layout
|
||||
};
|
||||
|
||||
export const getDefaultLayoutSelectors = (): LayoutSelectorDictionary => ({
|
||||
screenshot: '[data-shared-items-container]',
|
||||
renderComplete: '[data-shared-item]',
|
||||
renderError: '[data-render-error]',
|
||||
renderErrorAttribute: 'data-render-error',
|
||||
itemsCountAttribute: 'data-shared-items-count',
|
||||
timefilterDurationAttribute: 'data-shared-timefilter-duration',
|
||||
});
|
||||
|
||||
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<void>;
|
||||
}
|
||||
|
||||
export type LayoutInstance = Layout & LayoutSelectors & Partial<Size>;
|
|
@ -1,90 +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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { set } from 'lodash';
|
||||
import { durationToNumber } from '../../../common/schema_utils';
|
||||
import { HeadlessChromiumDriver } from '../../browsers';
|
||||
import {
|
||||
createMockBrowserDriverFactory,
|
||||
createMockConfig,
|
||||
createMockConfigSchema,
|
||||
createMockLayoutInstance,
|
||||
createMockLevelLogger,
|
||||
createMockReportingCore,
|
||||
} from '../../test_helpers';
|
||||
import { CaptureConfig } from '../../types';
|
||||
import { LayoutInstance } from '../layouts';
|
||||
import { LevelLogger } from '../level_logger';
|
||||
import { getNumberOfItems } from './get_number_of_items';
|
||||
|
||||
describe('getNumberOfItems', () => {
|
||||
let captureConfig: CaptureConfig;
|
||||
let layout: LayoutInstance;
|
||||
let logger: jest.Mocked<LevelLogger>;
|
||||
let browser: HeadlessChromiumDriver;
|
||||
let timeout: number;
|
||||
|
||||
beforeEach(async () => {
|
||||
const schema = createMockConfigSchema(set({}, 'capture.timeouts.waitForElements', 0));
|
||||
const config = createMockConfig(schema);
|
||||
const core = await createMockReportingCore(schema);
|
||||
|
||||
captureConfig = config.get('capture');
|
||||
layout = createMockLayoutInstance(captureConfig);
|
||||
logger = createMockLevelLogger();
|
||||
timeout = durationToNumber(captureConfig.timeouts.waitForElements);
|
||||
|
||||
await createMockBrowserDriverFactory(core, logger, {
|
||||
evaluate: jest.fn(
|
||||
async <T extends (...args: unknown[]) => unknown>({
|
||||
fn,
|
||||
args,
|
||||
}: {
|
||||
fn: T;
|
||||
args: Parameters<T>;
|
||||
}) => fn(...args)
|
||||
),
|
||||
getCreatePage: (driver) => {
|
||||
browser = driver;
|
||||
|
||||
return jest.fn();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = '';
|
||||
});
|
||||
|
||||
it('should determine the number of items by attribute', async () => {
|
||||
document.body.innerHTML = `
|
||||
<div itemsSelector="10" />
|
||||
`;
|
||||
|
||||
await expect(getNumberOfItems(timeout, browser, layout, logger)).resolves.toBe(10);
|
||||
});
|
||||
|
||||
it('should determine the number of items by selector ', async () => {
|
||||
document.body.innerHTML = `
|
||||
<renderedSelector />
|
||||
<renderedSelector />
|
||||
<renderedSelector />
|
||||
`;
|
||||
|
||||
await expect(getNumberOfItems(timeout, browser, layout, logger)).resolves.toBe(3);
|
||||
});
|
||||
|
||||
it('should fall back to the selector when the attribute is empty', async () => {
|
||||
document.body.innerHTML = `
|
||||
<div itemsSelector />
|
||||
<renderedSelector />
|
||||
<renderedSelector />
|
||||
`;
|
||||
|
||||
await expect(getNumberOfItems(timeout, browser, layout, logger)).resolves.toBe(2);
|
||||
});
|
||||
});
|
|
@ -1,82 +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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { HeadlessChromiumDriver } from '../../browsers';
|
||||
import {
|
||||
createMockBrowserDriverFactory,
|
||||
createMockConfig,
|
||||
createMockConfigSchema,
|
||||
createMockLayoutInstance,
|
||||
createMockLevelLogger,
|
||||
createMockReportingCore,
|
||||
} from '../../test_helpers';
|
||||
import { CaptureConfig } from '../../types';
|
||||
import { LayoutInstance } from '../layouts';
|
||||
import { LevelLogger } from '../level_logger';
|
||||
import { getRenderErrors } from './get_render_errors';
|
||||
|
||||
describe('getRenderErrors', () => {
|
||||
let captureConfig: CaptureConfig;
|
||||
let layout: LayoutInstance;
|
||||
let logger: jest.Mocked<LevelLogger>;
|
||||
let browser: HeadlessChromiumDriver;
|
||||
|
||||
beforeEach(async () => {
|
||||
const schema = createMockConfigSchema();
|
||||
const config = createMockConfig(schema);
|
||||
const core = await createMockReportingCore(schema);
|
||||
|
||||
captureConfig = config.get('capture');
|
||||
layout = createMockLayoutInstance(captureConfig);
|
||||
logger = createMockLevelLogger();
|
||||
|
||||
await createMockBrowserDriverFactory(core, logger, {
|
||||
evaluate: jest.fn(
|
||||
async <T extends (...args: unknown[]) => unknown>({
|
||||
fn,
|
||||
args,
|
||||
}: {
|
||||
fn: T;
|
||||
args: Parameters<T>;
|
||||
}) => fn(...args)
|
||||
),
|
||||
getCreatePage: (driver) => {
|
||||
browser = driver;
|
||||
|
||||
return jest.fn();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = '';
|
||||
});
|
||||
|
||||
it('should extract the error messages', async () => {
|
||||
document.body.innerHTML = `
|
||||
<div dataRenderErrorSelector="a test error" />
|
||||
<div dataRenderErrorSelector="a test error" />
|
||||
<div dataRenderErrorSelector="a test error" />
|
||||
<div dataRenderErrorSelector="a test error" />
|
||||
`;
|
||||
|
||||
await expect(getRenderErrors(browser, layout, logger)).resolves.toEqual([
|
||||
'a test error',
|
||||
'a test error',
|
||||
'a test error',
|
||||
'a test error',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should extract the error messages, even when there are none', async () => {
|
||||
document.body.innerHTML = `
|
||||
<renderedSelector />
|
||||
`;
|
||||
|
||||
await expect(getRenderErrors(browser, layout, logger)).resolves.toEqual(undefined);
|
||||
});
|
||||
});
|
|
@ -1,76 +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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { HeadlessChromiumDriver } from '../../browsers';
|
||||
import {
|
||||
createMockBrowserDriverFactory,
|
||||
createMockConfig,
|
||||
createMockConfigSchema,
|
||||
createMockLayoutInstance,
|
||||
createMockLevelLogger,
|
||||
createMockReportingCore,
|
||||
} from '../../test_helpers';
|
||||
import { LayoutInstance } from '../layouts';
|
||||
import { LevelLogger } from '../level_logger';
|
||||
import { getTimeRange } from './get_time_range';
|
||||
|
||||
describe('getTimeRange', () => {
|
||||
let layout: LayoutInstance;
|
||||
let logger: jest.Mocked<LevelLogger>;
|
||||
let browser: HeadlessChromiumDriver;
|
||||
|
||||
beforeEach(async () => {
|
||||
const schema = createMockConfigSchema();
|
||||
const config = createMockConfig(schema);
|
||||
const captureConfig = config.get('capture');
|
||||
const core = await createMockReportingCore(schema);
|
||||
|
||||
layout = createMockLayoutInstance(captureConfig);
|
||||
logger = createMockLevelLogger();
|
||||
|
||||
await createMockBrowserDriverFactory(core, logger, {
|
||||
evaluate: jest.fn(
|
||||
async <T extends (...args: unknown[]) => unknown>({
|
||||
fn,
|
||||
args,
|
||||
}: {
|
||||
fn: T;
|
||||
args: Parameters<T>;
|
||||
}) => fn(...args)
|
||||
),
|
||||
getCreatePage: (driver) => {
|
||||
browser = driver;
|
||||
|
||||
return jest.fn();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = '';
|
||||
});
|
||||
|
||||
it('should return null when there is no duration element', async () => {
|
||||
await expect(getTimeRange(browser, layout, logger)).resolves.toBeNull();
|
||||
});
|
||||
|
||||
it('should return null when duration attrbute is empty', async () => {
|
||||
document.body.innerHTML = `
|
||||
<div timefilterDurationSelector />
|
||||
`;
|
||||
|
||||
await expect(getTimeRange(browser, layout, logger)).resolves.toBeNull();
|
||||
});
|
||||
|
||||
it('should return duration', async () => {
|
||||
document.body.innerHTML = `
|
||||
<div timefilterDurationSelector="10" />
|
||||
`;
|
||||
|
||||
await expect(getTimeRange(browser, layout, logger)).resolves.toBe('10');
|
||||
});
|
||||
});
|
|
@ -1,83 +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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { LevelLogger } from '../';
|
||||
import { UrlOrUrlLocatorTuple } from '../../../common/types';
|
||||
import { ConditionalHeaders } from '../../export_types/common';
|
||||
import { LayoutInstance } from '../layouts';
|
||||
|
||||
export { getScreenshots$ } from './observable';
|
||||
|
||||
export interface PhaseInstance {
|
||||
timeoutValue: number;
|
||||
configValue: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface PhaseTimeouts {
|
||||
openUrl: PhaseInstance;
|
||||
waitForElements: PhaseInstance;
|
||||
renderComplete: PhaseInstance;
|
||||
loadDelay: number;
|
||||
}
|
||||
|
||||
export interface ScreenshotObservableOpts {
|
||||
logger: LevelLogger;
|
||||
urlsOrUrlLocatorTuples: UrlOrUrlLocatorTuple[];
|
||||
conditionalHeaders: ConditionalHeaders;
|
||||
layout: LayoutInstance;
|
||||
browserTimezone?: string;
|
||||
}
|
||||
|
||||
export interface AttributesMap {
|
||||
[key: string]: string | null;
|
||||
}
|
||||
|
||||
export interface ElementPosition {
|
||||
boundingClientRect: {
|
||||
// modern browsers support x/y, but older ones don't
|
||||
top: number;
|
||||
left: number;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
scroll: {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ElementsPositionAndAttribute {
|
||||
position: ElementPosition;
|
||||
attributes: AttributesMap;
|
||||
}
|
||||
|
||||
export interface Screenshot {
|
||||
data: Buffer;
|
||||
title: string | null;
|
||||
description: string | null;
|
||||
}
|
||||
|
||||
export interface PageSetupResults {
|
||||
elementsPositionAndAttributes: ElementsPositionAndAttribute[] | null;
|
||||
timeRange: string | null;
|
||||
error?: Error;
|
||||
}
|
||||
|
||||
export interface ScreenshotResults {
|
||||
timeRange: string | null;
|
||||
screenshots: Screenshot[];
|
||||
error?: Error;
|
||||
|
||||
/**
|
||||
* Individual visualizations might encounter errors at runtime. If there are any they are added to this
|
||||
* field. Any text captured here is intended to be shown to the user for debugging purposes, reporting
|
||||
* does no further sanitization on these strings.
|
||||
*/
|
||||
renderErrors?: string[];
|
||||
elementsPositionAndAttributes?: ElementsPositionAndAttribute[]; // NOTE: for testing
|
||||
}
|
|
@ -1,490 +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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
jest.mock('puppeteer', () => ({
|
||||
launch: () => ({
|
||||
// Fixme needs event emitters
|
||||
newPage: () => ({
|
||||
emulateTimezone: jest.fn(),
|
||||
setDefaultTimeout: jest.fn(),
|
||||
}),
|
||||
process: jest.fn(),
|
||||
close: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
import moment from 'moment';
|
||||
import * as Rx from 'rxjs';
|
||||
import { ReportingCore } from '../..';
|
||||
import { HeadlessChromiumDriver } from '../../browsers';
|
||||
import { ConditionalHeaders } from '../../export_types/common';
|
||||
import {
|
||||
createMockBrowserDriverFactory,
|
||||
createMockConfig,
|
||||
createMockConfigSchema,
|
||||
createMockLayoutInstance,
|
||||
createMockLevelLogger,
|
||||
createMockReportingCore,
|
||||
} from '../../test_helpers';
|
||||
import * as contexts from './constants';
|
||||
import { getScreenshots$ } from './';
|
||||
|
||||
/*
|
||||
* Mocks
|
||||
*/
|
||||
const logger = createMockLevelLogger();
|
||||
|
||||
const mockSchema = createMockConfigSchema({
|
||||
capture: {
|
||||
loadDelay: moment.duration(2, 's'),
|
||||
timeouts: {
|
||||
openUrl: moment.duration(2, 'm'),
|
||||
waitForElements: moment.duration(20, 's'),
|
||||
renderComplete: moment.duration(10, 's'),
|
||||
},
|
||||
},
|
||||
});
|
||||
const mockConfig = createMockConfig(mockSchema);
|
||||
const captureConfig = mockConfig.get('capture');
|
||||
const mockLayout = createMockLayoutInstance(captureConfig);
|
||||
|
||||
let core: ReportingCore;
|
||||
|
||||
/*
|
||||
* Tests
|
||||
*/
|
||||
describe('Screenshot Observable Pipeline', () => {
|
||||
let mockBrowserDriverFactory: any;
|
||||
|
||||
beforeEach(async () => {
|
||||
core = await createMockReportingCore(mockSchema);
|
||||
mockBrowserDriverFactory = await createMockBrowserDriverFactory(core, logger, {});
|
||||
});
|
||||
|
||||
it('pipelines a single url into screenshot and timeRange', async () => {
|
||||
const result = await getScreenshots$(captureConfig, mockBrowserDriverFactory, {
|
||||
logger,
|
||||
urlsOrUrlLocatorTuples: ['/welcome/home/start/index.htm'],
|
||||
conditionalHeaders: {} as ConditionalHeaders,
|
||||
layout: mockLayout,
|
||||
browserTimezone: 'UTC',
|
||||
}).toPromise();
|
||||
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"elementsPositionAndAttributes": Array [
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"description": "Default ",
|
||||
"title": "Default Mock Title",
|
||||
},
|
||||
"position": Object {
|
||||
"boundingClientRect": Object {
|
||||
"height": 600,
|
||||
"left": 0,
|
||||
"top": 0,
|
||||
"width": 800,
|
||||
},
|
||||
"scroll": Object {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
"error": undefined,
|
||||
"screenshots": Array [
|
||||
Object {
|
||||
"data": Object {
|
||||
"data": Array [
|
||||
115,
|
||||
99,
|
||||
114,
|
||||
101,
|
||||
101,
|
||||
110,
|
||||
115,
|
||||
104,
|
||||
111,
|
||||
116,
|
||||
],
|
||||
"type": "Buffer",
|
||||
},
|
||||
"description": "Default ",
|
||||
"title": "Default Mock Title",
|
||||
},
|
||||
],
|
||||
"timeRange": "Default GetTimeRange Result",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('pipelines multiple urls into', async () => {
|
||||
// mock implementations
|
||||
const mockScreenshot = jest.fn(async () => Buffer.from('some screenshots'));
|
||||
const mockOpen = jest.fn();
|
||||
|
||||
// mocks
|
||||
mockBrowserDriverFactory = await createMockBrowserDriverFactory(core, logger, {
|
||||
screenshot: mockScreenshot,
|
||||
open: mockOpen,
|
||||
});
|
||||
|
||||
// test
|
||||
const result = await getScreenshots$(captureConfig, mockBrowserDriverFactory, {
|
||||
logger,
|
||||
urlsOrUrlLocatorTuples: [
|
||||
'/welcome/home/start/index2.htm',
|
||||
'/welcome/home/start/index.php3?page=./home.php',
|
||||
],
|
||||
conditionalHeaders: {} as ConditionalHeaders,
|
||||
layout: mockLayout,
|
||||
browserTimezone: 'UTC',
|
||||
}).toPromise();
|
||||
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"elementsPositionAndAttributes": Array [
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"description": "Default ",
|
||||
"title": "Default Mock Title",
|
||||
},
|
||||
"position": Object {
|
||||
"boundingClientRect": Object {
|
||||
"height": 600,
|
||||
"left": 0,
|
||||
"top": 0,
|
||||
"width": 800,
|
||||
},
|
||||
"scroll": Object {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
"error": undefined,
|
||||
"screenshots": Array [
|
||||
Object {
|
||||
"data": Object {
|
||||
"data": Array [
|
||||
115,
|
||||
111,
|
||||
109,
|
||||
101,
|
||||
32,
|
||||
115,
|
||||
99,
|
||||
114,
|
||||
101,
|
||||
101,
|
||||
110,
|
||||
115,
|
||||
104,
|
||||
111,
|
||||
116,
|
||||
115,
|
||||
],
|
||||
"type": "Buffer",
|
||||
},
|
||||
"description": "Default ",
|
||||
"title": "Default Mock Title",
|
||||
},
|
||||
],
|
||||
"timeRange": "Default GetTimeRange Result",
|
||||
},
|
||||
Object {
|
||||
"elementsPositionAndAttributes": Array [
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"description": "Default ",
|
||||
"title": "Default Mock Title",
|
||||
},
|
||||
"position": Object {
|
||||
"boundingClientRect": Object {
|
||||
"height": 600,
|
||||
"left": 0,
|
||||
"top": 0,
|
||||
"width": 800,
|
||||
},
|
||||
"scroll": Object {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
"error": undefined,
|
||||
"screenshots": Array [
|
||||
Object {
|
||||
"data": Object {
|
||||
"data": Array [
|
||||
115,
|
||||
111,
|
||||
109,
|
||||
101,
|
||||
32,
|
||||
115,
|
||||
99,
|
||||
114,
|
||||
101,
|
||||
101,
|
||||
110,
|
||||
115,
|
||||
104,
|
||||
111,
|
||||
116,
|
||||
115,
|
||||
],
|
||||
"type": "Buffer",
|
||||
},
|
||||
"description": "Default ",
|
||||
"title": "Default Mock Title",
|
||||
},
|
||||
],
|
||||
"timeRange": "Default GetTimeRange Result",
|
||||
},
|
||||
]
|
||||
`);
|
||||
|
||||
// ensures the correct selectors are waited on for multi URL jobs
|
||||
expect(mockOpen.mock.calls.length).toBe(2);
|
||||
|
||||
const firstSelector = mockOpen.mock.calls[0][1].waitForSelector;
|
||||
expect(firstSelector).toBe('.kbnAppWrapper');
|
||||
|
||||
const secondSelector = mockOpen.mock.calls[1][1].waitForSelector;
|
||||
expect(secondSelector).toBe('[data-shared-page="2"]');
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
it('recovers if waitForSelector fails', async () => {
|
||||
// mock implementations
|
||||
const mockWaitForSelector = jest.fn().mockImplementation((selectorArg: string) => {
|
||||
throw new Error('Mock error!');
|
||||
});
|
||||
|
||||
// mocks
|
||||
mockBrowserDriverFactory = await createMockBrowserDriverFactory(core, logger, {
|
||||
waitForSelector: mockWaitForSelector,
|
||||
});
|
||||
|
||||
// test
|
||||
const getScreenshot = async () => {
|
||||
return await getScreenshots$(captureConfig, mockBrowserDriverFactory, {
|
||||
logger,
|
||||
urlsOrUrlLocatorTuples: [
|
||||
'/welcome/home/start/index2.htm',
|
||||
'/welcome/home/start/index.php3?page=./home.php3',
|
||||
],
|
||||
conditionalHeaders: {} as ConditionalHeaders,
|
||||
layout: mockLayout,
|
||||
browserTimezone: 'UTC',
|
||||
}).toPromise();
|
||||
};
|
||||
|
||||
await expect(getScreenshot()).resolves.toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"elementsPositionAndAttributes": Array [
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"position": Object {
|
||||
"boundingClientRect": Object {
|
||||
"height": 100,
|
||||
"left": 0,
|
||||
"top": 0,
|
||||
"width": 100,
|
||||
},
|
||||
"scroll": Object {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
"error": [Error: The "wait for elements" phase encountered an error: Error: An error occurred when trying to read the page for visualization panel info: Error: Mock error!],
|
||||
"screenshots": Array [
|
||||
Object {
|
||||
"data": Object {
|
||||
"data": Array [
|
||||
115,
|
||||
99,
|
||||
114,
|
||||
101,
|
||||
101,
|
||||
110,
|
||||
115,
|
||||
104,
|
||||
111,
|
||||
116,
|
||||
],
|
||||
"type": "Buffer",
|
||||
},
|
||||
"description": undefined,
|
||||
"title": undefined,
|
||||
},
|
||||
],
|
||||
"timeRange": null,
|
||||
},
|
||||
Object {
|
||||
"elementsPositionAndAttributes": Array [
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"position": Object {
|
||||
"boundingClientRect": Object {
|
||||
"height": 100,
|
||||
"left": 0,
|
||||
"top": 0,
|
||||
"width": 100,
|
||||
},
|
||||
"scroll": Object {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
"error": [Error: The "wait for elements" phase encountered an error: Error: An error occurred when trying to read the page for visualization panel info: Error: Mock error!],
|
||||
"screenshots": Array [
|
||||
Object {
|
||||
"data": Object {
|
||||
"data": Array [
|
||||
115,
|
||||
99,
|
||||
114,
|
||||
101,
|
||||
101,
|
||||
110,
|
||||
115,
|
||||
104,
|
||||
111,
|
||||
116,
|
||||
],
|
||||
"type": "Buffer",
|
||||
},
|
||||
"description": undefined,
|
||||
"title": undefined,
|
||||
},
|
||||
],
|
||||
"timeRange": null,
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('observes page exit', async () => {
|
||||
// mocks
|
||||
const mockGetCreatePage = (driver: HeadlessChromiumDriver) =>
|
||||
jest
|
||||
.fn()
|
||||
.mockImplementation(() =>
|
||||
Rx.of({ driver, exit$: Rx.throwError('Instant timeout has fired!') })
|
||||
);
|
||||
|
||||
const mockWaitForSelector = jest.fn().mockImplementation((selectorArg: string) => {
|
||||
return Rx.never().toPromise();
|
||||
});
|
||||
|
||||
mockBrowserDriverFactory = await createMockBrowserDriverFactory(core, logger, {
|
||||
getCreatePage: mockGetCreatePage,
|
||||
waitForSelector: mockWaitForSelector,
|
||||
});
|
||||
|
||||
// test
|
||||
const getScreenshot = async () => {
|
||||
return await getScreenshots$(captureConfig, mockBrowserDriverFactory, {
|
||||
logger,
|
||||
urlsOrUrlLocatorTuples: ['/welcome/home/start/index.php3?page=./home.php3'],
|
||||
conditionalHeaders: {} as ConditionalHeaders,
|
||||
layout: mockLayout,
|
||||
browserTimezone: 'UTC',
|
||||
}).toPromise();
|
||||
};
|
||||
|
||||
await expect(getScreenshot()).rejects.toMatchInlineSnapshot(`"Instant timeout has fired!"`);
|
||||
});
|
||||
|
||||
it(`uses defaults for element positions and size when Kibana page is not ready`, async () => {
|
||||
// mocks
|
||||
const mockBrowserEvaluate = jest.fn();
|
||||
mockBrowserEvaluate.mockImplementation(() => {
|
||||
const lastCallIndex = mockBrowserEvaluate.mock.calls.length - 1;
|
||||
const { context: mockCall } = mockBrowserEvaluate.mock.calls[lastCallIndex][1];
|
||||
|
||||
if (mockCall === contexts.CONTEXT_ELEMENTATTRIBUTES) {
|
||||
return Promise.resolve(null);
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
});
|
||||
mockBrowserDriverFactory = await createMockBrowserDriverFactory(core, logger, {
|
||||
evaluate: mockBrowserEvaluate,
|
||||
});
|
||||
mockLayout.getViewport = () => null;
|
||||
|
||||
const screenshots = await getScreenshots$(captureConfig, mockBrowserDriverFactory, {
|
||||
logger,
|
||||
urlsOrUrlLocatorTuples: ['/welcome/home/start/index.php3?page=./home.php3'],
|
||||
conditionalHeaders: {} as ConditionalHeaders,
|
||||
layout: mockLayout,
|
||||
browserTimezone: 'UTC',
|
||||
}).toPromise();
|
||||
|
||||
expect(screenshots).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"elementsPositionAndAttributes": Array [
|
||||
Object {
|
||||
"attributes": Object {},
|
||||
"position": Object {
|
||||
"boundingClientRect": Object {
|
||||
"height": 1200,
|
||||
"left": 0,
|
||||
"top": 0,
|
||||
"width": 1800,
|
||||
},
|
||||
"scroll": Object {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
"error": undefined,
|
||||
"screenshots": Array [
|
||||
Object {
|
||||
"data": Object {
|
||||
"data": Array [
|
||||
115,
|
||||
99,
|
||||
114,
|
||||
101,
|
||||
101,
|
||||
110,
|
||||
115,
|
||||
104,
|
||||
111,
|
||||
116,
|
||||
],
|
||||
"type": "Buffer",
|
||||
},
|
||||
"description": undefined,
|
||||
"title": undefined,
|
||||
},
|
||||
],
|
||||
"timeRange": undefined,
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,85 +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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import apm from 'elastic-apm-node';
|
||||
import * as Rx from 'rxjs';
|
||||
import { catchError, concatMap, first, mergeMap, take, takeUntil, toArray } from 'rxjs/operators';
|
||||
import { durationToNumber } from '../../../common/schema_utils';
|
||||
import { REPORTING_TRANSACTION_TYPE } from '../../../common/constants';
|
||||
import { HeadlessChromiumDriverFactory } from '../../browsers';
|
||||
import { CaptureConfig } from '../../types';
|
||||
import {
|
||||
ElementPosition,
|
||||
ElementsPositionAndAttribute,
|
||||
PageSetupResults,
|
||||
ScreenshotObservableOpts,
|
||||
ScreenshotResults,
|
||||
} from './';
|
||||
import { ScreenshotObservableHandler } from './observable_handler';
|
||||
|
||||
export type { ElementPosition, ElementsPositionAndAttribute, ScreenshotResults };
|
||||
|
||||
const getTimeouts = (captureConfig: CaptureConfig) => ({
|
||||
openUrl: {
|
||||
timeoutValue: durationToNumber(captureConfig.timeouts.openUrl),
|
||||
configValue: `xpack.reporting.capture.timeouts.openUrl`,
|
||||
label: 'open URL',
|
||||
},
|
||||
waitForElements: {
|
||||
timeoutValue: durationToNumber(captureConfig.timeouts.waitForElements),
|
||||
configValue: `xpack.reporting.capture.timeouts.waitForElements`,
|
||||
label: 'wait for elements',
|
||||
},
|
||||
renderComplete: {
|
||||
timeoutValue: durationToNumber(captureConfig.timeouts.renderComplete),
|
||||
configValue: `xpack.reporting.capture.timeouts.renderComplete`,
|
||||
label: 'render complete',
|
||||
},
|
||||
loadDelay: durationToNumber(captureConfig.loadDelay),
|
||||
});
|
||||
|
||||
export function getScreenshots$(
|
||||
captureConfig: CaptureConfig,
|
||||
browserDriverFactory: HeadlessChromiumDriverFactory,
|
||||
opts: ScreenshotObservableOpts
|
||||
): Rx.Observable<ScreenshotResults[]> {
|
||||
const apmTrans = apm.startTransaction('screenshot-pipeline', REPORTING_TRANSACTION_TYPE);
|
||||
const apmCreatePage = apmTrans?.startSpan('create-page', 'wait');
|
||||
const { browserTimezone, logger } = opts;
|
||||
|
||||
return browserDriverFactory.createPage({ browserTimezone }, logger).pipe(
|
||||
mergeMap(({ driver, exit$ }) => {
|
||||
apmCreatePage?.end();
|
||||
exit$.subscribe({ error: () => apmTrans?.end() });
|
||||
|
||||
const screen = new ScreenshotObservableHandler(driver, opts, getTimeouts(captureConfig));
|
||||
|
||||
return Rx.from(opts.urlsOrUrlLocatorTuples).pipe(
|
||||
concatMap((urlOrUrlLocatorTuple, index) =>
|
||||
screen.setupPage(index, urlOrUrlLocatorTuple, apmTrans).pipe(
|
||||
catchError((err) => {
|
||||
screen.checkPageIsOpen(); // this fails the job if the browser has closed
|
||||
|
||||
logger.error(err);
|
||||
return Rx.of({ ...defaultSetupResult, error: err }); // allow failover screenshot capture
|
||||
}),
|
||||
takeUntil(exit$),
|
||||
screen.getScreenshots()
|
||||
)
|
||||
),
|
||||
take(opts.urlsOrUrlLocatorTuples.length),
|
||||
toArray()
|
||||
);
|
||||
}),
|
||||
first()
|
||||
);
|
||||
}
|
||||
|
||||
const defaultSetupResult: PageSetupResults = {
|
||||
elementsPositionAndAttributes: null,
|
||||
timeRange: null,
|
||||
};
|
|
@ -1,160 +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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import * as Rx from 'rxjs';
|
||||
import { first, map } from 'rxjs/operators';
|
||||
import { HeadlessChromiumDriver } from '../../browsers';
|
||||
import { ReportingConfigType } from '../../config';
|
||||
import { ConditionalHeaders } from '../../export_types/common';
|
||||
import {
|
||||
createMockBrowserDriverFactory,
|
||||
createMockConfigSchema,
|
||||
createMockLayoutInstance,
|
||||
createMockLevelLogger,
|
||||
createMockReportingCore,
|
||||
} from '../../test_helpers';
|
||||
import { LayoutInstance } from '../layouts';
|
||||
import { PhaseTimeouts, ScreenshotObservableOpts } from './';
|
||||
import { ScreenshotObservableHandler } from './observable_handler';
|
||||
|
||||
const logger = createMockLevelLogger();
|
||||
|
||||
describe('ScreenshotObservableHandler', () => {
|
||||
let captureConfig: ReportingConfigType['capture'];
|
||||
let layout: LayoutInstance;
|
||||
let conditionalHeaders: ConditionalHeaders;
|
||||
let opts: ScreenshotObservableOpts;
|
||||
let timeouts: PhaseTimeouts;
|
||||
let driver: HeadlessChromiumDriver;
|
||||
|
||||
beforeAll(async () => {
|
||||
captureConfig = {
|
||||
timeouts: {
|
||||
openUrl: 30000,
|
||||
waitForElements: 30000,
|
||||
renderComplete: 30000,
|
||||
},
|
||||
loadDelay: 5000,
|
||||
} as unknown as typeof captureConfig;
|
||||
|
||||
layout = createMockLayoutInstance(captureConfig);
|
||||
|
||||
conditionalHeaders = {
|
||||
headers: { testHeader: 'testHeadValue' },
|
||||
conditions: {} as unknown as ConditionalHeaders['conditions'],
|
||||
};
|
||||
|
||||
opts = {
|
||||
conditionalHeaders,
|
||||
layout,
|
||||
logger,
|
||||
urlsOrUrlLocatorTuples: [],
|
||||
};
|
||||
|
||||
timeouts = {
|
||||
openUrl: {
|
||||
timeoutValue: 60000,
|
||||
configValue: `xpack.reporting.capture.timeouts.openUrl`,
|
||||
label: 'open URL',
|
||||
},
|
||||
waitForElements: {
|
||||
timeoutValue: 30000,
|
||||
configValue: `xpack.reporting.capture.timeouts.waitForElements`,
|
||||
label: 'wait for elements',
|
||||
},
|
||||
renderComplete: {
|
||||
timeoutValue: 60000,
|
||||
configValue: `xpack.reporting.capture.timeouts.renderComplete`,
|
||||
label: 'render complete',
|
||||
},
|
||||
loadDelay: 5000,
|
||||
};
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
const reporting = await createMockReportingCore(createMockConfigSchema());
|
||||
const driverFactory = await createMockBrowserDriverFactory(reporting, logger);
|
||||
({ driver } = await driverFactory.createPage({}, logger).pipe(first()).toPromise());
|
||||
driver.isPageOpen = jest.fn().mockImplementation(() => true);
|
||||
});
|
||||
|
||||
describe('waitUntil', () => {
|
||||
it('catches TimeoutError and references the timeout config in a custom message', async () => {
|
||||
const screenshots = new ScreenshotObservableHandler(driver, opts, timeouts);
|
||||
const test$ = Rx.interval(1000).pipe(
|
||||
screenshots.waitUntil({
|
||||
timeoutValue: 200,
|
||||
configValue: 'test.config.value',
|
||||
label: 'Test Config',
|
||||
})
|
||||
);
|
||||
|
||||
const testPipeline = () => test$.toPromise();
|
||||
await expect(testPipeline).rejects.toMatchInlineSnapshot(
|
||||
`[Error: The "Test Config" phase took longer than 0.2 seconds. You may need to increase "test.config.value"]`
|
||||
);
|
||||
});
|
||||
|
||||
it('catches other Errors and explains where they were thrown', async () => {
|
||||
const screenshots = new ScreenshotObservableHandler(driver, opts, timeouts);
|
||||
const test$ = Rx.throwError(new Error(`Test Error to Throw`)).pipe(
|
||||
screenshots.waitUntil({
|
||||
timeoutValue: 200,
|
||||
configValue: 'test.config.value',
|
||||
label: 'Test Config',
|
||||
})
|
||||
);
|
||||
|
||||
const testPipeline = () => test$.toPromise();
|
||||
await expect(testPipeline).rejects.toMatchInlineSnapshot(
|
||||
`[Error: The "Test Config" phase encountered an error: Error: Test Error to Throw]`
|
||||
);
|
||||
});
|
||||
|
||||
it('is a pass-through if there is no Error', async () => {
|
||||
const screenshots = new ScreenshotObservableHandler(driver, opts, timeouts);
|
||||
const test$ = Rx.of('nice to see you').pipe(
|
||||
screenshots.waitUntil({
|
||||
timeoutValue: 20,
|
||||
configValue: 'xxxxxxxxxxxxxxxxx',
|
||||
label: 'xxxxxxxxxxx',
|
||||
})
|
||||
);
|
||||
|
||||
await expect(test$.toPromise()).resolves.toBe(`nice to see you`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkPageIsOpen', () => {
|
||||
it('throws a decorated Error when page is not open', async () => {
|
||||
driver.isPageOpen = jest.fn().mockImplementation(() => false);
|
||||
const screenshots = new ScreenshotObservableHandler(driver, opts, timeouts);
|
||||
const test$ = Rx.of(234455).pipe(
|
||||
map((input) => {
|
||||
screenshots.checkPageIsOpen();
|
||||
return input;
|
||||
})
|
||||
);
|
||||
|
||||
await expect(test$.toPromise()).rejects.toMatchInlineSnapshot(
|
||||
`[Error: Browser was closed unexpectedly! Check the server logs for more info.]`
|
||||
);
|
||||
});
|
||||
|
||||
it('is a pass-through when the page is open', async () => {
|
||||
const screenshots = new ScreenshotObservableHandler(driver, opts, timeouts);
|
||||
const test$ = Rx.of(234455).pipe(
|
||||
map((input) => {
|
||||
screenshots.checkPageIsOpen();
|
||||
return input;
|
||||
})
|
||||
);
|
||||
|
||||
await expect(test$.toPromise()).resolves.toBe(234455);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,197 +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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import apm from 'elastic-apm-node';
|
||||
import * as Rx from 'rxjs';
|
||||
import { catchError, mergeMap, switchMapTo, timeoutWith } from 'rxjs/operators';
|
||||
import { numberToDuration } from '../../../common/schema_utils';
|
||||
import { UrlOrUrlLocatorTuple } from '../../../common/types';
|
||||
import { HeadlessChromiumDriver } from '../../browsers';
|
||||
import { getChromiumDisconnectedError } from '../../browsers/chromium';
|
||||
import {
|
||||
PageSetupResults,
|
||||
PhaseInstance,
|
||||
PhaseTimeouts,
|
||||
ScreenshotObservableOpts,
|
||||
ScreenshotResults,
|
||||
} from './';
|
||||
import { getElementPositionAndAttributes } from './get_element_position_data';
|
||||
import { getNumberOfItems } from './get_number_of_items';
|
||||
import { getRenderErrors } from './get_render_errors';
|
||||
import { getScreenshots } from './get_screenshots';
|
||||
import { getTimeRange } from './get_time_range';
|
||||
import { injectCustomCss } from './inject_css';
|
||||
import { openUrl } from './open_url';
|
||||
import { waitForRenderComplete } from './wait_for_render';
|
||||
import { waitForVisualizations } from './wait_for_visualizations';
|
||||
|
||||
export class ScreenshotObservableHandler {
|
||||
private conditionalHeaders: ScreenshotObservableOpts['conditionalHeaders'];
|
||||
private layout: ScreenshotObservableOpts['layout'];
|
||||
private logger: ScreenshotObservableOpts['logger'];
|
||||
|
||||
constructor(
|
||||
private readonly driver: HeadlessChromiumDriver,
|
||||
opts: ScreenshotObservableOpts,
|
||||
private timeouts: PhaseTimeouts
|
||||
) {
|
||||
this.conditionalHeaders = opts.conditionalHeaders;
|
||||
this.layout = opts.layout;
|
||||
this.logger = opts.logger;
|
||||
}
|
||||
|
||||
/*
|
||||
* Decorates a TimeoutError with context of the phase that has timed out.
|
||||
*/
|
||||
public waitUntil<O>(phase: PhaseInstance) {
|
||||
const { timeoutValue, label, configValue } = phase;
|
||||
|
||||
return (source: Rx.Observable<O>) =>
|
||||
source.pipe(
|
||||
catchError((error) => {
|
||||
throw new Error(`The "${label}" phase encountered an error: ${error}`);
|
||||
}),
|
||||
timeoutWith(
|
||||
timeoutValue,
|
||||
Rx.throwError(
|
||||
new Error(
|
||||
`The "${label}" phase took longer than ${numberToDuration(
|
||||
timeoutValue
|
||||
).asSeconds()} seconds. You may need to increase "${configValue}"`
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private openUrl(index: number, urlOrUrlLocatorTuple: UrlOrUrlLocatorTuple) {
|
||||
return Rx.defer(() =>
|
||||
openUrl(
|
||||
this.timeouts.openUrl.timeoutValue,
|
||||
this.driver,
|
||||
index,
|
||||
urlOrUrlLocatorTuple,
|
||||
this.conditionalHeaders,
|
||||
this.layout,
|
||||
this.logger
|
||||
)
|
||||
).pipe(this.waitUntil(this.timeouts.openUrl));
|
||||
}
|
||||
|
||||
private waitForElements() {
|
||||
const driver = this.driver;
|
||||
const waitTimeout = this.timeouts.waitForElements.timeoutValue;
|
||||
|
||||
return Rx.defer(() => getNumberOfItems(waitTimeout, driver, this.layout, this.logger)).pipe(
|
||||
mergeMap((itemsCount) => {
|
||||
// set the viewport to the dimentions from the job, to allow elements to flow into the expected layout
|
||||
const viewport = this.layout.getViewport(itemsCount) || getDefaultViewPort();
|
||||
|
||||
return Rx.forkJoin([
|
||||
driver.setViewport(viewport, this.logger),
|
||||
waitForVisualizations(waitTimeout, driver, itemsCount, this.layout, this.logger),
|
||||
]);
|
||||
}),
|
||||
this.waitUntil(this.timeouts.waitForElements)
|
||||
);
|
||||
}
|
||||
|
||||
private completeRender(apmTrans: apm.Transaction | null) {
|
||||
const driver = this.driver;
|
||||
const layout = this.layout;
|
||||
const logger = this.logger;
|
||||
|
||||
return Rx.defer(async () => {
|
||||
// Waiting till _after_ elements have rendered before injecting our CSS
|
||||
// allows for them to be displayed properly in many cases
|
||||
await injectCustomCss(driver, layout, logger);
|
||||
|
||||
const apmPositionElements = apmTrans?.startSpan('position-elements', 'correction');
|
||||
// position panel elements for print layout
|
||||
await layout.positionElements?.(driver, logger);
|
||||
apmPositionElements?.end();
|
||||
|
||||
await waitForRenderComplete(this.timeouts.loadDelay, driver, layout, logger);
|
||||
}).pipe(
|
||||
mergeMap(() =>
|
||||
Rx.forkJoin({
|
||||
timeRange: getTimeRange(driver, layout, logger),
|
||||
elementsPositionAndAttributes: getElementPositionAndAttributes(driver, layout, logger),
|
||||
renderErrors: getRenderErrors(driver, layout, logger),
|
||||
})
|
||||
),
|
||||
this.waitUntil(this.timeouts.renderComplete)
|
||||
);
|
||||
}
|
||||
|
||||
public setupPage(
|
||||
index: number,
|
||||
urlOrUrlLocatorTuple: UrlOrUrlLocatorTuple,
|
||||
apmTrans: apm.Transaction | null
|
||||
) {
|
||||
return this.openUrl(index, urlOrUrlLocatorTuple).pipe(
|
||||
switchMapTo(this.waitForElements()),
|
||||
switchMapTo(this.completeRender(apmTrans))
|
||||
);
|
||||
}
|
||||
|
||||
public getScreenshots() {
|
||||
return (withRenderComplete: Rx.Observable<PageSetupResults>) =>
|
||||
withRenderComplete.pipe(
|
||||
mergeMap(async (data: PageSetupResults): Promise<ScreenshotResults> => {
|
||||
this.checkPageIsOpen(); // fail the report job if the browser has closed
|
||||
|
||||
const elements =
|
||||
data.elementsPositionAndAttributes ??
|
||||
getDefaultElementPosition(this.layout.getViewport(1));
|
||||
const screenshots = await getScreenshots(this.driver, elements, this.logger);
|
||||
const { timeRange, error: setupError } = data;
|
||||
|
||||
return {
|
||||
timeRange,
|
||||
screenshots,
|
||||
error: setupError,
|
||||
elementsPositionAndAttributes: elements,
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public checkPageIsOpen() {
|
||||
if (!this.driver.isPageOpen()) {
|
||||
throw getChromiumDisconnectedError();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_SCREENSHOT_CLIP_HEIGHT = 1200;
|
||||
const DEFAULT_SCREENSHOT_CLIP_WIDTH = 1800;
|
||||
|
||||
const getDefaultElementPosition = (dimensions: { height?: number; width?: number } | null) => {
|
||||
const height = dimensions?.height || DEFAULT_SCREENSHOT_CLIP_HEIGHT;
|
||||
const width = dimensions?.width || DEFAULT_SCREENSHOT_CLIP_WIDTH;
|
||||
|
||||
return [
|
||||
{
|
||||
position: {
|
||||
boundingClientRect: { top: 0, left: 0, height, width },
|
||||
scroll: { x: 0, y: 0 },
|
||||
},
|
||||
attributes: {},
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
/*
|
||||
* If Kibana is showing a non-HTML error message, the viewport might not be
|
||||
* provided by the browser.
|
||||
*/
|
||||
const getDefaultViewPort = () => ({
|
||||
height: DEFAULT_SCREENSHOT_CLIP_HEIGHT,
|
||||
width: DEFAULT_SCREENSHOT_CLIP_WIDTH,
|
||||
zoom: 1,
|
||||
});
|
|
@ -34,7 +34,6 @@ export const mapping = {
|
|||
},
|
||||
},
|
||||
},
|
||||
browser_type: { type: 'keyword' },
|
||||
migration_version: { type: 'keyword' }, // new field (7.14) to distinguish reports that were scheduled with Task Manager
|
||||
jobtype: { type: 'keyword' },
|
||||
payload: { type: 'object', enabled: false },
|
||||
|
|
|
@ -13,7 +13,6 @@ describe('Class Report', () => {
|
|||
_index: '.reporting-test-index-12345',
|
||||
jobtype: 'test-report',
|
||||
created_by: 'created_by_test_string',
|
||||
browser_type: 'browser_type_test_string',
|
||||
max_attempts: 50,
|
||||
payload: {
|
||||
headers: 'payload_test_field',
|
||||
|
@ -28,7 +27,6 @@ describe('Class Report', () => {
|
|||
|
||||
expect(report.toReportSource()).toMatchObject({
|
||||
attempts: 0,
|
||||
browser_type: 'browser_type_test_string',
|
||||
completed_at: undefined,
|
||||
created_by: 'created_by_test_string',
|
||||
jobtype: 'test-report',
|
||||
|
@ -49,7 +47,6 @@ describe('Class Report', () => {
|
|||
});
|
||||
expect(report.toApiJSON()).toMatchObject({
|
||||
attempts: 0,
|
||||
browser_type: 'browser_type_test_string',
|
||||
created_by: 'created_by_test_string',
|
||||
index: '.reporting-test-index-12345',
|
||||
jobtype: 'test-report',
|
||||
|
@ -68,7 +65,6 @@ describe('Class Report', () => {
|
|||
_index: '.reporting-test-index-12345',
|
||||
jobtype: 'test-report',
|
||||
created_by: 'created_by_test_string',
|
||||
browser_type: 'browser_type_test_string',
|
||||
max_attempts: 50,
|
||||
payload: {
|
||||
headers: 'payload_test_field',
|
||||
|
@ -91,7 +87,6 @@ describe('Class Report', () => {
|
|||
|
||||
expect(report.toReportSource()).toMatchObject({
|
||||
attempts: 0,
|
||||
browser_type: 'browser_type_test_string',
|
||||
completed_at: undefined,
|
||||
created_by: 'created_by_test_string',
|
||||
jobtype: 'test-report',
|
||||
|
@ -113,7 +108,6 @@ describe('Class Report', () => {
|
|||
});
|
||||
expect(report.toApiJSON()).toMatchObject({
|
||||
attempts: 0,
|
||||
browser_type: 'browser_type_test_string',
|
||||
completed_at: undefined,
|
||||
created_by: 'created_by_test_string',
|
||||
id: '12342p9o387549o2345',
|
||||
|
|
|
@ -38,7 +38,6 @@ export class Report implements Partial<ReportSource & ReportDocumentHead> {
|
|||
public readonly payload: ReportSource['payload'];
|
||||
|
||||
public readonly meta: ReportSource['meta'];
|
||||
public readonly browser_type: ReportSource['browser_type'];
|
||||
|
||||
public readonly status: ReportSource['status'];
|
||||
public readonly attempts: ReportSource['attempts'];
|
||||
|
@ -82,7 +81,6 @@ export class Report implements Partial<ReportSource & ReportDocumentHead> {
|
|||
this.max_attempts = opts.max_attempts;
|
||||
this.attempts = opts.attempts || 0;
|
||||
this.timeout = opts.timeout;
|
||||
this.browser_type = opts.browser_type;
|
||||
|
||||
this.process_expiration = opts.process_expiration;
|
||||
this.started_at = opts.started_at;
|
||||
|
@ -125,7 +123,6 @@ export class Report implements Partial<ReportSource & ReportDocumentHead> {
|
|||
meta: this.meta,
|
||||
timeout: this.timeout,
|
||||
max_attempts: this.max_attempts,
|
||||
browser_type: this.browser_type,
|
||||
status: this.status,
|
||||
attempts: this.attempts,
|
||||
started_at: this.started_at,
|
||||
|
@ -170,7 +167,6 @@ export class Report implements Partial<ReportSource & ReportDocumentHead> {
|
|||
meta: this.meta,
|
||||
timeout: this.timeout,
|
||||
max_attempts: this.max_attempts,
|
||||
browser_type: this.browser_type,
|
||||
status: this.status,
|
||||
attempts: this.attempts,
|
||||
started_at: this.started_at,
|
||||
|
|
|
@ -193,7 +193,6 @@ describe('ReportingStore', () => {
|
|||
status: 'pending',
|
||||
meta: { testMeta: 'meta' } as any,
|
||||
payload: { testPayload: 'payload' } as any,
|
||||
browser_type: 'browser type string',
|
||||
attempts: 0,
|
||||
max_attempts: 1,
|
||||
timeout: 30000,
|
||||
|
@ -214,7 +213,6 @@ describe('ReportingStore', () => {
|
|||
"_primary_term": 1234,
|
||||
"_seq_no": 5678,
|
||||
"attempts": 0,
|
||||
"browser_type": "browser type string",
|
||||
"completed_at": undefined,
|
||||
"created_at": "some time",
|
||||
"created_by": "some security person",
|
||||
|
@ -247,7 +245,6 @@ describe('ReportingStore', () => {
|
|||
_primary_term: 10002,
|
||||
jobtype: 'test-report',
|
||||
created_by: 'created_by_test_string',
|
||||
browser_type: 'browser_type_test_string',
|
||||
max_attempts: 50,
|
||||
payload: {
|
||||
title: 'test report',
|
||||
|
@ -279,7 +276,6 @@ describe('ReportingStore', () => {
|
|||
_primary_term: 10002,
|
||||
jobtype: 'test-report',
|
||||
created_by: 'created_by_test_string',
|
||||
browser_type: 'browser_type_test_string',
|
||||
max_attempts: 50,
|
||||
payload: {
|
||||
title: 'test report',
|
||||
|
@ -310,7 +306,6 @@ describe('ReportingStore', () => {
|
|||
_primary_term: 10002,
|
||||
jobtype: 'test-report',
|
||||
created_by: 'created_by_test_string',
|
||||
browser_type: 'browser_type_test_string',
|
||||
max_attempts: 50,
|
||||
payload: {
|
||||
title: 'test report',
|
||||
|
@ -341,7 +336,6 @@ describe('ReportingStore', () => {
|
|||
_primary_term: 10002,
|
||||
jobtype: 'test-report',
|
||||
created_by: 'created_by_test_string',
|
||||
browser_type: 'browser_type_test_string',
|
||||
max_attempts: 50,
|
||||
payload: {
|
||||
title: 'test report',
|
||||
|
@ -385,7 +379,6 @@ describe('ReportingStore', () => {
|
|||
_primary_term: 10002,
|
||||
jobtype: 'test-report-2',
|
||||
created_by: 'created_by_test_string',
|
||||
browser_type: 'browser_type_test_string',
|
||||
status: 'processing',
|
||||
process_expiration: '2002',
|
||||
max_attempts: 3,
|
||||
|
|
|
@ -24,7 +24,6 @@ import { MIGRATION_VERSION } from './report';
|
|||
export type ReportProcessingFields = Required<{
|
||||
kibana_id: Report['kibana_id'];
|
||||
kibana_name: Report['kibana_name'];
|
||||
browser_type: Report['browser_type'];
|
||||
attempts: Report['attempts'];
|
||||
started_at: Report['started_at'];
|
||||
max_attempts: Report['max_attempts'];
|
||||
|
@ -252,7 +251,6 @@ export class ReportingStore {
|
|||
_primary_term: document._primary_term,
|
||||
jobtype: document._source?.jobtype,
|
||||
attempts: document._source?.attempts,
|
||||
browser_type: document._source?.browser_type,
|
||||
created_at: document._source?.created_at,
|
||||
created_by: document._source?.created_by,
|
||||
max_attempts: document._source?.max_attempts,
|
||||
|
|
|
@ -159,7 +159,6 @@ export class ExecuteReportTask implements ReportingTask {
|
|||
const doc: ReportProcessingFields = {
|
||||
kibana_id: this.kibanaId,
|
||||
kibana_name: this.kibanaName,
|
||||
browser_type: this.config.capture.browser.type,
|
||||
attempts: report.attempts + 1,
|
||||
max_attempts: maxAttempts,
|
||||
started_at: startTime,
|
||||
|
|
|
@ -5,16 +5,6 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
jest.mock('./browsers/install', () => ({
|
||||
installBrowser: jest.fn().mockImplementation(() => ({
|
||||
binaryPath$: {
|
||||
pipe: jest.fn().mockImplementation(() => ({
|
||||
toPromise: () => Promise.resolve(),
|
||||
})),
|
||||
},
|
||||
})),
|
||||
}));
|
||||
|
||||
import { coreMock } from 'src/core/server/mocks';
|
||||
import { featuresPluginMock } from '../../features/server/mocks';
|
||||
import { TaskManagerSetupContract } from '../../task_manager/server';
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
import type { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/server';
|
||||
import { PLUGIN_ID } from '../common/constants';
|
||||
import { ReportingCore } from './';
|
||||
import { initializeBrowserDriverFactory } from './browsers';
|
||||
import { buildConfig, registerUiSettings, ReportingConfigType } from './config';
|
||||
import { registerDeprecations } from './deprecations';
|
||||
import { LevelLogger, ReportingStore } from './lib';
|
||||
|
@ -35,7 +34,7 @@ export class ReportingPlugin
|
|||
|
||||
public setup(core: CoreSetup, plugins: ReportingSetupDeps) {
|
||||
const { http } = core;
|
||||
const { screenshotMode, features, licensing, security, spaces, taskManager } = plugins;
|
||||
const { features, licensing, security, spaces, taskManager } = plugins;
|
||||
|
||||
const reportingCore = new ReportingCore(this.logger, this.initContext);
|
||||
|
||||
|
@ -53,7 +52,6 @@ export class ReportingPlugin
|
|||
const router = http.createRouter<ReportingRequestHandlerContext>();
|
||||
const basePath = http.basePath;
|
||||
reportingCore.pluginSetup({
|
||||
screenshotMode,
|
||||
features,
|
||||
licensing,
|
||||
basePath,
|
||||
|
@ -98,11 +96,9 @@ export class ReportingPlugin
|
|||
(async () => {
|
||||
await reportingCore.pluginSetsUp();
|
||||
|
||||
const browserDriverFactory = await initializeBrowserDriverFactory(reportingCore, this.logger);
|
||||
const store = new ReportingStore(reportingCore, this.logger);
|
||||
|
||||
await reportingCore.pluginStart({
|
||||
browserDriverFactory,
|
||||
savedObjects: core.savedObjects,
|
||||
uiSettings: core.uiSettings,
|
||||
store,
|
||||
|
@ -110,6 +106,7 @@ export class ReportingPlugin
|
|||
data: plugins.data,
|
||||
taskManager: plugins.taskManager,
|
||||
logger: this.logger,
|
||||
screenshotting: plugins.screenshotting,
|
||||
});
|
||||
|
||||
// Note: this must be called after ReportingCore.pluginStart
|
||||
|
|
|
@ -6,11 +6,10 @@
|
|||
*/
|
||||
|
||||
import { UnwrapPromise } from '@kbn/utility-types';
|
||||
import { spawn } from 'child_process';
|
||||
import { createInterface } from 'readline';
|
||||
import { setupServer } from 'src/core/server/test_utils';
|
||||
import supertest from 'supertest';
|
||||
import * as Rx from 'rxjs';
|
||||
import type { ScreenshottingStart } from '../../../../screenshotting/server';
|
||||
import { ReportingCore } from '../..';
|
||||
import {
|
||||
createMockConfigSchema,
|
||||
|
@ -21,17 +20,11 @@ import {
|
|||
import type { ReportingRequestHandlerContext } from '../../types';
|
||||
import { registerDiagnoseBrowser } from './browser';
|
||||
|
||||
jest.mock('child_process');
|
||||
jest.mock('readline');
|
||||
|
||||
type SetupServerReturn = UnwrapPromise<ReturnType<typeof setupServer>>;
|
||||
|
||||
const devtoolMessage = 'DevTools listening on (ws://localhost:4000)';
|
||||
const fontNotFoundMessage = 'Could not find the default font';
|
||||
|
||||
const wait = (ms: number): Rx.Observable<0> =>
|
||||
Rx.from(new Promise<0>((resolve) => setTimeout(() => resolve(0), ms)));
|
||||
|
||||
describe('POST /diagnose/browser', () => {
|
||||
jest.setTimeout(6000);
|
||||
const reportingSymbol = Symbol('reporting');
|
||||
|
@ -40,12 +33,11 @@ describe('POST /diagnose/browser', () => {
|
|||
let server: SetupServerReturn['server'];
|
||||
let httpSetup: SetupServerReturn['httpSetup'];
|
||||
let core: ReportingCore;
|
||||
const mockedSpawn: any = spawn;
|
||||
const mockedCreateInterface: any = createInterface;
|
||||
let screenshotting: jest.Mocked<ScreenshottingStart>;
|
||||
|
||||
const config = createMockConfigSchema({
|
||||
queue: { timeout: 120000 },
|
||||
capture: { browser: { chromium: { proxy: { enabled: false } } } },
|
||||
capture: {},
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
|
@ -56,9 +48,6 @@ describe('POST /diagnose/browser', () => {
|
|||
() => ({ usesUiCapabilities: () => false })
|
||||
);
|
||||
|
||||
// Make all uses of 'Rx.timer' return an observable that completes in 50ms
|
||||
jest.spyOn(Rx, 'timer').mockImplementation(() => wait(50));
|
||||
|
||||
core = await createMockReportingCore(
|
||||
config,
|
||||
createMockPluginSetup({
|
||||
|
@ -67,21 +56,7 @@ describe('POST /diagnose/browser', () => {
|
|||
})
|
||||
);
|
||||
|
||||
mockedSpawn.mockImplementation(() => ({
|
||||
removeAllListeners: jest.fn(),
|
||||
kill: jest.fn(),
|
||||
pid: 123,
|
||||
stderr: 'stderr',
|
||||
addEventListener: jest.fn(),
|
||||
removeEventListener: jest.fn(),
|
||||
}));
|
||||
|
||||
mockedCreateInterface.mockImplementation(() => ({
|
||||
addEventListener: jest.fn(),
|
||||
removeEventListener: jest.fn(),
|
||||
removeAllListeners: jest.fn(),
|
||||
close: jest.fn(),
|
||||
}));
|
||||
screenshotting = (await core.getPluginStartDeps()).screenshotting as typeof screenshotting;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
|
@ -94,12 +69,7 @@ describe('POST /diagnose/browser', () => {
|
|||
|
||||
await server.start();
|
||||
|
||||
mockedCreateInterface.mockImplementation(() => ({
|
||||
addEventListener: (_e: string, cb: any) => setTimeout(() => cb(devtoolMessage), 0),
|
||||
removeEventListener: jest.fn(),
|
||||
removeAllListeners: jest.fn(),
|
||||
close: jest.fn(),
|
||||
}));
|
||||
screenshotting.diagnose.mockReturnValue(Rx.of(devtoolMessage));
|
||||
|
||||
return supertest(httpSetup.server.listener)
|
||||
.post('/api/reporting/diagnose/browser')
|
||||
|
@ -115,20 +85,7 @@ describe('POST /diagnose/browser', () => {
|
|||
registerDiagnoseBrowser(core, mockLogger);
|
||||
|
||||
await server.start();
|
||||
|
||||
mockedCreateInterface.mockImplementation(() => ({
|
||||
addEventListener: (_e: string, cb: any) => setTimeout(() => cb(logs), 0),
|
||||
removeEventListener: jest.fn(),
|
||||
removeAllListeners: jest.fn(),
|
||||
close: jest.fn(),
|
||||
}));
|
||||
|
||||
mockedSpawn.mockImplementation(() => ({
|
||||
removeAllListeners: jest.fn(),
|
||||
kill: jest.fn(),
|
||||
addEventListener: jest.fn(),
|
||||
removeEventListener: jest.fn(),
|
||||
}));
|
||||
screenshotting.diagnose.mockReturnValue(Rx.of(logs));
|
||||
|
||||
return supertest(httpSetup.server.listener)
|
||||
.post('/api/reporting/diagnose/browser')
|
||||
|
@ -139,8 +96,7 @@ describe('POST /diagnose/browser', () => {
|
|||
"help": Array [
|
||||
"The browser couldn't locate a default font. Please see https://www.elastic.co/guide/en/kibana/current/reporting-troubleshooting.html#reporting-troubleshooting-system-dependencies to fix this issue.",
|
||||
],
|
||||
"logs": "Could not find the default font
|
||||
",
|
||||
"logs": "Could not find the default font",
|
||||
"success": false,
|
||||
}
|
||||
`);
|
||||
|
@ -151,23 +107,7 @@ describe('POST /diagnose/browser', () => {
|
|||
registerDiagnoseBrowser(core, mockLogger);
|
||||
|
||||
await server.start();
|
||||
|
||||
mockedCreateInterface.mockImplementation(() => ({
|
||||
addEventListener: (_e: string, cb: any) => {
|
||||
setTimeout(() => cb(devtoolMessage), 0);
|
||||
setTimeout(() => cb(fontNotFoundMessage), 0);
|
||||
},
|
||||
removeEventListener: jest.fn(),
|
||||
removeAllListeners: jest.fn(),
|
||||
close: jest.fn(),
|
||||
}));
|
||||
|
||||
mockedSpawn.mockImplementation(() => ({
|
||||
removeAllListeners: jest.fn(),
|
||||
kill: jest.fn(),
|
||||
addEventListener: jest.fn(),
|
||||
removeEventListener: jest.fn(),
|
||||
}));
|
||||
screenshotting.diagnose.mockReturnValue(Rx.of(`${devtoolMessage}\n${fontNotFoundMessage}`));
|
||||
|
||||
return supertest(httpSetup.server.listener)
|
||||
.post('/api/reporting/diagnose/browser')
|
||||
|
@ -179,89 +119,10 @@ describe('POST /diagnose/browser', () => {
|
|||
"The browser couldn't locate a default font. Please see https://www.elastic.co/guide/en/kibana/current/reporting-troubleshooting.html#reporting-troubleshooting-system-dependencies to fix this issue.",
|
||||
],
|
||||
"logs": "DevTools listening on (ws://localhost:4000)
|
||||
Could not find the default font
|
||||
",
|
||||
Could not find the default font",
|
||||
"success": false,
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
it('logs a message when the browser starts, but then crashes', async () => {
|
||||
registerDiagnoseBrowser(core, mockLogger);
|
||||
|
||||
await server.start();
|
||||
|
||||
mockedCreateInterface.mockImplementation(() => ({
|
||||
addEventListener: (_e: string, cb: any) => {
|
||||
setTimeout(() => cb(fontNotFoundMessage), 0);
|
||||
},
|
||||
removeEventListener: jest.fn(),
|
||||
removeAllListeners: jest.fn(),
|
||||
close: jest.fn(),
|
||||
}));
|
||||
|
||||
mockedSpawn.mockImplementation(() => ({
|
||||
removeAllListeners: jest.fn(),
|
||||
kill: jest.fn(),
|
||||
addEventListener: (e: string, cb: any) => {
|
||||
if (e === 'exit') {
|
||||
setTimeout(() => cb(), 5);
|
||||
}
|
||||
},
|
||||
removeEventListener: jest.fn(),
|
||||
}));
|
||||
|
||||
return supertest(httpSetup.server.listener)
|
||||
.post('/api/reporting/diagnose/browser')
|
||||
.expect(200)
|
||||
.then(({ body }) => {
|
||||
const helpArray = [...body.help];
|
||||
helpArray.sort();
|
||||
expect(helpArray).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"The browser couldn't locate a default font. Please see https://www.elastic.co/guide/en/kibana/current/reporting-troubleshooting.html#reporting-troubleshooting-system-dependencies to fix this issue.",
|
||||
]
|
||||
`);
|
||||
expect(body.logs).toMatch(/Could not find the default font/);
|
||||
expect(body.logs).toMatch(/Browser exited abnormally during startup/);
|
||||
expect(body.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('cleans up process and subscribers', async () => {
|
||||
registerDiagnoseBrowser(core, mockLogger);
|
||||
|
||||
await server.start();
|
||||
const killMock = jest.fn();
|
||||
const spawnListenersMock = jest.fn();
|
||||
const createInterfaceListenersMock = jest.fn();
|
||||
const createInterfaceCloseMock = jest.fn();
|
||||
|
||||
mockedSpawn.mockImplementation(() => ({
|
||||
removeAllListeners: spawnListenersMock,
|
||||
kill: killMock,
|
||||
pid: 123,
|
||||
stderr: 'stderr',
|
||||
addEventListener: jest.fn(),
|
||||
removeEventListener: jest.fn(),
|
||||
}));
|
||||
|
||||
mockedCreateInterface.mockImplementation(() => ({
|
||||
addEventListener: (_e: string, cb: any) => setTimeout(() => cb(devtoolMessage), 0),
|
||||
removeEventListener: jest.fn(),
|
||||
removeAllListeners: createInterfaceListenersMock,
|
||||
close: createInterfaceCloseMock,
|
||||
}));
|
||||
|
||||
return supertest(httpSetup.server.listener)
|
||||
.post('/api/reporting/diagnose/browser')
|
||||
.expect(200)
|
||||
.then(() => {
|
||||
expect(killMock.mock.calls.length).toBe(1);
|
||||
expect(spawnListenersMock.mock.calls.length).toBe(1);
|
||||
expect(createInterfaceListenersMock.mock.calls.length).toBe(1);
|
||||
expect(createInterfaceCloseMock.mock.calls.length).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { ReportingCore } from '../..';
|
||||
import { API_DIAGNOSE_URL } from '../../../common/constants';
|
||||
import { browserStartLogs } from '../../browsers/chromium/driver_factory/start_logs';
|
||||
import { LevelLogger as Logger } from '../../lib';
|
||||
import { authorizedUserPreRouting } from '../lib/authorized_user_pre_routing';
|
||||
import { DiagnosticResponse } from './';
|
||||
|
@ -52,7 +51,8 @@ export const registerDiagnoseBrowser = (reporting: ReportingCore, logger: Logger
|
|||
},
|
||||
authorizedUserPreRouting(reporting, async (_user, _context, _req, res) => {
|
||||
try {
|
||||
const logs = await browserStartLogs(reporting, logger).toPromise();
|
||||
const { screenshotting } = await reporting.getPluginStartDeps();
|
||||
const logs = await screenshotting.diagnose().toPromise();
|
||||
const knownIssues = Object.keys(logsToHelpMap) as Array<keyof typeof logsToHelpMap>;
|
||||
|
||||
const boundSuccessfully = logs.includes(`DevTools listening on`);
|
||||
|
|
|
@ -20,7 +20,7 @@ import type { ReportingRequestHandlerContext } from '../../types';
|
|||
|
||||
jest.mock('../../export_types/common/generate_png');
|
||||
|
||||
import { generatePngObservableFactory } from '../../export_types/common';
|
||||
import { generatePngObservable } from '../../export_types/common';
|
||||
|
||||
type SetupServerReturn = UnwrapPromise<ReturnType<typeof setupServer>>;
|
||||
|
||||
|
@ -31,12 +31,12 @@ describe('POST /diagnose/screenshot', () => {
|
|||
let core: ReportingCore;
|
||||
|
||||
const setScreenshotResponse = (resp: object | Error) => {
|
||||
const generateMock = Promise.resolve(() => ({
|
||||
const generateMock = {
|
||||
pipe: () => ({
|
||||
toPromise: () => (resp instanceof Error ? Promise.reject(resp) : Promise.resolve(resp)),
|
||||
}),
|
||||
}));
|
||||
(generatePngObservableFactory as jest.Mock).mockResolvedValue(generateMock);
|
||||
};
|
||||
(generatePngObservable as jest.Mock).mockReturnValue(generateMock);
|
||||
};
|
||||
|
||||
const config = createMockConfigSchema({ queue: { timeout: 120000 } });
|
||||
|
|
|
@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n';
|
|||
import { ReportingCore } from '../..';
|
||||
import { APP_WRAPPER_CLASS } from '../../../../../../src/core/server';
|
||||
import { API_DIAGNOSE_URL } from '../../../common/constants';
|
||||
import { omitBlockedHeaders, generatePngObservableFactory } from '../../export_types/common';
|
||||
import { omitBlockedHeaders, generatePngObservable } from '../../export_types/common';
|
||||
import { getAbsoluteUrlFactory } from '../../export_types/common/get_absolute_url';
|
||||
import { LevelLogger as Logger } from '../../lib';
|
||||
import { authorizedUserPreRouting } from '../lib/authorized_user_pre_routing';
|
||||
|
@ -25,7 +25,6 @@ export const registerDiagnoseScreenshot = (reporting: ReportingCore, logger: Log
|
|||
validate: {},
|
||||
},
|
||||
authorizedUserPreRouting(reporting, async (_user, _context, req, res) => {
|
||||
const generatePngObservable = await generatePngObservableFactory(reporting);
|
||||
const config = reporting.getConfig();
|
||||
const decryptedHeaders = req.headers as Record<string, string>;
|
||||
const [basePath, protocol, hostname, port] = [
|
||||
|
@ -40,7 +39,6 @@ export const registerDiagnoseScreenshot = (reporting: ReportingCore, logger: Log
|
|||
|
||||
// Hack the layout to make the base/login page work
|
||||
const layout = {
|
||||
id: 'png',
|
||||
dimensions: {
|
||||
width: 1440,
|
||||
height: 2024,
|
||||
|
@ -53,7 +51,7 @@ export const registerDiagnoseScreenshot = (reporting: ReportingCore, logger: Log
|
|||
},
|
||||
};
|
||||
|
||||
const headers = {
|
||||
const conditionalHeaders = {
|
||||
headers: omitBlockedHeaders(decryptedHeaders),
|
||||
conditions: {
|
||||
hostname,
|
||||
|
@ -63,7 +61,12 @@ export const registerDiagnoseScreenshot = (reporting: ReportingCore, logger: Log
|
|||
},
|
||||
};
|
||||
|
||||
return generatePngObservable(logger, hashUrl, 'America/Los_Angeles', headers, layout)
|
||||
return generatePngObservable(reporting, logger, {
|
||||
conditionalHeaders,
|
||||
layout,
|
||||
browserTimezone: 'America/Los_Angeles',
|
||||
urls: [hashUrl],
|
||||
})
|
||||
.pipe()
|
||||
.toPromise()
|
||||
.then((screenshot) => {
|
||||
|
|
|
@ -103,7 +103,6 @@ describe('Handle request to generate', () => {
|
|||
"_primary_term": undefined,
|
||||
"_seq_no": undefined,
|
||||
"attempts": 0,
|
||||
"browser_type": undefined,
|
||||
"completed_at": undefined,
|
||||
"created_by": "testymcgee",
|
||||
"jobtype": "printable_pdf",
|
||||
|
@ -180,7 +179,6 @@ describe('Handle request to generate', () => {
|
|||
expect(snapObj).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"attempts": 0,
|
||||
"browser_type": undefined,
|
||||
"completed_at": undefined,
|
||||
"created_by": "testymcgee",
|
||||
"index": ".reporting-foo-index-234",
|
||||
|
|
|
@ -1,146 +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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import moment from 'moment';
|
||||
import { Page } from 'puppeteer';
|
||||
import * as Rx from 'rxjs';
|
||||
import { ReportingCore } from '..';
|
||||
import { chromium, HeadlessChromiumDriver, HeadlessChromiumDriverFactory } from '../browsers';
|
||||
import { LevelLogger } from '../lib';
|
||||
import { ElementsPositionAndAttribute } from '../lib/screenshots';
|
||||
import * as contexts from '../lib/screenshots/constants';
|
||||
import { CaptureConfig } from '../types';
|
||||
|
||||
interface CreateMockBrowserDriverFactoryOpts {
|
||||
evaluate: jest.Mock<Promise<any>, any[]>;
|
||||
waitForSelector: jest.Mock<Promise<any>, any[]>;
|
||||
waitFor: jest.Mock<Promise<any>, any[]>;
|
||||
screenshot: jest.Mock<Promise<any>, any[]>;
|
||||
open: jest.Mock<Promise<any>, any[]>;
|
||||
getCreatePage: (driver: HeadlessChromiumDriver) => jest.Mock<any, any>;
|
||||
}
|
||||
|
||||
const mockSelectors = {
|
||||
renderComplete: 'renderedSelector',
|
||||
itemsCountAttribute: 'itemsSelector',
|
||||
screenshot: 'screenshotSelector',
|
||||
timefilterDurationAttribute: 'timefilterDurationSelector',
|
||||
toastHeader: 'toastHeaderSelector',
|
||||
};
|
||||
|
||||
const getMockElementsPositionAndAttributes = (
|
||||
title: string,
|
||||
description: string
|
||||
): ElementsPositionAndAttribute[] => [
|
||||
{
|
||||
position: {
|
||||
boundingClientRect: { top: 0, left: 0, width: 800, height: 600 },
|
||||
scroll: { x: 0, y: 0 },
|
||||
},
|
||||
attributes: { title, description },
|
||||
},
|
||||
];
|
||||
|
||||
const mockWaitForSelector = jest.fn();
|
||||
mockWaitForSelector.mockImplementation((selectorArg: string) => {
|
||||
const { renderComplete, itemsCountAttribute, toastHeader } = mockSelectors;
|
||||
if (selectorArg === `${renderComplete},[${itemsCountAttribute}]`) {
|
||||
return Promise.resolve(true);
|
||||
} else if (selectorArg === toastHeader) {
|
||||
return Rx.never().toPromise();
|
||||
}
|
||||
throw new Error(selectorArg);
|
||||
});
|
||||
const mockBrowserEvaluate = jest.fn();
|
||||
mockBrowserEvaluate.mockImplementation(() => {
|
||||
const lastCallIndex = mockBrowserEvaluate.mock.calls.length - 1;
|
||||
const { context: mockCall } = mockBrowserEvaluate.mock.calls[lastCallIndex][1];
|
||||
|
||||
if (mockCall === contexts.CONTEXT_SKIPTELEMETRY) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
if (mockCall === contexts.CONTEXT_GETNUMBEROFITEMS) {
|
||||
return Promise.resolve(1);
|
||||
}
|
||||
if (mockCall === contexts.CONTEXT_INJECTCSS) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
if (mockCall === contexts.CONTEXT_WAITFORRENDER) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
if (mockCall === contexts.CONTEXT_GETTIMERANGE) {
|
||||
return Promise.resolve('Default GetTimeRange Result');
|
||||
}
|
||||
if (mockCall === contexts.CONTEXT_ELEMENTATTRIBUTES) {
|
||||
return Promise.resolve(getMockElementsPositionAndAttributes('Default Mock Title', 'Default '));
|
||||
}
|
||||
if (mockCall === contexts.CONTEXT_GETRENDERERRORS) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
throw new Error(mockCall);
|
||||
});
|
||||
const mockScreenshot = jest.fn(async () => Buffer.from('screenshot'));
|
||||
const getCreatePage = (driver: HeadlessChromiumDriver) =>
|
||||
jest.fn().mockImplementation(() => Rx.of({ driver, exit$: Rx.never() }));
|
||||
|
||||
const defaultOpts: CreateMockBrowserDriverFactoryOpts = {
|
||||
evaluate: mockBrowserEvaluate,
|
||||
waitForSelector: mockWaitForSelector,
|
||||
waitFor: jest.fn(),
|
||||
screenshot: mockScreenshot,
|
||||
open: jest.fn(),
|
||||
getCreatePage,
|
||||
};
|
||||
|
||||
export const createMockBrowserDriverFactory = async (
|
||||
core: ReportingCore,
|
||||
logger: LevelLogger,
|
||||
opts: Partial<CreateMockBrowserDriverFactoryOpts> = {}
|
||||
): Promise<HeadlessChromiumDriverFactory> => {
|
||||
const captureConfig: CaptureConfig = {
|
||||
timeouts: {
|
||||
openUrl: moment.duration(60, 's'),
|
||||
waitForElements: moment.duration(30, 's'),
|
||||
renderComplete: moment.duration(30, 's'),
|
||||
},
|
||||
browser: {
|
||||
type: 'chromium',
|
||||
chromium: {
|
||||
inspect: false,
|
||||
disableSandbox: false,
|
||||
proxy: { enabled: false, server: undefined, bypass: undefined },
|
||||
},
|
||||
autoDownload: false,
|
||||
},
|
||||
networkPolicy: { enabled: true, rules: [] },
|
||||
loadDelay: moment.duration(2, 's'),
|
||||
zoom: 2,
|
||||
maxAttempts: 1,
|
||||
};
|
||||
|
||||
const binaryPath = '/usr/local/share/common/secure/super_awesome_binary';
|
||||
const mockBrowserDriverFactory = chromium.createDriverFactory(core, binaryPath, logger);
|
||||
const mockPage = { setViewport: () => {} } as unknown as Page;
|
||||
const mockBrowserDriver = new HeadlessChromiumDriver(core, 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
|
||||
mockBrowserDriver.waitFor = opts.waitFor ? opts.waitFor : defaultOpts.waitFor;
|
||||
mockBrowserDriver.evaluate = opts.evaluate ? opts.evaluate : defaultOpts.evaluate;
|
||||
mockBrowserDriver.screenshot = opts.screenshot ? opts.screenshot : defaultOpts.screenshot;
|
||||
mockBrowserDriver.open = opts.open ? opts.open : defaultOpts.open;
|
||||
mockBrowserDriver.isPageOpen = () => true;
|
||||
|
||||
mockBrowserDriverFactory.createPage = opts.getCreatePage
|
||||
? opts.getCreatePage(mockBrowserDriver)
|
||||
: getCreatePage(mockBrowserDriver);
|
||||
|
||||
return mockBrowserDriverFactory;
|
||||
};
|
|
@ -7,7 +7,6 @@
|
|||
|
||||
jest.mock('../routes');
|
||||
jest.mock('../usage');
|
||||
jest.mock('../browsers');
|
||||
|
||||
import _ from 'lodash';
|
||||
import * as Rx from 'rxjs';
|
||||
|
@ -18,24 +17,15 @@ import { FieldFormatsRegistry } from 'src/plugins/field_formats/common';
|
|||
import { ReportingConfig, ReportingCore } from '../';
|
||||
import { featuresPluginMock } from '../../../features/server/mocks';
|
||||
import { securityMock } from '../../../security/server/mocks';
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { createMockScreenshottingStart } from '../../../screenshotting/server/mock';
|
||||
import { taskManagerMock } from '../../../task_manager/server/mocks';
|
||||
import {
|
||||
chromium,
|
||||
HeadlessChromiumDriverFactory,
|
||||
initializeBrowserDriverFactory,
|
||||
} from '../browsers';
|
||||
import { ReportingConfigType } from '../config';
|
||||
import { ReportingInternalSetup, ReportingInternalStart } from '../core';
|
||||
import { ReportingStore } from '../lib';
|
||||
import { setFieldFormats } from '../services';
|
||||
import { createMockLevelLogger } from './create_mock_levellogger';
|
||||
|
||||
(
|
||||
initializeBrowserDriverFactory as jest.Mock<Promise<HeadlessChromiumDriverFactory>>
|
||||
).mockImplementation(() => Promise.resolve({} as HeadlessChromiumDriverFactory));
|
||||
|
||||
(chromium as any).createDriverFactory.mockImplementation(() => ({}));
|
||||
|
||||
export const createMockPluginSetup = (setupMock?: any): ReportingInternalSetup => {
|
||||
return {
|
||||
features: featuresPluginMock.createSetup(),
|
||||
|
@ -63,7 +53,6 @@ export const createMockPluginStart = (
|
|||
: createMockReportingStore();
|
||||
|
||||
return {
|
||||
browserDriverFactory: startMock.browserDriverFactory,
|
||||
esClient: elasticsearchServiceMock.createClusterClient(),
|
||||
savedObjects: startMock.savedObjects || { getScopedClient: jest.fn() },
|
||||
uiSettings: startMock.uiSettings || { asScopedToClient: () => ({ get: jest.fn() }) },
|
||||
|
@ -74,6 +63,7 @@ export const createMockPluginStart = (
|
|||
ensureScheduled: jest.fn(),
|
||||
} as any,
|
||||
logger: createMockLevelLogger(),
|
||||
screenshotting: startMock.screenshotting || createMockScreenshottingStart(),
|
||||
...startMock,
|
||||
};
|
||||
};
|
||||
|
@ -102,14 +92,6 @@ export const createMockConfigSchema = (
|
|||
port: 80,
|
||||
...overrides.kibanaServer,
|
||||
},
|
||||
capture: {
|
||||
browser: {
|
||||
chromium: {
|
||||
disableSandbox: true,
|
||||
},
|
||||
},
|
||||
...overrides.capture,
|
||||
},
|
||||
queue: {
|
||||
indexInterval: 'week',
|
||||
pollEnabled: true,
|
||||
|
|
|
@ -5,8 +5,6 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export { createMockBrowserDriverFactory } from './create_mock_browserdriverfactory';
|
||||
export { createMockLayoutInstance } from './create_mock_layoutinstance';
|
||||
export { createMockLevelLogger } from './create_mock_levellogger';
|
||||
export {
|
||||
createMockConfig,
|
||||
|
|
|
@ -11,13 +11,17 @@ import { DataPluginStart } from 'src/plugins/data/server/plugin';
|
|||
import { ScreenshotModePluginSetup } from 'src/plugins/screenshot_mode/server';
|
||||
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
|
||||
import { Writable } from 'stream';
|
||||
import type {
|
||||
ScreenshottingStart,
|
||||
ScreenshotOptions as BaseScreenshotOptions,
|
||||
} from '../../screenshotting/server';
|
||||
import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server';
|
||||
import { LicensingPluginSetup } from '../../licensing/server';
|
||||
import { AuthenticatedUser, SecurityPluginSetup } from '../../security/server';
|
||||
import { SpacesPluginSetup } from '../../spaces/server';
|
||||
import { TaskManagerSetupContract, TaskManagerStartContract } from '../../task_manager/server';
|
||||
import { CancellationToken } from '../common';
|
||||
import { BaseParams, BasePayload, TaskRunResult } from '../common/types';
|
||||
import { BaseParams, BasePayload, TaskRunResult, UrlOrUrlLocatorTuple } from '../common/types';
|
||||
import { ReportingConfigType } from './config';
|
||||
import { ReportingCore } from './core';
|
||||
import { LevelLogger } from './lib';
|
||||
|
@ -39,6 +43,7 @@ export interface ReportingSetupDeps {
|
|||
|
||||
export interface ReportingStartDeps {
|
||||
data: DataPluginStart;
|
||||
screenshotting: ScreenshottingStart;
|
||||
taskManager: TaskManagerStartContract;
|
||||
}
|
||||
|
||||
|
@ -109,3 +114,10 @@ export interface ReportingRequestHandlerContext {
|
|||
* @internal
|
||||
*/
|
||||
export type ReportingPluginRouter = IRouter<ReportingRequestHandlerContext>;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export interface ScreenshotOptions extends Omit<BaseScreenshotOptions, 'timeouts' | 'urls'> {
|
||||
urls: UrlOrUrlLocatorTuple[];
|
||||
}
|
||||
|
|
|
@ -129,9 +129,6 @@ Object {
|
|||
"available": Object {
|
||||
"type": "boolean",
|
||||
},
|
||||
"browser_type": Object {
|
||||
"type": "keyword",
|
||||
},
|
||||
"csv": Object {
|
||||
"app": Object {
|
||||
"canvas workpad": Object {
|
||||
|
@ -1973,7 +1970,6 @@ Object {
|
|||
},
|
||||
"_all": 9,
|
||||
"available": true,
|
||||
"browser_type": undefined,
|
||||
"csv": Object {
|
||||
"app": Object {
|
||||
"canvas workpad": 0,
|
||||
|
@ -2243,7 +2239,6 @@ Object {
|
|||
},
|
||||
"_all": 0,
|
||||
"available": true,
|
||||
"browser_type": undefined,
|
||||
"csv": Object {
|
||||
"app": Object {
|
||||
"canvas workpad": 0,
|
||||
|
@ -2492,7 +2487,6 @@ Object {
|
|||
},
|
||||
"_all": 4,
|
||||
"available": true,
|
||||
"browser_type": undefined,
|
||||
"csv": Object {
|
||||
"app": Object {
|
||||
"canvas workpad": 0,
|
||||
|
@ -2768,7 +2762,6 @@ Object {
|
|||
},
|
||||
"_all": 11,
|
||||
"available": true,
|
||||
"browser_type": undefined,
|
||||
"csv": Object {
|
||||
"app": Object {
|
||||
"canvas workpad": 0,
|
||||
|
|
|
@ -206,10 +206,6 @@ export async function getReportingUsage(
|
|||
.search(params)
|
||||
.then(({ body: response }) => handleResponse(response))
|
||||
.then((usage: Partial<RangeStatSets>): ReportingUsageType => {
|
||||
// Allow this to explicitly throw an exception if/when this config is deprecated,
|
||||
// because we shouldn't collect browserType in that case!
|
||||
const browserType = config.get('capture', 'browser', 'type');
|
||||
|
||||
const exportTypesHandler = getExportTypesHandler(exportTypesRegistry);
|
||||
const availability = exportTypesHandler.getAvailability(
|
||||
featureAvailability
|
||||
|
@ -219,7 +215,6 @@ export async function getReportingUsage(
|
|||
|
||||
return {
|
||||
available: true,
|
||||
browser_type: browserType,
|
||||
enabled: true,
|
||||
last7Days: getExportStats(last7Days, availability, exportTypesHandler),
|
||||
...getExportStats(all, availability, exportTypesHandler),
|
||||
|
|
|
@ -92,7 +92,6 @@ const rangeStatsSchema: MakeSchemaFrom<RangeStats> = {
|
|||
export const reportingSchema: MakeSchemaFrom<ReportingUsageType> = {
|
||||
...rangeStatsSchema,
|
||||
available: { type: 'boolean' },
|
||||
browser_type: { type: 'keyword' },
|
||||
enabled: { type: 'boolean' },
|
||||
last7Days: rangeStatsSchema,
|
||||
};
|
||||
|
|
|
@ -129,7 +129,6 @@ export type RangeStats = JobTypes & {
|
|||
|
||||
export type ReportingUsageType = RangeStats & {
|
||||
available: boolean;
|
||||
browser_type: string;
|
||||
enabled: boolean;
|
||||
last7Days: RangeStats;
|
||||
};
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
{ "path": "../../../src/plugins/field_formats/tsconfig.json" },
|
||||
{ "path": "../features/tsconfig.json" },
|
||||
{ "path": "../licensing/tsconfig.json" },
|
||||
{ "path": "../screenshotting/tsconfig.json" },
|
||||
{ "path": "../security/tsconfig.json" },
|
||||
{ "path": "../spaces/tsconfig.json" },
|
||||
]
|
||||
|
|
11
x-pack/plugins/screenshotting/README.md
Normal file
11
x-pack/plugins/screenshotting/README.md
Normal file
|
@ -0,0 +1,11 @@
|
|||
# Kibana Screenshotting
|
||||
|
||||
This plugin provides functionality to take screenshots of the Kibana pages.
|
||||
It uses Chromium and Puppeteer underneath to run the browser in headless mode.
|
||||
|
||||
## API
|
||||
|
||||
The plugin exposes most of the functionality in the start contract.
|
||||
The Chromium download and setup is happening during the setup stage.
|
||||
|
||||
To learn more about the public API, please use automatically generated API reference or generated TypeDoc comments.
|
17
x-pack/plugins/screenshotting/common/context.ts
Normal file
17
x-pack/plugins/screenshotting/common/context.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Screenshot context.
|
||||
* This is a serializable object that can be passed from the screenshotting backend and then deserialized on the target page.
|
||||
*/
|
||||
export type Context = Record<string, unknown>;
|
||||
|
||||
/**
|
||||
* @interal
|
||||
*/
|
||||
export const SCREENSHOTTING_CONTEXT_KEY = '__SCREENSHOTTING_CONTEXT_KEY__';
|
|
@ -5,4 +5,6 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export { HeadlessChromiumDriver } from './chromium_driver';
|
||||
export type { Context } from './context';
|
||||
export type { LayoutParams } from './layout';
|
||||
export { LayoutTypes } from './layout';
|
75
x-pack/plugins/screenshotting/common/layout.ts
Normal file
75
x-pack/plugins/screenshotting/common/layout.ts
Normal file
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* 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 type { Ensure, SerializableRecord } from '@kbn/utility-types';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export type Size = Ensure<
|
||||
{
|
||||
/**
|
||||
* Layout width.
|
||||
*/
|
||||
width: number;
|
||||
|
||||
/**
|
||||
* Layout height.
|
||||
*/
|
||||
height: number;
|
||||
},
|
||||
SerializableRecord
|
||||
>;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export interface LayoutSelectorDictionary {
|
||||
screenshot: string;
|
||||
renderComplete: string;
|
||||
renderError: string;
|
||||
renderErrorAttribute: string;
|
||||
itemsCountAttribute: string;
|
||||
timefilterDurationAttribute: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Screenshot layout parameters.
|
||||
*/
|
||||
export type LayoutParams = Ensure<
|
||||
{
|
||||
/**
|
||||
* Unique layout name.
|
||||
*/
|
||||
id?: string;
|
||||
|
||||
/**
|
||||
* Layout sizing.
|
||||
*/
|
||||
dimensions?: Size;
|
||||
|
||||
/**
|
||||
* Element selectors determining the page state.
|
||||
*/
|
||||
selectors?: Partial<LayoutSelectorDictionary>;
|
||||
|
||||
/**
|
||||
* Page zoom.
|
||||
*/
|
||||
zoom?: number;
|
||||
},
|
||||
SerializableRecord
|
||||
>;
|
||||
|
||||
/**
|
||||
* Supported layout types.
|
||||
*/
|
||||
export const LayoutTypes = {
|
||||
PRESERVE_LAYOUT: 'preserve_layout',
|
||||
PRINT: 'print',
|
||||
CANVAS: 'canvas', // no margins or branding in the layout
|
||||
};
|
15
x-pack/plugins/screenshotting/jest.config.js
Normal file
15
x-pack/plugins/screenshotting/jest.config.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
preset: '@kbn/test',
|
||||
rootDir: '../../..',
|
||||
roots: ['<rootDir>/x-pack/plugins/screenshotting'],
|
||||
coverageDirectory: '<rootDir>/target/kibana-coverage/jest/x-pack/plugins/screenshotting',
|
||||
coverageReporters: ['text', 'html'],
|
||||
collectCoverageFrom: ['<rootDir>/x-pack/plugins/screenshotting/server/**/*.{ts}'],
|
||||
};
|
14
x-pack/plugins/screenshotting/kibana.json
Normal file
14
x-pack/plugins/screenshotting/kibana.json
Normal file
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"id": "screenshotting",
|
||||
"version": "8.0.0",
|
||||
"kibanaVersion": "kibana",
|
||||
"owner": {
|
||||
"name": "Kibana Reporting Services",
|
||||
"githubTeam": "kibana-reporting-services"
|
||||
},
|
||||
"description": "Kibana Screenshotting Plugin",
|
||||
"requiredPlugins": ["screenshotMode"],
|
||||
"configPath": ["xpack", "screenshotting"],
|
||||
"server": true,
|
||||
"ui": true
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue