chore: save chrome trace on the client side (#24414)

This commit is contained in:
Pavel Feldman 2023-07-26 14:11:26 -07:00 committed by GitHub
parent e036603aa3
commit 4949cef09c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 64 additions and 33 deletions

View File

@ -60,6 +60,20 @@ export class Artifact extends ChannelOwner<channels.ArtifactChannel> {
return stream.stream(); return stream.stream();
} }
async readIntoBuffer(): Promise<Buffer> {
const stream = (await this.createReadStream())!;
return new Promise((resolve, reject) => {
const chunks: Buffer[] = [];
stream.on('data', (chunk: Buffer) => {
chunks.push(chunk);
});
stream.on('end', () => {
resolve(Buffer.concat(chunks));
});
stream.on('error', reject);
});
}
async cancel(): Promise<void> { async cancel(): Promise<void> {
return this._channel.cancel(); return this._channel.cancel();
} }

View File

@ -14,6 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
import fs from 'fs';
import type * as channels from '@protocol/channels'; import type * as channels from '@protocol/channels';
import { BrowserContext, prepareBrowserContextParams } from './browserContext'; import { BrowserContext, prepareBrowserContextParams } from './browserContext';
import type { Page } from './page'; import type { Page } from './page';
@ -24,6 +25,8 @@ import { isSafeCloseError, kBrowserClosedError } from '../common/errors';
import type * as api from '../../types/types'; import type * as api from '../../types/types';
import { CDPSession } from './cdpSession'; import { CDPSession } from './cdpSession';
import type { BrowserType } from './browserType'; import type { BrowserType } from './browserType';
import { Artifact } from './artifact';
import { mkdirIfNeeded } from '../utils';
export class Browser extends ChannelOwner<channels.BrowserChannel> implements api.Browser { export class Browser extends ChannelOwner<channels.BrowserChannel> implements api.Browser {
readonly _contexts = new Set<BrowserContext>(); readonly _contexts = new Set<BrowserContext>();
@ -33,6 +36,7 @@ export class Browser extends ChannelOwner<channels.BrowserChannel> implements ap
_browserType!: BrowserType; _browserType!: BrowserType;
_options: LaunchOptions = {}; _options: LaunchOptions = {};
readonly _name: string; readonly _name: string;
private _path: string | undefined;
// Used from @playwright/test fixtures. // Used from @playwright/test fixtures.
_connectHeaders?: HeadersArray; _connectHeaders?: HeadersArray;
@ -104,11 +108,20 @@ export class Browser extends ChannelOwner<channels.BrowserChannel> implements ap
} }
async startTracing(page?: Page, options: { path?: string; screenshots?: boolean; categories?: string[]; } = {}) { async startTracing(page?: Page, options: { path?: string; screenshots?: boolean; categories?: string[]; } = {}) {
this._path = options.path;
await this._channel.startTracing({ ...options, page: page ? page._channel : undefined }); await this._channel.startTracing({ ...options, page: page ? page._channel : undefined });
} }
async stopTracing(): Promise<Buffer> { async stopTracing(): Promise<Buffer> {
return (await this._channel.stopTracing()).binary; const artifact = Artifact.from((await this._channel.stopTracing()).artifact);
const buffer = await artifact.readIntoBuffer();
await artifact.delete();
if (this._path) {
await mkdirIfNeeded(this._path);
await fs.promises.writeFile(this._path, buffer);
this._path = undefined;
}
return buffer;
} }
async close(): Promise<void> { async close(): Promise<void> {

View File

@ -723,14 +723,13 @@ scheme.BrowserNewBrowserCDPSessionResult = tObject({
}); });
scheme.BrowserStartTracingParams = tObject({ scheme.BrowserStartTracingParams = tObject({
page: tOptional(tChannel(['Page'])), page: tOptional(tChannel(['Page'])),
path: tOptional(tString),
screenshots: tOptional(tBoolean), screenshots: tOptional(tBoolean),
categories: tOptional(tArray(tString)), categories: tOptional(tArray(tString)),
}); });
scheme.BrowserStartTracingResult = tOptional(tObject({})); scheme.BrowserStartTracingResult = tOptional(tObject({}));
scheme.BrowserStopTracingParams = tOptional(tObject({})); scheme.BrowserStopTracingParams = tOptional(tObject({}));
scheme.BrowserStopTracingResult = tObject({ scheme.BrowserStopTracingResult = tObject({
binary: tBinary, artifact: tChannel(['Artifact']),
}); });
scheme.EventTargetInitializer = tOptional(tObject({})); scheme.EventTargetInitializer = tOptional(tObject({}));
scheme.EventTargetWaitForEventInfoParams = tObject({ scheme.EventTargetWaitForEventInfoParams = tObject({

View File

@ -16,9 +16,10 @@
*/ */
import type { BrowserOptions } from '../browser'; import type { BrowserOptions } from '../browser';
import path from 'path';
import { Browser } from '../browser'; import { Browser } from '../browser';
import { assertBrowserContextIsNotOwned, BrowserContext, verifyGeolocation } from '../browserContext'; import { assertBrowserContextIsNotOwned, BrowserContext, verifyGeolocation } from '../browserContext';
import { assert } from '../../utils'; import { assert, createGuid } from '../../utils';
import * as network from '../network'; import * as network from '../network';
import type { PageBinding, PageDelegate, Worker } from '../page'; import type { PageBinding, PageDelegate, Worker } from '../page';
import { Page } from '../page'; import { Page } from '../page';
@ -30,11 +31,12 @@ import type * as channels from '@protocol/channels';
import type { CRSession } from './crConnection'; import type { CRSession } from './crConnection';
import { ConnectionEvents, CRConnection } from './crConnection'; import { ConnectionEvents, CRConnection } from './crConnection';
import { CRPage } from './crPage'; import { CRPage } from './crPage';
import { readProtocolStream } from './crProtocolHelper'; import { saveProtocolStream } from './crProtocolHelper';
import type { Protocol } from './protocol'; import type { Protocol } from './protocol';
import type { CRDevTools } from './crDevTools'; import type { CRDevTools } from './crDevTools';
import { CRServiceWorker } from './crServiceWorker'; import { CRServiceWorker } from './crServiceWorker';
import type { SdkObject } from '../instrumentation'; import type { SdkObject } from '../instrumentation';
import { Artifact } from '../artifact';
export class CRBrowser extends Browser { export class CRBrowser extends Browser {
readonly _connection: CRConnection; readonly _connection: CRConnection;
@ -48,7 +50,6 @@ export class CRBrowser extends Browser {
private _version = ''; private _version = '';
private _tracingRecording = false; private _tracingRecording = false;
private _tracingPath: string | null = '';
private _tracingClient: CRSession | undefined; private _tracingClient: CRSession | undefined;
private _userAgent: string = ''; private _userAgent: string = '';
@ -276,7 +277,7 @@ export class CRBrowser extends Browser {
return await this._connection.createBrowserSession(); return await this._connection.createBrowserSession();
} }
async startTracing(page?: Page, options: { path?: string; screenshots?: boolean; categories?: string[]; } = {}) { async startTracing(page?: Page, options: { screenshots?: boolean; categories?: string[]; } = {}) {
assert(!this._tracingRecording, 'Cannot start recording trace while already recording trace.'); assert(!this._tracingRecording, 'Cannot start recording trace while already recording trace.');
this._tracingClient = page ? (page._delegate as CRPage)._mainFrameSession._client : this._session; this._tracingClient = page ? (page._delegate as CRPage)._mainFrameSession._client : this._session;
@ -287,7 +288,6 @@ export class CRBrowser extends Browser {
'disabled-by-default-v8.cpu_profiler', 'disabled-by-default-v8.cpu_profiler.hires' 'disabled-by-default-v8.cpu_profiler', 'disabled-by-default-v8.cpu_profiler.hires'
]; ];
const { const {
path = null,
screenshots = false, screenshots = false,
categories = defaultCategories, categories = defaultCategories,
} = options; } = options;
@ -295,7 +295,6 @@ export class CRBrowser extends Browser {
if (screenshots) if (screenshots)
categories.push('disabled-by-default-devtools.screenshot'); categories.push('disabled-by-default-devtools.screenshot');
this._tracingPath = path;
this._tracingRecording = true; this._tracingRecording = true;
await this._tracingClient.send('Tracing.start', { await this._tracingClient.send('Tracing.start', {
transferMode: 'ReturnAsStream', transferMode: 'ReturnAsStream',
@ -303,15 +302,18 @@ export class CRBrowser extends Browser {
}); });
} }
async stopTracing(): Promise<Buffer> { async stopTracing(): Promise<Artifact> {
assert(this._tracingClient, 'Tracing was not started.'); assert(this._tracingClient, 'Tracing was not started.');
const [event] = await Promise.all([ const [event] = await Promise.all([
new Promise(f => this._tracingClient!.once('Tracing.tracingComplete', f)), new Promise(f => this._tracingClient!.once('Tracing.tracingComplete', f)),
this._tracingClient.send('Tracing.end') this._tracingClient.send('Tracing.end')
]); ]);
const result = await readProtocolStream(this._tracingClient, (event as any).stream!, this._tracingPath); const tracingPath = path.join(this.options.artifactsDir, createGuid() + '.crtrace');
await saveProtocolStream(this._tracingClient, (event as any).stream!, tracingPath);
this._tracingRecording = false; this._tracingRecording = false;
return result; const artifact = new Artifact(this, tracingPath);
artifact.reportFinished();
return artifact;
} }
isConnected(): boolean { isConnected(): boolean {

View File

@ -114,6 +114,6 @@ export class CRPDF {
pageRanges, pageRanges,
preferCSSPageSize preferCSSPageSize
}); });
return await readProtocolStream(this._client, result.stream!, null); return await readProtocolStream(this._client, result.stream!);
} }
} }

View File

@ -40,26 +40,31 @@ export async function releaseObject(client: CRSession, objectId: string) {
await client.send('Runtime.releaseObject', { objectId }).catch(error => {}); await client.send('Runtime.releaseObject', { objectId }).catch(error => {});
} }
export async function readProtocolStream(client: CRSession, handle: string, path: string | null): Promise<Buffer> { export async function saveProtocolStream(client: CRSession, handle: string, path: string) {
let eof = false; let eof = false;
let fd: fs.promises.FileHandle | undefined;
if (path) {
await mkdirIfNeeded(path); await mkdirIfNeeded(path);
fd = await fs.promises.open(path, 'w'); const fd = await fs.promises.open(path, 'w');
}
const bufs = [];
while (!eof) { while (!eof) {
const response = await client.send('IO.read', { handle }); const response = await client.send('IO.read', { handle });
eof = response.eof; eof = response.eof;
const buf = Buffer.from(response.data, response.base64Encoded ? 'base64' : undefined); const buf = Buffer.from(response.data, response.base64Encoded ? 'base64' : undefined);
bufs.push(buf);
if (fd)
await fd.write(buf); await fd.write(buf);
} }
if (fd)
await fd.close(); await fd.close();
await client.send('IO.close', { handle }); await client.send('IO.close', { handle });
return Buffer.concat(bufs); }
export async function readProtocolStream(client: CRSession, handle: string): Promise<Buffer> {
let eof = false;
const chunks = [];
while (!eof) {
const response = await client.send('IO.read', { handle });
eof = response.eof;
const buf = Buffer.from(response.data, response.base64Encoded ? 'base64' : undefined);
chunks.push(buf);
}
await client.send('IO.close', { handle });
return Buffer.concat(chunks);
} }
export function toConsoleMessageLocation(stackTrace: Protocol.Runtime.StackTrace | undefined): types.ConsoleMessageLocation { export function toConsoleMessageLocation(stackTrace: Protocol.Runtime.StackTrace | undefined): types.ConsoleMessageLocation {

View File

@ -28,6 +28,7 @@ import { serverSideCallMetadata } from '../instrumentation';
import { BrowserContext } from '../browserContext'; import { BrowserContext } from '../browserContext';
import { Selectors } from '../selectors'; import { Selectors } from '../selectors';
import type { BrowserTypeDispatcher } from './browserTypeDispatcher'; import type { BrowserTypeDispatcher } from './browserTypeDispatcher';
import { ArtifactDispatcher } from './artifactDispatcher';
export class BrowserDispatcher extends Dispatcher<Browser, channels.BrowserChannel, BrowserTypeDispatcher> implements channels.BrowserChannel { export class BrowserDispatcher extends Dispatcher<Browser, channels.BrowserChannel, BrowserTypeDispatcher> implements channels.BrowserChannel {
_type_Browser = true; _type_Browser = true;
@ -81,7 +82,7 @@ export class BrowserDispatcher extends Dispatcher<Browser, channels.BrowserChann
if (!this._object.options.isChromium) if (!this._object.options.isChromium)
throw new Error(`Tracing is only available in Chromium`); throw new Error(`Tracing is only available in Chromium`);
const crBrowser = this._object as CRBrowser; const crBrowser = this._object as CRBrowser;
return { binary: await crBrowser.stopTracing() }; return { artifact: ArtifactDispatcher.from(this, await crBrowser.stopTracing()) };
} }
} }
@ -142,7 +143,7 @@ export class ConnectedBrowserDispatcher extends Dispatcher<Browser, channels.Bro
if (!this._object.options.isChromium) if (!this._object.options.isChromium)
throw new Error(`Tracing is only available in Chromium`); throw new Error(`Tracing is only available in Chromium`);
const crBrowser = this._object as CRBrowser; const crBrowser = this._object as CRBrowser;
return { binary: await crBrowser.stopTracing() }; return { artifact: ArtifactDispatcher.from(this, await crBrowser.stopTracing()) };
} }
async cleanupContexts() { async cleanupContexts() {

View File

@ -1348,13 +1348,11 @@ export type BrowserNewBrowserCDPSessionResult = {
}; };
export type BrowserStartTracingParams = { export type BrowserStartTracingParams = {
page?: PageChannel, page?: PageChannel,
path?: string,
screenshots?: boolean, screenshots?: boolean,
categories?: string[], categories?: string[],
}; };
export type BrowserStartTracingOptions = { export type BrowserStartTracingOptions = {
page?: PageChannel, page?: PageChannel,
path?: string,
screenshots?: boolean, screenshots?: boolean,
categories?: string[], categories?: string[],
}; };
@ -1362,7 +1360,7 @@ export type BrowserStartTracingResult = void;
export type BrowserStopTracingParams = {}; export type BrowserStopTracingParams = {};
export type BrowserStopTracingOptions = {}; export type BrowserStopTracingOptions = {};
export type BrowserStopTracingResult = { export type BrowserStopTracingResult = {
binary: Binary, artifact: ArtifactChannel,
}; };
export interface BrowserEvents { export interface BrowserEvents {

View File

@ -955,7 +955,6 @@ Browser:
startTracing: startTracing:
parameters: parameters:
page: Page? page: Page?
path: string?
screenshots: boolean? screenshots: boolean?
categories: categories:
type: array? type: array?
@ -963,7 +962,7 @@ Browser:
stopTracing: stopTracing:
returns: returns:
binary: binary artifact: Artifact
events: events: