mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
chore: save chrome trace on the client side (#24414)
This commit is contained in:
parent
e036603aa3
commit
4949cef09c
@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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> {
|
||||||
|
|||||||
@ -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({
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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() {
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user