chore: make stacks rendering live in ui mode (#21728)
Co-authored-by: Max Schmitt <max@schmitt.mx>
This commit is contained in:
parent
f37f38e553
commit
ecd0f927f4
|
|
@ -71,7 +71,7 @@ export class Connection extends EventEmitter {
|
||||||
private _localUtils?: LocalUtils;
|
private _localUtils?: LocalUtils;
|
||||||
// Some connections allow resolving in-process dispatchers.
|
// Some connections allow resolving in-process dispatchers.
|
||||||
toImpl: ((client: ChannelOwner) => any) | undefined;
|
toImpl: ((client: ChannelOwner) => any) | undefined;
|
||||||
private _stackCollectors = new Set<channels.ClientSideCallMetadata[]>();
|
private _tracingCount = 0;
|
||||||
|
|
||||||
constructor(localUtils?: LocalUtils) {
|
constructor(localUtils?: LocalUtils) {
|
||||||
super();
|
super();
|
||||||
|
|
@ -103,12 +103,11 @@ export class Connection extends EventEmitter {
|
||||||
return this._objects.get(guid)!;
|
return this._objects.get(guid)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
startCollectingCallMetadata(collector: channels.ClientSideCallMetadata[]) {
|
async setIsTracing(isTracing: boolean) {
|
||||||
this._stackCollectors.add(collector);
|
if (isTracing)
|
||||||
}
|
this._tracingCount++;
|
||||||
|
else
|
||||||
stopCollectingCallMetadata(collector: channels.ClientSideCallMetadata[]) {
|
this._tracingCount--;
|
||||||
this._stackCollectors.delete(collector);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendMessageToServer(object: ChannelOwner, type: string, method: string, params: any, stackTrace: ParsedStackTrace | null, wallTime: number | undefined): Promise<any> {
|
async sendMessageToServer(object: ChannelOwner, type: string, method: string, params: any, stackTrace: ParsedStackTrace | null, wallTime: number | undefined): Promise<any> {
|
||||||
|
|
@ -121,12 +120,11 @@ export class Connection extends EventEmitter {
|
||||||
const converted = { id, guid, method, params };
|
const converted = { id, guid, method, params };
|
||||||
// Do not include metadata in debug logs to avoid noise.
|
// Do not include metadata in debug logs to avoid noise.
|
||||||
debugLogger.log('channel:command', converted);
|
debugLogger.log('channel:command', converted);
|
||||||
for (const collector of this._stackCollectors)
|
|
||||||
collector.push({ stack: frames, id: id });
|
|
||||||
const location = frames[0] ? { file: frames[0].file, line: frames[0].line, column: frames[0].column } : undefined;
|
const location = frames[0] ? { file: frames[0].file, line: frames[0].line, column: frames[0].column } : undefined;
|
||||||
const metadata: channels.Metadata = { wallTime, apiName, location, internal: !apiName };
|
const metadata: channels.Metadata = { wallTime, apiName, location, internal: !apiName };
|
||||||
this.onmessage({ ...converted, metadata });
|
this.onmessage({ ...converted, metadata });
|
||||||
|
if (this._tracingCount && frames && type !== 'LocalUtils')
|
||||||
|
this._localUtils?._channel.addStackToTracingNoReply({ callData: { stack: frames, id } }).catch(() => {});
|
||||||
return await new Promise((resolve, reject) => this._callbacks.set(id, { resolve, reject, stackTrace, type, method }));
|
return await new Promise((resolve, reject) => this._callbacks.set(id, { resolve, reject, stackTrace, type, method }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,9 @@ import { ChannelOwner } from './channelOwner';
|
||||||
|
|
||||||
export class Tracing extends ChannelOwner<channels.TracingChannel> implements api.Tracing {
|
export class Tracing extends ChannelOwner<channels.TracingChannel> implements api.Tracing {
|
||||||
private _includeSources = false;
|
private _includeSources = false;
|
||||||
private _metadataCollector: channels.ClientSideCallMetadata[] = [];
|
|
||||||
_tracesDir: string | undefined;
|
_tracesDir: string | undefined;
|
||||||
|
private _stacksId: string | undefined;
|
||||||
|
private _isTracing = false;
|
||||||
|
|
||||||
static from(channel: channels.TracingChannel): Tracing {
|
static from(channel: channels.TracingChannel): Tracing {
|
||||||
return (channel as any)._object;
|
return (channel as any)._object;
|
||||||
|
|
@ -34,18 +35,26 @@ export class Tracing extends ChannelOwner<channels.TracingChannel> implements ap
|
||||||
|
|
||||||
async start(options: { name?: string, title?: string, snapshots?: boolean, screenshots?: boolean, sources?: boolean } = {}) {
|
async start(options: { name?: string, title?: string, snapshots?: boolean, screenshots?: boolean, sources?: boolean } = {}) {
|
||||||
this._includeSources = !!options.sources;
|
this._includeSources = !!options.sources;
|
||||||
await this._wrapApiCall(async () => {
|
const traceName = await this._wrapApiCall(async () => {
|
||||||
await this._channel.tracingStart(options);
|
await this._channel.tracingStart(options);
|
||||||
await this._channel.tracingStartChunk({ name: options.name, title: options.title });
|
const response = await this._channel.tracingStartChunk({ name: options.name, title: options.title });
|
||||||
|
return response.traceName;
|
||||||
});
|
});
|
||||||
this._metadataCollector = [];
|
await this._startCollectingStacks(traceName);
|
||||||
this._connection.startCollectingCallMetadata(this._metadataCollector);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async startChunk(options: { name?: string, title?: string } = {}) {
|
async startChunk(options: { name?: string, title?: string } = {}) {
|
||||||
await this._channel.tracingStartChunk(options);
|
const { traceName } = await this._channel.tracingStartChunk(options);
|
||||||
this._metadataCollector = [];
|
await this._startCollectingStacks(traceName);
|
||||||
this._connection.startCollectingCallMetadata(this._metadataCollector);
|
}
|
||||||
|
|
||||||
|
private async _startCollectingStacks(traceName: string) {
|
||||||
|
if (!this._isTracing) {
|
||||||
|
this._isTracing = true;
|
||||||
|
this._connection.setIsTracing(true);
|
||||||
|
}
|
||||||
|
const result = await this._connection.localUtils()._channel.tracingStarted({ tracesDir: this._tracesDir, traceName });
|
||||||
|
this._stacksId = result.stacksId;
|
||||||
}
|
}
|
||||||
|
|
||||||
async stopChunk(options: { path?: string } = {}) {
|
async stopChunk(options: { path?: string } = {}) {
|
||||||
|
|
@ -60,12 +69,16 @@ export class Tracing extends ChannelOwner<channels.TracingChannel> implements ap
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _doStopChunk(filePath: string | undefined) {
|
private async _doStopChunk(filePath: string | undefined) {
|
||||||
this._connection.stopCollectingCallMetadata(this._metadataCollector);
|
if (this._isTracing) {
|
||||||
const metadata = this._metadataCollector;
|
this._isTracing = false;
|
||||||
this._metadataCollector = [];
|
this._connection.setIsTracing(false);
|
||||||
|
}
|
||||||
|
|
||||||
if (!filePath) {
|
if (!filePath) {
|
||||||
await this._channel.tracingStopChunk({ mode: 'discard' });
|
|
||||||
// Not interested in artifacts.
|
// Not interested in artifacts.
|
||||||
|
await this._channel.tracingStopChunk({ mode: 'discard' });
|
||||||
|
if (this._stacksId)
|
||||||
|
await this._connection.localUtils()._channel.traceDiscarded({ stacksId: this._stacksId });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -73,23 +86,24 @@ export class Tracing extends ChannelOwner<channels.TracingChannel> implements ap
|
||||||
|
|
||||||
if (isLocal) {
|
if (isLocal) {
|
||||||
const result = await this._channel.tracingStopChunk({ mode: 'entries' });
|
const result = await this._channel.tracingStopChunk({ mode: 'entries' });
|
||||||
await this._connection.localUtils()._channel.zip({ zipFile: filePath, entries: result.entries!, metadata, mode: 'write', includeSources: this._includeSources });
|
await this._connection.localUtils()._channel.zip({ zipFile: filePath, entries: result.entries!, mode: 'write', stacksId: this._stacksId, includeSources: this._includeSources });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await this._channel.tracingStopChunk({ mode: 'archive' });
|
const result = await this._channel.tracingStopChunk({ mode: 'archive' });
|
||||||
|
|
||||||
// The artifact may be missing if the browser closed while stopping tracing.
|
// The artifact may be missing if the browser closed while stopping tracing.
|
||||||
if (!result.artifact)
|
if (!result.artifact) {
|
||||||
|
if (this._stacksId)
|
||||||
|
await this._connection.localUtils()._channel.traceDiscarded({ stacksId: this._stacksId });
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Save trace to the final local file.
|
// Save trace to the final local file.
|
||||||
const artifact = Artifact.from(result.artifact);
|
const artifact = Artifact.from(result.artifact);
|
||||||
await artifact.saveAs(filePath);
|
await artifact.saveAs(filePath);
|
||||||
await artifact.delete();
|
await artifact.delete();
|
||||||
|
|
||||||
// Add local sources to the remote trace if necessary.
|
await this._connection.localUtils()._channel.zip({ zipFile: filePath, entries: [], mode: 'append', stacksId: this._stacksId, includeSources: this._includeSources });
|
||||||
if (metadata.length)
|
|
||||||
await this._connection.localUtils()._channel.zip({ zipFile: filePath, entries: [], metadata, mode: 'append', includeSources: this._includeSources });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -220,8 +220,8 @@ scheme.LocalUtilsInitializer = tOptional(tObject({}));
|
||||||
scheme.LocalUtilsZipParams = tObject({
|
scheme.LocalUtilsZipParams = tObject({
|
||||||
zipFile: tString,
|
zipFile: tString,
|
||||||
entries: tArray(tType('NameValue')),
|
entries: tArray(tType('NameValue')),
|
||||||
|
stacksId: tOptional(tString),
|
||||||
mode: tEnum(['write', 'append']),
|
mode: tEnum(['write', 'append']),
|
||||||
metadata: tArray(tType('ClientSideCallMetadata')),
|
|
||||||
includeSources: tBoolean,
|
includeSources: tBoolean,
|
||||||
});
|
});
|
||||||
scheme.LocalUtilsZipResult = tOptional(tObject({}));
|
scheme.LocalUtilsZipResult = tOptional(tObject({}));
|
||||||
|
|
@ -268,6 +268,21 @@ scheme.LocalUtilsConnectParams = tObject({
|
||||||
scheme.LocalUtilsConnectResult = tObject({
|
scheme.LocalUtilsConnectResult = tObject({
|
||||||
pipe: tChannel(['JsonPipe']),
|
pipe: tChannel(['JsonPipe']),
|
||||||
});
|
});
|
||||||
|
scheme.LocalUtilsTracingStartedParams = tObject({
|
||||||
|
tracesDir: tOptional(tString),
|
||||||
|
traceName: tString,
|
||||||
|
});
|
||||||
|
scheme.LocalUtilsTracingStartedResult = tObject({
|
||||||
|
stacksId: tString,
|
||||||
|
});
|
||||||
|
scheme.LocalUtilsAddStackToTracingNoReplyParams = tObject({
|
||||||
|
callData: tType('ClientSideCallMetadata'),
|
||||||
|
});
|
||||||
|
scheme.LocalUtilsAddStackToTracingNoReplyResult = tOptional(tObject({}));
|
||||||
|
scheme.LocalUtilsTraceDiscardedParams = tObject({
|
||||||
|
stacksId: tString,
|
||||||
|
});
|
||||||
|
scheme.LocalUtilsTraceDiscardedResult = tOptional(tObject({}));
|
||||||
scheme.RootInitializer = tOptional(tObject({}));
|
scheme.RootInitializer = tOptional(tObject({}));
|
||||||
scheme.RootInitializeParams = tObject({
|
scheme.RootInitializeParams = tObject({
|
||||||
sdkLanguage: tEnum(['javascript', 'python', 'java', 'csharp']),
|
sdkLanguage: tEnum(['javascript', 'python', 'java', 'csharp']),
|
||||||
|
|
@ -2095,7 +2110,9 @@ scheme.TracingTracingStartChunkParams = tObject({
|
||||||
name: tOptional(tString),
|
name: tOptional(tString),
|
||||||
title: tOptional(tString),
|
title: tOptional(tString),
|
||||||
});
|
});
|
||||||
scheme.TracingTracingStartChunkResult = tOptional(tObject({}));
|
scheme.TracingTracingStartChunkResult = tObject({
|
||||||
|
traceName: tString,
|
||||||
|
});
|
||||||
scheme.TracingTracingStopChunkParams = tObject({
|
scheme.TracingTracingStopChunkParams = tObject({
|
||||||
mode: tEnum(['archive', 'discard', 'entries']),
|
mode: tEnum(['archive', 'discard', 'entries']),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,10 @@
|
||||||
import type EventEmitter from 'events';
|
import type EventEmitter from 'events';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import os from 'os';
|
||||||
import type * as channels from '@protocol/channels';
|
import type * as channels from '@protocol/channels';
|
||||||
import { ManualPromise } from '../../utils/manualPromise';
|
import { ManualPromise } from '../../utils/manualPromise';
|
||||||
import { assert, calculateSha1, createGuid } from '../../utils';
|
import { assert, calculateSha1, createGuid, removeFolders } from '../../utils';
|
||||||
import type { RootDispatcher } from './dispatcher';
|
import type { RootDispatcher } from './dispatcher';
|
||||||
import { Dispatcher } from './dispatcher';
|
import { Dispatcher } from './dispatcher';
|
||||||
import { yazl, yauzl } from '../../zipBundle';
|
import { yazl, yauzl } from '../../zipBundle';
|
||||||
|
|
@ -42,7 +43,13 @@ import { serializeClientSideCallMetadata } from '../../utils';
|
||||||
|
|
||||||
export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels.LocalUtilsChannel, RootDispatcher> implements channels.LocalUtilsChannel {
|
export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels.LocalUtilsChannel, RootDispatcher> implements channels.LocalUtilsChannel {
|
||||||
_type_LocalUtils: boolean;
|
_type_LocalUtils: boolean;
|
||||||
private _harBakends = new Map<string, HarBackend>();
|
private _harBackends = new Map<string, HarBackend>();
|
||||||
|
private _stackSessions = new Map<string, {
|
||||||
|
file: string,
|
||||||
|
writer: Promise<void>,
|
||||||
|
tmpDir: string | undefined,
|
||||||
|
callStacks: channels.ClientSideCallMetadata[]
|
||||||
|
}>();
|
||||||
|
|
||||||
constructor(scope: RootDispatcher, playwright: Playwright) {
|
constructor(scope: RootDispatcher, playwright: Playwright) {
|
||||||
const localUtils = new SdkObject(playwright, 'localUtils', 'localUtils');
|
const localUtils = new SdkObject(playwright, 'localUtils', 'localUtils');
|
||||||
|
|
@ -67,12 +74,21 @@ export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels.
|
||||||
addFile(entry.value, entry.name);
|
addFile(entry.value, entry.name);
|
||||||
|
|
||||||
// Add stacks and the sources.
|
// Add stacks and the sources.
|
||||||
zipFile.addBuffer(Buffer.from(JSON.stringify(serializeClientSideCallMetadata(params.metadata))), 'trace.stacks');
|
const stackSession = params.stacksId ? this._stackSessions.get(params.stacksId) : undefined;
|
||||||
|
if (stackSession?.callStacks.length) {
|
||||||
|
await stackSession.writer;
|
||||||
|
if (process.env.PW_LIVE_TRACE_STACKS) {
|
||||||
|
zipFile.addFile(stackSession.file, 'trace.stacks');
|
||||||
|
} else {
|
||||||
|
const buffer = Buffer.from(JSON.stringify(serializeClientSideCallMetadata(stackSession.callStacks)));
|
||||||
|
zipFile.addBuffer(buffer, 'trace.stacks');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Collect sources from stacks.
|
// Collect sources from stacks.
|
||||||
if (params.includeSources) {
|
if (params.includeSources) {
|
||||||
const sourceFiles = new Set<string>();
|
const sourceFiles = new Set<string>();
|
||||||
for (const { stack } of params.metadata) {
|
for (const { stack } of stackSession?.callStacks || []) {
|
||||||
if (!stack)
|
if (!stack)
|
||||||
continue;
|
continue;
|
||||||
for (const { file } of stack)
|
for (const { file } of stack)
|
||||||
|
|
@ -88,7 +104,9 @@ export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels.
|
||||||
zipFile.end(undefined, () => {
|
zipFile.end(undefined, () => {
|
||||||
zipFile.outputStream.pipe(fs.createWriteStream(params.zipFile)).on('close', () => promise.resolve());
|
zipFile.outputStream.pipe(fs.createWriteStream(params.zipFile)).on('close', () => promise.resolve());
|
||||||
});
|
});
|
||||||
return promise;
|
await promise;
|
||||||
|
await this._deleteStackSession(params.stacksId);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// File already exists. Repack and add new entries.
|
// File already exists. Repack and add new entries.
|
||||||
|
|
@ -121,7 +139,8 @@ export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels.
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return promise;
|
await promise;
|
||||||
|
await this._deleteStackSession(params.stacksId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async harOpen(params: channels.LocalUtilsHarOpenParams, metadata: CallMetadata): Promise<channels.LocalUtilsHarOpenResult> {
|
async harOpen(params: channels.LocalUtilsHarOpenParams, metadata: CallMetadata): Promise<channels.LocalUtilsHarOpenResult> {
|
||||||
|
|
@ -139,21 +158,21 @@ export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels.
|
||||||
const harFile = JSON.parse(await fs.promises.readFile(params.file, 'utf-8')) as har.HARFile;
|
const harFile = JSON.parse(await fs.promises.readFile(params.file, 'utf-8')) as har.HARFile;
|
||||||
harBackend = new HarBackend(harFile, path.dirname(params.file), null);
|
harBackend = new HarBackend(harFile, path.dirname(params.file), null);
|
||||||
}
|
}
|
||||||
this._harBakends.set(harBackend.id, harBackend);
|
this._harBackends.set(harBackend.id, harBackend);
|
||||||
return { harId: harBackend.id };
|
return { harId: harBackend.id };
|
||||||
}
|
}
|
||||||
|
|
||||||
async harLookup(params: channels.LocalUtilsHarLookupParams, metadata: CallMetadata): Promise<channels.LocalUtilsHarLookupResult> {
|
async harLookup(params: channels.LocalUtilsHarLookupParams, metadata: CallMetadata): Promise<channels.LocalUtilsHarLookupResult> {
|
||||||
const harBackend = this._harBakends.get(params.harId);
|
const harBackend = this._harBackends.get(params.harId);
|
||||||
if (!harBackend)
|
if (!harBackend)
|
||||||
return { action: 'error', message: `Internal error: har was not opened` };
|
return { action: 'error', message: `Internal error: har was not opened` };
|
||||||
return await harBackend.lookup(params.url, params.method, params.headers, params.postData, params.isNavigationRequest);
|
return await harBackend.lookup(params.url, params.method, params.headers, params.postData, params.isNavigationRequest);
|
||||||
}
|
}
|
||||||
|
|
||||||
async harClose(params: channels.LocalUtilsHarCloseParams, metadata: CallMetadata): Promise<void> {
|
async harClose(params: channels.LocalUtilsHarCloseParams, metadata: CallMetadata): Promise<void> {
|
||||||
const harBackend = this._harBakends.get(params.harId);
|
const harBackend = this._harBackends.get(params.harId);
|
||||||
if (harBackend) {
|
if (harBackend) {
|
||||||
this._harBakends.delete(harBackend.id);
|
this._harBackends.delete(harBackend.id);
|
||||||
harBackend.dispose();
|
harBackend.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -213,6 +232,40 @@ export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels.
|
||||||
}, params.timeout || 0);
|
}, params.timeout || 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async tracingStarted(params: channels.LocalUtilsTracingStartedParams, metadata?: CallMetadata | undefined): Promise<channels.LocalUtilsTracingStartedResult> {
|
||||||
|
let tmpDir = undefined;
|
||||||
|
if (!params.tracesDir)
|
||||||
|
tmpDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'playwright-tracing-'));
|
||||||
|
const traceStacksFile = path.join(params.tracesDir || tmpDir!, params.traceName + '.stacks');
|
||||||
|
this._stackSessions.set(traceStacksFile, { callStacks: [], file: traceStacksFile, writer: Promise.resolve(), tmpDir });
|
||||||
|
return { stacksId: traceStacksFile };
|
||||||
|
}
|
||||||
|
|
||||||
|
async traceDiscarded(params: channels.LocalUtilsTraceDiscardedParams, metadata?: CallMetadata | undefined): Promise<void> {
|
||||||
|
await this._deleteStackSession(params.stacksId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async addStackToTracingNoReply(params: channels.LocalUtilsAddStackToTracingNoReplyParams, metadata?: CallMetadata | undefined): Promise<void> {
|
||||||
|
for (const session of this._stackSessions.values()) {
|
||||||
|
session.callStacks.push(params.callData);
|
||||||
|
if (process.env.PW_LIVE_TRACE_STACKS) {
|
||||||
|
session.writer = session.writer.then(() => {
|
||||||
|
const buffer = Buffer.from(JSON.stringify(serializeClientSideCallMetadata(session.callStacks)));
|
||||||
|
return fs.promises.writeFile(session.file, buffer);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _deleteStackSession(stacksId?: string) {
|
||||||
|
const session = stacksId ? this._stackSessions.get(stacksId) : undefined;
|
||||||
|
if (!session)
|
||||||
|
return;
|
||||||
|
await session.writer;
|
||||||
|
if (session.tmpDir)
|
||||||
|
await removeFolders([session.tmpDir]);
|
||||||
|
this._stackSessions.delete(stacksId!);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const redirectStatus = [301, 302, 303, 307, 308];
|
const redirectStatus = [301, 302, 303, 307, 308];
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ export class TracingDispatcher extends Dispatcher<Tracing, channels.TracingChann
|
||||||
}
|
}
|
||||||
|
|
||||||
async tracingStartChunk(params: channels.TracingTracingStartChunkParams): Promise<channels.TracingTracingStartChunkResult> {
|
async tracingStartChunk(params: channels.TracingTracingStartChunkParams): Promise<channels.TracingTracingStartChunkResult> {
|
||||||
await this._object.startChunk(params);
|
return await this._object.startChunk(params);
|
||||||
}
|
}
|
||||||
|
|
||||||
async tracingStopChunk(params: channels.TracingTracingStopChunkParams): Promise<channels.TracingTracingStopChunkResult> {
|
async tracingStopChunk(params: channels.TracingTracingStopChunkParams): Promise<channels.TracingTracingStopChunkResult> {
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ type RecordingState = {
|
||||||
traceFile: string,
|
traceFile: string,
|
||||||
tracesDir: string,
|
tracesDir: string,
|
||||||
resourcesDir: string,
|
resourcesDir: string,
|
||||||
filesCount: number,
|
chunkOrdinal: number,
|
||||||
networkSha1s: Set<string>,
|
networkSha1s: Set<string>,
|
||||||
traceSha1s: Set<string>,
|
traceSha1s: Set<string>,
|
||||||
recording: boolean;
|
recording: boolean;
|
||||||
|
|
@ -132,7 +132,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
|
||||||
// and conflict.
|
// and conflict.
|
||||||
const traceName = options.name || createGuid();
|
const traceName = options.name || createGuid();
|
||||||
// Init the state synchronously.
|
// Init the state synchronously.
|
||||||
this._state = { options, traceName, traceFile: '', networkFile: '', tracesDir: '', resourcesDir: '', filesCount: 0, traceSha1s: new Set(), networkSha1s: new Set(), recording: false };
|
this._state = { options, traceName, traceFile: '', networkFile: '', tracesDir: '', resourcesDir: '', chunkOrdinal: 0, traceSha1s: new Set(), networkSha1s: new Set(), recording: false };
|
||||||
const state = this._state;
|
const state = this._state;
|
||||||
|
|
||||||
state.tracesDir = await this._createTracesDirIfNeeded();
|
state.tracesDir = await this._createTracesDirIfNeeded();
|
||||||
|
|
@ -144,7 +144,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
|
||||||
this._harTracer.start();
|
this._harTracer.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
async startChunk(options: { name?: string, title?: string } = {}) {
|
async startChunk(options: { name?: string, title?: string } = {}): Promise<{ traceName: string }> {
|
||||||
if (this._state && this._state.recording)
|
if (this._state && this._state.recording)
|
||||||
await this.stopChunk({ mode: 'discard' });
|
await this.stopChunk({ mode: 'discard' });
|
||||||
|
|
||||||
|
|
@ -154,13 +154,14 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
|
||||||
throw new Error('Cannot start a trace chunk while stopping');
|
throw new Error('Cannot start a trace chunk while stopping');
|
||||||
|
|
||||||
const state = this._state;
|
const state = this._state;
|
||||||
const suffix = state.filesCount ? `-${state.filesCount}` : ``;
|
const suffix = state.chunkOrdinal ? `-${state.chunkOrdinal}` : ``;
|
||||||
state.filesCount++;
|
state.chunkOrdinal++;
|
||||||
state.traceFile = path.join(state.tracesDir, `${state.traceName}${suffix}.trace`);
|
state.traceFile = path.join(state.tracesDir, `${state.traceName}${suffix}.trace`);
|
||||||
state.recording = true;
|
state.recording = true;
|
||||||
|
|
||||||
if (options.name && options.name !== this._state.traceName)
|
if (options.name && options.name !== this._state.traceName)
|
||||||
this._changeTraceName(this._state, options.name);
|
this._changeTraceName(this._state, options.name);
|
||||||
|
|
||||||
this._appendTraceOperation(async () => {
|
this._appendTraceOperation(async () => {
|
||||||
await mkdirIfNeeded(state.traceFile);
|
await mkdirIfNeeded(state.traceFile);
|
||||||
await fs.promises.appendFile(state.traceFile, JSON.stringify({ ...this._contextCreatedEvent, title: options.title, wallTime: Date.now() }) + '\n');
|
await fs.promises.appendFile(state.traceFile, JSON.stringify({ ...this._contextCreatedEvent, title: options.title, wallTime: Date.now() }) + '\n');
|
||||||
|
|
@ -171,6 +172,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
|
||||||
this._startScreencast();
|
this._startScreencast();
|
||||||
if (state.options.snapshots)
|
if (state.options.snapshots)
|
||||||
await this._snapshotter?.start();
|
await this._snapshotter?.start();
|
||||||
|
return { traceName: state.traceName };
|
||||||
}
|
}
|
||||||
|
|
||||||
private _startScreencast() {
|
private _startScreencast() {
|
||||||
|
|
@ -194,6 +196,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
|
||||||
private async _changeTraceName(state: RecordingState, name: string) {
|
private async _changeTraceName(state: RecordingState, name: string) {
|
||||||
await this._appendTraceOperation(async () => {
|
await this._appendTraceOperation(async () => {
|
||||||
const oldNetworkFile = state.networkFile;
|
const oldNetworkFile = state.networkFile;
|
||||||
|
state.traceName = name;
|
||||||
state.traceFile = path.join(state.tracesDir, name + '.trace');
|
state.traceFile = path.join(state.tracesDir, name + '.trace');
|
||||||
state.networkFile = path.join(state.tracesDir, name + '.network');
|
state.networkFile = path.join(state.tracesDir, name + '.network');
|
||||||
// Network file survives across chunks, so make a copy with the new name.
|
// Network file survives across chunks, so make a copy with the new name.
|
||||||
|
|
@ -258,7 +261,8 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
|
||||||
return {};
|
return {};
|
||||||
|
|
||||||
// Network file survives across chunks, make a snapshot before returning the resulting entries.
|
// Network file survives across chunks, make a snapshot before returning the resulting entries.
|
||||||
const networkFile = path.join(state.networkFile, '..', createGuid());
|
const suffix = state.chunkOrdinal ? `-${state.chunkOrdinal}` : ``;
|
||||||
|
const networkFile = path.join(state.tracesDir, state.traceName + `${suffix}.network`);
|
||||||
await fs.promises.copyFile(state.networkFile, networkFile);
|
await fs.promises.copyFile(state.networkFile, networkFile);
|
||||||
|
|
||||||
const entries: NameValue[] = [];
|
const entries: NameValue[] = [];
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import type { APIRequestContext, BrowserContext, BrowserContextOptions, LaunchOptions, Page, Tracing, Video } from 'playwright-core';
|
import type { APIRequestContext, BrowserContext, BrowserContextOptions, LaunchOptions, Page, Tracing, Video } from 'playwright-core';
|
||||||
import * as playwrightLibrary from 'playwright-core';
|
import * as playwrightLibrary from 'playwright-core';
|
||||||
import { createGuid, debugMode, removeFolders, addInternalStackPrefix, mergeTraceFiles, saveTraceFile } from 'playwright-core/lib/utils';
|
import { createGuid, debugMode, addInternalStackPrefix, mergeTraceFiles, saveTraceFile, removeFolders } from 'playwright-core/lib/utils';
|
||||||
import type { Fixtures, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, ScreenshotMode, TestInfo, TestType, TraceMode, VideoMode } from '../types/test';
|
import type { Fixtures, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, ScreenshotMode, TestInfo, TestType, TraceMode, VideoMode } from '../types/test';
|
||||||
import type { TestInfoImpl } from './worker/testInfo';
|
import type { TestInfoImpl } from './worker/testInfo';
|
||||||
import { rootTestType } from './common/testType';
|
import { rootTestType } from './common/testType';
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ class UIMode {
|
||||||
|
|
||||||
constructor(config: FullConfigInternal) {
|
constructor(config: FullConfigInternal) {
|
||||||
this._config = config;
|
this._config = config;
|
||||||
|
process.env.PW_LIVE_TRACE_STACKS = '1';
|
||||||
config._internal.configCLIOverrides.forbidOnly = false;
|
config._internal.configCLIOverrides.forbidOnly = false;
|
||||||
config._internal.configCLIOverrides.globalTimeout = 0;
|
config._internal.configCLIOverrides.globalTimeout = 0;
|
||||||
config._internal.configCLIOverrides.repeatEach = 0;
|
config._internal.configCLIOverrides.repeatEach = 0;
|
||||||
|
|
|
||||||
|
|
@ -402,16 +402,19 @@ export interface LocalUtilsChannel extends LocalUtilsEventTarget, Channel {
|
||||||
harClose(params: LocalUtilsHarCloseParams, metadata?: CallMetadata): Promise<LocalUtilsHarCloseResult>;
|
harClose(params: LocalUtilsHarCloseParams, metadata?: CallMetadata): Promise<LocalUtilsHarCloseResult>;
|
||||||
harUnzip(params: LocalUtilsHarUnzipParams, metadata?: CallMetadata): Promise<LocalUtilsHarUnzipResult>;
|
harUnzip(params: LocalUtilsHarUnzipParams, metadata?: CallMetadata): Promise<LocalUtilsHarUnzipResult>;
|
||||||
connect(params: LocalUtilsConnectParams, metadata?: CallMetadata): Promise<LocalUtilsConnectResult>;
|
connect(params: LocalUtilsConnectParams, metadata?: CallMetadata): Promise<LocalUtilsConnectResult>;
|
||||||
|
tracingStarted(params: LocalUtilsTracingStartedParams, metadata?: CallMetadata): Promise<LocalUtilsTracingStartedResult>;
|
||||||
|
addStackToTracingNoReply(params: LocalUtilsAddStackToTracingNoReplyParams, metadata?: CallMetadata): Promise<LocalUtilsAddStackToTracingNoReplyResult>;
|
||||||
|
traceDiscarded(params: LocalUtilsTraceDiscardedParams, metadata?: CallMetadata): Promise<LocalUtilsTraceDiscardedResult>;
|
||||||
}
|
}
|
||||||
export type LocalUtilsZipParams = {
|
export type LocalUtilsZipParams = {
|
||||||
zipFile: string,
|
zipFile: string,
|
||||||
entries: NameValue[],
|
entries: NameValue[],
|
||||||
|
stacksId?: string,
|
||||||
mode: 'write' | 'append',
|
mode: 'write' | 'append',
|
||||||
metadata: ClientSideCallMetadata[],
|
|
||||||
includeSources: boolean,
|
includeSources: boolean,
|
||||||
};
|
};
|
||||||
export type LocalUtilsZipOptions = {
|
export type LocalUtilsZipOptions = {
|
||||||
|
stacksId?: string,
|
||||||
};
|
};
|
||||||
export type LocalUtilsZipResult = void;
|
export type LocalUtilsZipResult = void;
|
||||||
export type LocalUtilsHarOpenParams = {
|
export type LocalUtilsHarOpenParams = {
|
||||||
|
|
@ -476,6 +479,30 @@ export type LocalUtilsConnectOptions = {
|
||||||
export type LocalUtilsConnectResult = {
|
export type LocalUtilsConnectResult = {
|
||||||
pipe: JsonPipeChannel,
|
pipe: JsonPipeChannel,
|
||||||
};
|
};
|
||||||
|
export type LocalUtilsTracingStartedParams = {
|
||||||
|
tracesDir?: string,
|
||||||
|
traceName: string,
|
||||||
|
};
|
||||||
|
export type LocalUtilsTracingStartedOptions = {
|
||||||
|
tracesDir?: string,
|
||||||
|
};
|
||||||
|
export type LocalUtilsTracingStartedResult = {
|
||||||
|
stacksId: string,
|
||||||
|
};
|
||||||
|
export type LocalUtilsAddStackToTracingNoReplyParams = {
|
||||||
|
callData: ClientSideCallMetadata,
|
||||||
|
};
|
||||||
|
export type LocalUtilsAddStackToTracingNoReplyOptions = {
|
||||||
|
|
||||||
|
};
|
||||||
|
export type LocalUtilsAddStackToTracingNoReplyResult = void;
|
||||||
|
export type LocalUtilsTraceDiscardedParams = {
|
||||||
|
stacksId: string,
|
||||||
|
};
|
||||||
|
export type LocalUtilsTraceDiscardedOptions = {
|
||||||
|
|
||||||
|
};
|
||||||
|
export type LocalUtilsTraceDiscardedResult = void;
|
||||||
|
|
||||||
export interface LocalUtilsEvents {
|
export interface LocalUtilsEvents {
|
||||||
}
|
}
|
||||||
|
|
@ -3756,7 +3783,9 @@ export type TracingTracingStartChunkOptions = {
|
||||||
name?: string,
|
name?: string,
|
||||||
title?: string,
|
title?: string,
|
||||||
};
|
};
|
||||||
export type TracingTracingStartChunkResult = void;
|
export type TracingTracingStartChunkResult = {
|
||||||
|
traceName: string,
|
||||||
|
};
|
||||||
export type TracingTracingStopChunkParams = {
|
export type TracingTracingStopChunkParams = {
|
||||||
mode: 'archive' | 'discard' | 'entries',
|
mode: 'archive' | 'discard' | 'entries',
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -500,14 +500,12 @@ LocalUtils:
|
||||||
entries:
|
entries:
|
||||||
type: array
|
type: array
|
||||||
items: NameValue
|
items: NameValue
|
||||||
|
stacksId: string?
|
||||||
mode:
|
mode:
|
||||||
type: enum
|
type: enum
|
||||||
literals:
|
literals:
|
||||||
- write
|
- write
|
||||||
- append
|
- append
|
||||||
metadata:
|
|
||||||
type: array
|
|
||||||
items: ClientSideCallMetadata
|
|
||||||
includeSources: boolean
|
includeSources: boolean
|
||||||
|
|
||||||
harOpen:
|
harOpen:
|
||||||
|
|
@ -563,6 +561,21 @@ LocalUtils:
|
||||||
returns:
|
returns:
|
||||||
pipe: JsonPipe
|
pipe: JsonPipe
|
||||||
|
|
||||||
|
tracingStarted:
|
||||||
|
parameters:
|
||||||
|
tracesDir: string?
|
||||||
|
traceName: string
|
||||||
|
returns:
|
||||||
|
stacksId: string
|
||||||
|
|
||||||
|
addStackToTracingNoReply:
|
||||||
|
parameters:
|
||||||
|
callData: ClientSideCallMetadata
|
||||||
|
|
||||||
|
traceDiscarded:
|
||||||
|
parameters:
|
||||||
|
stacksId: string
|
||||||
|
|
||||||
Root:
|
Root:
|
||||||
type: interface
|
type: interface
|
||||||
|
|
||||||
|
|
@ -2927,6 +2940,8 @@ Tracing:
|
||||||
parameters:
|
parameters:
|
||||||
name: string?
|
name: string?
|
||||||
title: string?
|
title: string?
|
||||||
|
returns:
|
||||||
|
traceName: string
|
||||||
|
|
||||||
tracingStopChunk:
|
tracingStopChunk:
|
||||||
parameters:
|
parameters:
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,8 @@ import { test, expect } from './playwright-test-fixtures';
|
||||||
import { parseTrace } from '../config/utils';
|
import { parseTrace } from '../config/utils';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
|
||||||
|
test.describe.configure({ mode: 'parallel' });
|
||||||
|
|
||||||
test('should stop tracing with trace: on-first-retry, when not retrying', async ({ runInlineTest }, testInfo) => {
|
test('should stop tracing with trace: on-first-retry, when not retrying', async ({ runInlineTest }, testInfo) => {
|
||||||
const result = await runInlineTest({
|
const result = await runInlineTest({
|
||||||
'playwright.config.ts': `
|
'playwright.config.ts': `
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue