Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
d597622
extract message from error objects
jloleysens Jan 26, 2022
c0e1bab
only warn for 400 and up status codes
jloleysens Jan 26, 2022
bdf381a
naively wait for vis ready after resizing the browser viewport
jloleysens Jan 26, 2022
6f3a9cd
Merge branch 'main' of github.com:elastic/kibana into screenshotting/…
jloleysens Jan 28, 2022
9522cd2
use a single default viewport size, enable layout to set default page…
jloleysens Jan 31, 2022
bf82e12
refactor viewport -> windowSize in chromium args
jloleysens Jan 31, 2022
79f3af6
allow overriding defaults and use new windowSize arg for chromium args
jloleysens Jan 31, 2022
6ab39e8
always round page dimension numbers. note: this will break if we ever…
jloleysens Jan 31, 2022
c8583e2
added comment
jloleysens Jan 31, 2022
4877026
Merge branch 'main' into screenshotting/fix-potential-race-condition-…
kibanamachine Jan 31, 2022
73eb3c5
Merge branch 'main' into screenshotting/fix-potential-race-condition-…
kibanamachine Feb 1, 2022
eb45d09
Merge branch 'main' into screenshotting/fix-potential-race-condition-…
kibanamachine Feb 1, 2022
78c8506
update snapshot to new width value
jloleysens Feb 2, 2022
10bebea
Merge branch 'main' into screenshotting/fix-potential-race-condition-…
kibanamachine Feb 2, 2022
58d50b6
Merge branch 'main' into screenshotting/fix-potential-race-condition-…
kibanamachine Feb 2, 2022
f5f8fbe
Merge branch 'main' into screenshotting/fix-potential-race-condition-…
kibanamachine Feb 3, 2022
4c293fa
Merge branch 'main' into screenshotting/fix-potential-race-condition-…
kibanamachine Feb 3, 2022
5b072ee
Merge branch 'main' into screenshotting/fix-potential-race-condition-…
kibanamachine Feb 7, 2022
a119d82
make defaultViewport a required field on createPage
jloleysens Feb 7, 2022
deb0e4e
added comment
jloleysens Feb 7, 2022
9f7eb66
Merge branch 'main' into screenshotting/fix-potential-race-condition-…
kibanamachine Feb 8, 2022
b77f4cf
style: use async-await rather than .then chaining. also added a comment
jloleysens Feb 8, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import type { ConfigType } from '../../../config';

interface Viewport {
interface WindowSize {
height: number;
width: number;
}
Expand All @@ -16,12 +16,17 @@ type Proxy = ConfigType['browser']['chromium']['proxy'];

interface LaunchArgs {
userDataDir: string;
viewport?: Viewport;
windowSize?: WindowSize;
disableSandbox?: boolean;
proxy: Proxy;
}

export const args = ({ userDataDir, disableSandbox, viewport, proxy: proxyConfig }: LaunchArgs) => {
export const args = ({
userDataDir,
disableSandbox,
windowSize,
proxy: proxyConfig,
}: LaunchArgs) => {
const flags = [
// Disable built-in Google Translate service
'--disable-translate',
Expand Down Expand Up @@ -50,11 +55,11 @@ export const args = ({ userDataDir, disableSandbox, viewport, proxy: proxyConfig
`--mainFrameClipsContent=false`,
];

if (viewport) {
if (windowSize) {
// NOTE: setting the window size does NOT set the viewport size: viewport and window size are different.
// The viewport may later need to be resized depending on the position of the clip area.
// These numbers come from the job parameters, so this is a close guess.
flags.push(`--window-size=${Math.floor(viewport.width)},${Math.floor(viewport.height)}`);
flags.push(`--window-size=${Math.floor(windowSize.width)},${Math.floor(windowSize.height)}`);
}

if (proxyConfig.enabled) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { mergeMap, take } from 'rxjs/operators';
import type { Logger } from 'src/core/server';
import type { ScreenshotModePluginSetup } from 'src/plugins/screenshot_mode/server';
import { ConfigType } from '../../../config';
import { HeadlessChromiumDriverFactory } from '.';
import { HeadlessChromiumDriverFactory, DEFAULT_VIEWPORT } from '.';

jest.mock('puppeteer');

Expand Down Expand Up @@ -70,7 +70,10 @@ describe('HeadlessChromiumDriverFactory', () => {
describe('createPage', () => {
it('returns browser driver, unexpected process exit observable, and close callback', async () => {
await expect(
factory.createPage({ openUrlTimeout: 0 }).pipe(take(1)).toPromise()
factory
.createPage({ openUrlTimeout: 0, defaultViewport: DEFAULT_VIEWPORT })
.pipe(take(1))
.toPromise()
).resolves.toEqual(
expect.objectContaining({
driver: expect.anything(),
Expand All @@ -85,7 +88,10 @@ describe('HeadlessChromiumDriverFactory', () => {
`Puppeteer Launch mock fail.`
);
expect(() =>
factory.createPage({ openUrlTimeout: 0 }).pipe(take(1)).toPromise()
factory
.createPage({ openUrlTimeout: 0, defaultViewport: DEFAULT_VIEWPORT })
.pipe(take(1))
.toPromise()
).rejects.toThrowErrorMatchingInlineSnapshot(
`"Error spawning Chromium browser! Puppeteer Launch mock fail."`
);
Expand All @@ -94,7 +100,7 @@ describe('HeadlessChromiumDriverFactory', () => {
describe('close behaviour', () => {
it('does not allow close to be called on the browse more than once', async () => {
await factory
.createPage({ openUrlTimeout: 0 })
.createPage({ openUrlTimeout: 0, defaultViewport: DEFAULT_VIEWPORT })
.pipe(
take(1),
mergeMap(async ({ close }) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import { getDataPath } from '@kbn/utils';
import { spawn } from 'child_process';
import _ from 'lodash';
import del from 'del';
import fs from 'fs';
import { uniq } from 'lodash';
Expand Down Expand Up @@ -36,6 +37,12 @@ import { getMetrics, PerformanceMetrics } from './metrics';

interface CreatePageOptions {
browserTimezone?: string;
defaultViewport: {
/** Size in pixels */
width?: number;
/** Size in pixels */
height?: number;
};
openUrlTimeout: number;
}

Expand Down Expand Up @@ -110,15 +117,15 @@ export class HeadlessChromiumDriverFactory {
userDataDir: this.userDataDir,
disableSandbox: this.config.browser.chromium.disableSandbox,
proxy: this.config.browser.chromium.proxy,
viewport: DEFAULT_VIEWPORT,
windowSize: DEFAULT_VIEWPORT, // Approximate the default viewport size
});
}

/*
* Return an observable to objects which will drive screenshot capture for a page
*/
createPage(
{ browserTimezone, openUrlTimeout }: CreatePageOptions,
{ browserTimezone, openUrlTimeout, defaultViewport }: CreatePageOptions,
pLogger = this.logger
): Rx.Observable<CreatePageResult> {
// FIXME: 'create' is deprecated
Expand All @@ -139,6 +146,13 @@ export class HeadlessChromiumDriverFactory {
ignoreHTTPSErrors: true,
handleSIGHUP: false,
args: chromiumArgs,

// We optionally set this at page creation to reduce the chances of
// browser reflow. In most cases only the height needs to be adjusted
// before taking a screenshot.
// NOTE: _.defaults assigns to the target object, so we copy it.
// NOTE NOTE: _.defaults is not the same as { ...DEFAULT_VIEWPORT, ...defaultViewport }
defaultViewport: _.defaults({ ...defaultViewport }, DEFAULT_VIEWPORT),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason why this can't be just:

Suggested change
defaultViewport: _.defaults({ ...defaultViewport }, DEFAULT_VIEWPORT),
defaultViewport: { ...DEFAULT_VIEWPORT, ...defaultViewport },

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason I chose _.defaults is because the override behaviour is different with own props (it ignores overrides that are undefined, so we won't end up with width: undefined for ex.).

Do you think we should change the type of defaultViewport making both width and height required values? I see they are both required in the Size object. WDYT?

Copy link
Copy Markdown
Contributor Author

@jloleysens jloleysens Feb 7, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tested this locally, and it looks like height can currently be undefined when passing it directly from layout.height to the createPage call resulting in this error:

[2022-02-07T10:36:53.499+01:00][INFO ][plugins.reporting.runTask.printablePdfV2.printable_pdf_v2.execute-job.kzci20um17zm9d0062eb528s] Compiling PDF using "preserve_layout" layout...
[2022-02-07T10:36:55.542+01:00][INFO ][plugins.reporting.runTask] Saved printable_pdf_v2 job /.reporting-2022-02-06/_doc/kzci20um17zm9d0062eb528s
[2022-02-07T10:36:59.710+01:00][INFO ][plugins.screenshotting.screenshot.browser-driver] Creating browser page driver
Unhandled Promise rejection detected:

Error: Protocol error (Emulation.setDeviceMetricsOverride): Invalid parameters Failed to deserialize params.height - BINDINGS: mandatory field missing at position 88
    at /Users/jeanlouisleysens/repos/work/kibana/node_modules/puppeteer/src/common/Connection.ts:291:57
    at new Promise (<anonymous>)
    at CDPSession.send (/Users/jeanlouisleysens/repos/work/kibana/node_modules/puppeteer/src/common/Connection.ts:290:12)
    at EmulationManager.emulateViewport (/Users/jeanlouisleysens/repos/work/kibana/node_modules/puppeteer/src/common/EmulationManager.ts:41:20)
    at Page.setViewport (/Users/jeanlouisleysens/repos/work/kibana/node_modules/puppeteer/src/common/Page.ts:2383:54)
    at Function.create (/Users/jeanlouisleysens/repos/work/kibana/node_modules/puppeteer/src/common/Page.ts:444:37)
    at runMicrotasks (<anonymous>)
    at processTicksAndRejections (node:internal/process/task_queues:96:5)
    at Browser._createPageInContext (/Users/jeanlouisleysens/repos/work/kibana/node_modules/puppeteer/src/common/Browser.ts:468:18)
    at Observable._subscribe (/Users/jeanlouisleysens/repos/work/kibana/x-pack/plugins/screenshotting/server/browsers/chromium/driver_factory/index.ts:125:20)

I think, for now, we can leave this default using the lodash algo. WDYT?

env: {
TZ: browserTimezone,
},
Expand Down
12 changes: 10 additions & 2 deletions x-pack/plugins/screenshotting/server/layouts/create_layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,28 @@
* 2.0.
*/

import { map as mapRecord } from 'fp-ts/lib/Record';
import type { LayoutParams } from '../../common/layout';
import { LayoutTypes } from '../../common';
import type { Layout } from '.';
import { CanvasLayout } from './canvas_layout';
import { PreserveLayout } from './preserve_layout';
import { PrintLayout } from './print_layout';

/**
* We naively round all numeric values in the object, this will break screenshotting
* if ever a have a non-number set as a value, but this points to an issue
* in the code responsible for creating the dimensions object.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would it be possible to have a unit test that explores this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, I think we could, but it may need to be a funcitonal test because the "break" occurs when we pass any non integer value to Chromium when launching (so floats or NaN would break it).

Is that the kind of test you had in mind?

*/
const roundNumbers = mapRecord(Math.round);

export function createLayout({ id, dimensions, selectors, ...config }: LayoutParams): Layout {
if (dimensions && id === LayoutTypes.PRESERVE_LAYOUT) {
return new PreserveLayout(dimensions, selectors);
return new PreserveLayout(roundNumbers(dimensions), selectors);
}

if (dimensions && id === LayoutTypes.CANVAS) {
return new CanvasLayout(dimensions);
return new CanvasLayout(roundNumbers(dimensions));
}

// layoutParams is optional as PrintLayout doesn't use it
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,7 @@ describe('Screenshot Observable Pipeline', () => {
"height": 1200,
"left": 0,
"top": 0,
"width": 1800,
"width": 1950,
},
"scroll": Object {
"x": 0,
Expand Down
71 changes: 40 additions & 31 deletions x-pack/plugins/screenshotting/server/screenshots/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,38 +68,47 @@ export function getScreenshots(
timeouts: { openUrl: openUrlTimeout },
} = options;

return browserDriverFactory.createPage({ browserTimezone, openUrlTimeout }, logger).pipe(
mergeMap(({ driver, unexpectedExit$, metrics$, close }) => {
apmCreatePage?.end();
metrics$.subscribe(({ cpu, memory }) => {
apmTrans?.setLabel('cpu', cpu, false);
apmTrans?.setLabel('memory', memory, false);
});
unexpectedExit$.subscribe({ error: () => apmTrans?.end() });
return browserDriverFactory
.createPage(
{
browserTimezone,
openUrlTimeout,
defaultViewport: { height: layout.height, width: layout.width },
},
logger
)
.pipe(
mergeMap(({ driver, unexpectedExit$, metrics$, close }) => {
apmCreatePage?.end();
metrics$.subscribe(({ cpu, memory }) => {
apmTrans?.setLabel('cpu', cpu, false);
apmTrans?.setLabel('memory', memory, false);
});
unexpectedExit$.subscribe({ error: () => apmTrans?.end() });

const screen = new ScreenshotObservableHandler(driver, logger, layout, options);
const screen = new ScreenshotObservableHandler(driver, logger, layout, options);

return from(options.urls).pipe(
concatMap((url, index) =>
screen.setupPage(index, url, apmTrans).pipe(
catchError((error) => {
screen.checkPageIsOpen(); // this fails the job if the browser has closed
return from(options.urls).pipe(
concatMap((url, index) =>
screen.setupPage(index, url, apmTrans).pipe(
catchError((error) => {
screen.checkPageIsOpen(); // this fails the job if the browser has closed

logger.error(error);
return of({ ...DEFAULT_SETUP_RESULT, error }); // allow failover screenshot capture
}),
takeUntil(unexpectedExit$),
screen.getScreenshots()
)
),
take(options.urls.length),
toArray(),
mergeMap((results) => {
// At this point we no longer need the page, close it.
return close().pipe(mapTo({ layout, metrics$, results }));
})
);
}),
first()
);
logger.error(error);
return of({ ...DEFAULT_SETUP_RESULT, error }); // allow failover screenshot capture
}),
takeUntil(unexpectedExit$),
screen.getScreenshots()
)
),
take(options.urls.length),
toArray(),
mergeMap((results) => {
// At this point we no longer need the page, close it.
return close().pipe(mapTo({ layout, metrics$, results }));
})
);
}),
first()
);
}
24 changes: 10 additions & 14 deletions x-pack/plugins/screenshotting/server/screenshots/observable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { catchError, mergeMap, switchMapTo, timeoutWith } from 'rxjs/operators';
import type { Logger } from 'src/core/server';
import type { Layout as ScreenshotModeLayout } from 'src/plugins/screenshot_mode/common';
import type { ConditionalHeaders, HeadlessChromiumDriver } from '../browsers';
import { getChromiumDisconnectedError } from '../browsers';
import { getChromiumDisconnectedError, DEFAULT_VIEWPORT } from '../browsers';
import type { Layout } from '../layouts';
import type { ElementsPositionAndAttribute } from './get_element_position_data';
import { getElementPositionAndAttributes } from './get_element_position_data';
Expand Down Expand Up @@ -107,12 +107,9 @@ interface PageSetupResults {
error?: Error;
}

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;
const height = dimensions?.height || DEFAULT_VIEWPORT.height;
const width = dimensions?.width || DEFAULT_VIEWPORT.width;

return [
{
Expand All @@ -130,8 +127,7 @@ const getDefaultElementPosition = (dimensions: { height?: number; width?: number
* provided by the browser.
*/
const getDefaultViewPort = () => ({
height: DEFAULT_SCREENSHOT_CLIP_HEIGHT,
width: DEFAULT_SCREENSHOT_CLIP_WIDTH,
...DEFAULT_VIEWPORT,
zoom: 1,
});

Expand Down Expand Up @@ -180,14 +176,14 @@ export class ScreenshotObservableHandler {
const waitTimeout = this.options.timeouts.waitForElements;

return defer(() => getNumberOfItems(driver, this.logger, waitTimeout, this.layout)).pipe(
mergeMap((itemsCount) => {
// set the viewport to the dimentions from the job, to allow elements to flow into the expected layout
mergeMap(async (itemsCount) => {
// set the viewport to the dimensions from the job, to allow elements to flow into the expected layout
const viewport = this.layout.getViewport(itemsCount) || getDefaultViewPort();

return forkJoin([
driver.setViewport(viewport, this.logger),
waitForVisualizations(driver, this.logger, waitTimeout, itemsCount, this.layout),
]);
// Set the viewport allowing time for the browser to handle reflow and redraw
// before checking for readiness of visualizations.
await driver.setViewport(viewport, this.logger);
await waitForVisualizations(driver, this.logger, waitTimeout, itemsCount, this.layout);
}),
this.waitUntil(waitTimeout, 'wait for elements')
);
Expand Down