mirror of
https://github.com/microsoft/playwright.git
synced 2024-10-27 05:46:28 +03:00
chore: do not send stacks as a part of the call metainfo (#21089)
This commit is contained in:
parent
434fa470e3
commit
55c95a4463
@ -71,6 +71,7 @@ export class Connection extends EventEmitter {
|
||||
private _localUtils?: LocalUtils;
|
||||
// Some connections allow resolving in-process dispatchers.
|
||||
toImpl: ((client: ChannelOwner) => any) | undefined;
|
||||
private _stackCollectors = new Set<channels.ClientSideCallMetadata[]>();
|
||||
|
||||
constructor(localUtils?: LocalUtils) {
|
||||
super();
|
||||
@ -102,6 +103,14 @@ export class Connection extends EventEmitter {
|
||||
return this._objects.get(guid)!;
|
||||
}
|
||||
|
||||
startCollectingCallMetadata(collector: channels.ClientSideCallMetadata[]) {
|
||||
this._stackCollectors.add(collector);
|
||||
}
|
||||
|
||||
stopCollectingCallMetadata(collector: channels.ClientSideCallMetadata[]) {
|
||||
this._stackCollectors.delete(collector);
|
||||
}
|
||||
|
||||
async sendMessageToServer(object: ChannelOwner, type: string, method: string, params: any, stackTrace: ParsedStackTrace | null): Promise<any> {
|
||||
if (this._closedErrorMessage)
|
||||
throw new Error(this._closedErrorMessage);
|
||||
@ -112,7 +121,10 @@ export class Connection extends EventEmitter {
|
||||
const converted = { id, guid, method, params };
|
||||
// Do not include metadata in debug logs to avoid noise.
|
||||
debugLogger.log('channel:command', converted);
|
||||
const metadata: channels.Metadata = { stack: frames, apiName, internal: !apiName };
|
||||
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 metadata: channels.Metadata = { apiName, location, internal: !apiName };
|
||||
this.onmessage({ ...converted, metadata });
|
||||
|
||||
return await new Promise((resolve, reject) => this._callbacks.set(id, { resolve, reject, stackTrace, type, method }));
|
||||
|
@ -20,6 +20,8 @@ import { Artifact } from './artifact';
|
||||
import { ChannelOwner } from './channelOwner';
|
||||
|
||||
export class Tracing extends ChannelOwner<channels.TracingChannel> implements api.Tracing {
|
||||
private _includeSources = false;
|
||||
private _metadataCollector: channels.ClientSideCallMetadata[] = [];
|
||||
static from(channel: channels.TracingChannel): Tracing {
|
||||
return (channel as any)._object;
|
||||
}
|
||||
@ -29,14 +31,19 @@ export class Tracing extends ChannelOwner<channels.TracingChannel> implements ap
|
||||
}
|
||||
|
||||
async start(options: { name?: string, title?: string, snapshots?: boolean, screenshots?: boolean, sources?: boolean } = {}) {
|
||||
this._includeSources = !!options.sources;
|
||||
await this._wrapApiCall(async () => {
|
||||
await this._channel.tracingStart(options);
|
||||
await this._channel.tracingStartChunk({ title: options.title });
|
||||
});
|
||||
this._metadataCollector = [];
|
||||
this._connection.startCollectingCallMetadata(this._metadataCollector);
|
||||
}
|
||||
|
||||
async startChunk(options: { title?: string } = {}) {
|
||||
await this._channel.tracingStartChunk(options);
|
||||
this._metadataCollector = [];
|
||||
this._connection.startCollectingCallMetadata(this._metadataCollector);
|
||||
}
|
||||
|
||||
async stopChunk(options: { path?: string } = {}) {
|
||||
@ -51,22 +58,25 @@ export class Tracing extends ChannelOwner<channels.TracingChannel> implements ap
|
||||
}
|
||||
|
||||
private async _doStopChunk(filePath: string | undefined) {
|
||||
const isLocal = !this._connection.isRemote();
|
||||
|
||||
let mode: channels.TracingTracingStopChunkParams['mode'] = 'doNotSave';
|
||||
if (filePath) {
|
||||
if (isLocal)
|
||||
mode = 'compressTraceAndSources';
|
||||
else
|
||||
mode = 'compressTrace';
|
||||
}
|
||||
|
||||
const result = await this._channel.tracingStopChunk({ mode });
|
||||
this._connection.stopCollectingCallMetadata(this._metadataCollector);
|
||||
const metadata = this._metadataCollector;
|
||||
this._metadataCollector = [];
|
||||
if (!filePath) {
|
||||
await this._channel.tracingStopChunk({ mode: 'discard' });
|
||||
// Not interested in artifacts.
|
||||
return;
|
||||
}
|
||||
|
||||
const isLocal = !this._connection.isRemote();
|
||||
|
||||
if (isLocal) {
|
||||
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 });
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await this._channel.tracingStopChunk({ mode: 'archive' });
|
||||
|
||||
// The artifact may be missing if the browser closed while stopping tracing.
|
||||
if (!result.artifact)
|
||||
return;
|
||||
@ -77,7 +87,7 @@ export class Tracing extends ChannelOwner<channels.TracingChannel> implements ap
|
||||
await artifact.delete();
|
||||
|
||||
// Add local sources to the remote trace if necessary.
|
||||
if (result.sourceEntries?.length)
|
||||
await this._connection.localUtils()._channel.zip({ zipFile: filePath, entries: result.sourceEntries });
|
||||
if (result.entries?.length)
|
||||
await this._connection.localUtils()._channel.zip({ zipFile: filePath, entries: result.entries!, metadata, mode: 'append', includeSources: this._includeSources });
|
||||
}
|
||||
}
|
||||
|
@ -27,10 +27,18 @@ scheme.StackFrame = tObject({
|
||||
function: tOptional(tString),
|
||||
});
|
||||
scheme.Metadata = tObject({
|
||||
stack: tOptional(tArray(tType('StackFrame'))),
|
||||
location: tOptional(tObject({
|
||||
file: tString,
|
||||
line: tOptional(tNumber),
|
||||
column: tOptional(tNumber),
|
||||
})),
|
||||
apiName: tOptional(tString),
|
||||
internal: tOptional(tBoolean),
|
||||
});
|
||||
scheme.ClientSideCallMetadata = tObject({
|
||||
id: tNumber,
|
||||
stack: tOptional(tArray(tType('StackFrame'))),
|
||||
});
|
||||
scheme.Point = tObject({
|
||||
x: tNumber,
|
||||
y: tNumber,
|
||||
@ -211,6 +219,9 @@ scheme.LocalUtilsInitializer = tOptional(tObject({}));
|
||||
scheme.LocalUtilsZipParams = tObject({
|
||||
zipFile: tString,
|
||||
entries: tArray(tType('NameValue')),
|
||||
mode: tEnum(['write', 'append']),
|
||||
metadata: tArray(tType('ClientSideCallMetadata')),
|
||||
includeSources: tBoolean,
|
||||
});
|
||||
scheme.LocalUtilsZipResult = tOptional(tObject({}));
|
||||
scheme.LocalUtilsHarOpenParams = tObject({
|
||||
@ -2084,11 +2095,11 @@ scheme.TracingTracingStartChunkParams = tObject({
|
||||
});
|
||||
scheme.TracingTracingStartChunkResult = tOptional(tObject({}));
|
||||
scheme.TracingTracingStopChunkParams = tObject({
|
||||
mode: tEnum(['doNotSave', 'compressTrace', 'compressTraceAndSources']),
|
||||
mode: tEnum(['archive', 'discard', 'entries']),
|
||||
});
|
||||
scheme.TracingTracingStopChunkResult = tObject({
|
||||
artifact: tOptional(tChannel(['Artifact'])),
|
||||
sourceEntries: tOptional(tArray(tType('NameValue'))),
|
||||
entries: tOptional(tArray(tType('NameValue'))),
|
||||
});
|
||||
scheme.TracingTracingStopParams = tOptional(tObject({}));
|
||||
scheme.TracingTracingStopResult = tOptional(tObject({}));
|
||||
|
@ -251,7 +251,7 @@ export class DispatcherConnection {
|
||||
const sdkObject = dispatcher._object instanceof SdkObject ? dispatcher._object : undefined;
|
||||
const callMetadata: CallMetadata = {
|
||||
id: `call@${id}`,
|
||||
stack: validMetadata.stack,
|
||||
location: validMetadata.location,
|
||||
apiName: validMetadata.apiName,
|
||||
internal: validMetadata.internal,
|
||||
objectId: sdkObject?.guid,
|
||||
|
@ -19,7 +19,7 @@ import fs from 'fs';
|
||||
import path from 'path';
|
||||
import type * as channels from '@protocol/channels';
|
||||
import { ManualPromise } from '../../utils/manualPromise';
|
||||
import { assert, createGuid } from '../../utils';
|
||||
import { assert, calculateSha1, createGuid } from '../../utils';
|
||||
import type { RootDispatcher } from './dispatcher';
|
||||
import { Dispatcher } from './dispatcher';
|
||||
import { yazl, yauzl } from '../../zipBundle';
|
||||
@ -38,6 +38,7 @@ import type { HTTPRequestParams } from '../../utils/network';
|
||||
import type http from 'http';
|
||||
import type { Playwright } from '../playwright';
|
||||
import { SdkObject } from '../../server/instrumentation';
|
||||
import { serializeClientSideCallMetadata } from '../../utils';
|
||||
|
||||
export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels.LocalUtilsChannel, RootDispatcher> implements channels.LocalUtilsChannel {
|
||||
_type_LocalUtils: boolean;
|
||||
@ -49,20 +50,39 @@ export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels.
|
||||
this._type_LocalUtils = true;
|
||||
}
|
||||
|
||||
async zip(params: channels.LocalUtilsZipParams, metadata: CallMetadata): Promise<void> {
|
||||
async zip(params: channels.LocalUtilsZipParams): Promise<void> {
|
||||
const promise = new ManualPromise<void>();
|
||||
const zipFile = new yazl.ZipFile();
|
||||
(zipFile as any as EventEmitter).on('error', error => promise.reject(error));
|
||||
|
||||
for (const entry of params.entries) {
|
||||
const addFile = (file: string, name: string) => {
|
||||
try {
|
||||
if (fs.statSync(entry.value).isFile())
|
||||
zipFile.addFile(entry.value, entry.name);
|
||||
if (fs.statSync(file).isFile())
|
||||
zipFile.addFile(file, name);
|
||||
} catch (e) {
|
||||
}
|
||||
};
|
||||
|
||||
for (const entry of params.entries)
|
||||
addFile(entry.value, entry.name);
|
||||
|
||||
// Add stacks and the sources.
|
||||
zipFile.addBuffer(Buffer.from(JSON.stringify(serializeClientSideCallMetadata(params.metadata))), 'trace.stacks');
|
||||
|
||||
// Collect sources from stacks.
|
||||
if (params.includeSources) {
|
||||
const sourceFiles = new Set<string>();
|
||||
for (const { stack } of params.metadata) {
|
||||
if (!stack)
|
||||
continue;
|
||||
for (const { file } of stack)
|
||||
sourceFiles.add(file);
|
||||
}
|
||||
for (const sourceFile of sourceFiles)
|
||||
addFile(sourceFile, 'resources/src@' + calculateSha1(sourceFile) + '.txt');
|
||||
}
|
||||
|
||||
if (!fs.existsSync(params.zipFile)) {
|
||||
if (params.mode === 'write') {
|
||||
// New file, just compress the entries.
|
||||
await fs.promises.mkdir(path.dirname(params.zipFile), { recursive: true });
|
||||
zipFile.end(undefined, () => {
|
||||
|
@ -42,8 +42,8 @@ export class TracingDispatcher extends Dispatcher<Tracing, channels.TracingChann
|
||||
}
|
||||
|
||||
async tracingStopChunk(params: channels.TracingTracingStopChunkParams): Promise<channels.TracingTracingStopChunkResult> {
|
||||
const { artifact, sourceEntries } = await this._object.stopChunk(params);
|
||||
return { artifact: artifact ? ArtifactDispatcher.from(this, artifact) : undefined, sourceEntries };
|
||||
const { artifact, entries } = await this._object.stopChunk(params);
|
||||
return { artifact: artifact ? ArtifactDispatcher.from(this, artifact) : undefined, entries };
|
||||
}
|
||||
|
||||
async tracingStop(params: channels.TracingTracingStopParams): Promise<channels.TracingTracingStopResult> {
|
||||
|
@ -275,9 +275,9 @@ export class Recorder implements InstrumentationListener {
|
||||
// Apply new decorations.
|
||||
let fileToSelect = undefined;
|
||||
for (const metadata of this._currentCallsMetadata.keys()) {
|
||||
if (!metadata.stack || !metadata.stack[0])
|
||||
if (!metadata.location)
|
||||
continue;
|
||||
const { file, line } = metadata.stack[0];
|
||||
const { file, line } = metadata.location;
|
||||
let source = this._userSources.get(file);
|
||||
if (!source) {
|
||||
source = { isRecorded: false, label: file, id: file, text: this._readSource(file), highlight: [], language: languageForFile(file) };
|
||||
|
@ -24,7 +24,7 @@ import { commandsWithTracingSnapshots } from '../../../protocol/debug';
|
||||
import { ManualPromise } from '../../../utils/manualPromise';
|
||||
import type { RegisteredListener } from '../../../utils/eventsHelper';
|
||||
import { eventsHelper } from '../../../utils/eventsHelper';
|
||||
import { assert, calculateSha1, createGuid, monotonicTime } from '../../../utils';
|
||||
import { assert, createGuid, monotonicTime } from '../../../utils';
|
||||
import { mkdirIfNeeded, removeFolders } from '../../../utils/fileUtils';
|
||||
import { Artifact } from '../../artifact';
|
||||
import { BrowserContext } from '../../browserContext';
|
||||
@ -49,7 +49,6 @@ export type TracerOptions = {
|
||||
name?: string;
|
||||
snapshots?: boolean;
|
||||
screenshots?: boolean;
|
||||
sources?: boolean;
|
||||
};
|
||||
|
||||
type RecordingState = {
|
||||
@ -62,7 +61,6 @@ type RecordingState = {
|
||||
filesCount: number,
|
||||
networkSha1s: Set<string>,
|
||||
traceSha1s: Set<string>,
|
||||
sources: Set<string>,
|
||||
recording: boolean;
|
||||
};
|
||||
|
||||
@ -133,7 +131,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
|
||||
// and conflict.
|
||||
const traceName = options.name || createGuid();
|
||||
// Init the state synchrounously.
|
||||
this._state = { options, traceName, traceFile: '', networkFile: '', tracesDir: '', resourcesDir: '', filesCount: 0, traceSha1s: new Set(), networkSha1s: new Set(), sources: new Set(), recording: false };
|
||||
this._state = { options, traceName, traceFile: '', networkFile: '', tracesDir: '', resourcesDir: '', filesCount: 0, traceSha1s: new Set(), networkSha1s: new Set(), recording: false };
|
||||
const state = this._state;
|
||||
|
||||
state.tracesDir = await this._createTracesDirIfNeeded();
|
||||
@ -147,7 +145,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
|
||||
|
||||
async startChunk(options: { title?: string } = {}) {
|
||||
if (this._state && this._state.recording)
|
||||
await this.stopChunk({ mode: 'doNotSave' });
|
||||
await this.stopChunk({ mode: 'discard' });
|
||||
|
||||
if (!this._state)
|
||||
throw new Error('Must start tracing before starting a new chunk');
|
||||
@ -220,16 +218,16 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
|
||||
await this._writeChain;
|
||||
}
|
||||
|
||||
async stopChunk(params: TracingTracingStopChunkParams): Promise<{ artifact: Artifact | null, sourceEntries: NameValue[] | undefined }> {
|
||||
async stopChunk(params: TracingTracingStopChunkParams): Promise<{ artifact?: Artifact, entries?: NameValue[], sourceEntries?: NameValue[] }> {
|
||||
if (this._isStopping)
|
||||
throw new Error(`Tracing is already stopping`);
|
||||
this._isStopping = true;
|
||||
|
||||
if (!this._state || !this._state.recording) {
|
||||
this._isStopping = false;
|
||||
if (params.mode !== 'doNotSave')
|
||||
if (params.mode !== 'discard')
|
||||
throw new Error(`Must start tracing before stopping`);
|
||||
return { artifact: null, sourceEntries: [] };
|
||||
return { sourceEntries: [] };
|
||||
}
|
||||
|
||||
const state = this._state!;
|
||||
@ -256,10 +254,10 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
|
||||
// Chain the export operation against write operations,
|
||||
// so that neither trace files nor sha1s change during the export.
|
||||
return await this._appendTraceOperation(async () => {
|
||||
if (params.mode === 'doNotSave')
|
||||
return { artifact: null, sourceEntries: undefined };
|
||||
if (params.mode === 'discard')
|
||||
return {};
|
||||
|
||||
// Har files a live, make a snapshot before returning the resulting entries.
|
||||
// Har files are live, make a snapshot before returning the resulting entries.
|
||||
const networkFile = path.join(state.networkFile, '..', createGuid());
|
||||
await fs.promises.copyFile(state.networkFile, networkFile);
|
||||
|
||||
@ -269,34 +267,21 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
|
||||
for (const sha1 of new Set([...state.traceSha1s, ...state.networkSha1s]))
|
||||
entries.push({ name: path.join('resources', sha1), value: path.join(state.resourcesDir, sha1) });
|
||||
|
||||
let sourceEntries: NameValue[] | undefined;
|
||||
if (state.sources.size) {
|
||||
sourceEntries = [];
|
||||
for (const value of state.sources) {
|
||||
const entry = { name: 'resources/src@' + calculateSha1(value) + '.txt', value };
|
||||
if (params.mode === 'compressTraceAndSources') {
|
||||
if (fs.existsSync(entry.value))
|
||||
entries.push(entry);
|
||||
} else {
|
||||
sourceEntries.push(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const artifact = await this._exportZip(entries, state).catch(() => null);
|
||||
return { artifact, sourceEntries };
|
||||
if (params.mode === 'entries')
|
||||
return { entries };
|
||||
const artifact = await this._exportZip(entries, state).catch(() => undefined);
|
||||
return { artifact, entries };
|
||||
}).finally(() => {
|
||||
// Only reset trace sha1s, network resources are preserved between chunks.
|
||||
state.traceSha1s = new Set();
|
||||
state.sources = new Set();
|
||||
this._isStopping = false;
|
||||
state.recording = false;
|
||||
}) || { artifact: null, sourceEntries: undefined };
|
||||
}) || { };
|
||||
}
|
||||
|
||||
private async _exportZip(entries: NameValue[], state: RecordingState): Promise<Artifact | null> {
|
||||
private _exportZip(entries: NameValue[], state: RecordingState): Promise<Artifact | undefined> {
|
||||
const zipFile = new yazl.ZipFile();
|
||||
const result = new ManualPromise<Artifact | null>();
|
||||
const result = new ManualPromise<Artifact | undefined>();
|
||||
(zipFile as any as EventEmitter).on('error', error => result.reject(error));
|
||||
for (const entry of entries)
|
||||
zipFile.addFile(entry.value, entry.name);
|
||||
@ -335,10 +320,6 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
|
||||
metadata.afterSnapshot = `after@${metadata.id}`;
|
||||
const beforeSnapshot = this._captureSnapshot('before', sdkObject, metadata);
|
||||
this._pendingCalls.set(metadata.id, { sdkObject, metadata, beforeSnapshot });
|
||||
if (this._state?.options.sources) {
|
||||
for (const frame of metadata.stack || [])
|
||||
this._state.sources.add(frame.file);
|
||||
}
|
||||
await beforeSnapshot;
|
||||
}
|
||||
|
||||
|
@ -35,5 +35,6 @@ export * from './stackTrace';
|
||||
export * from './task';
|
||||
export * from './time';
|
||||
export * from './timeoutRunner';
|
||||
export * from './traceUtils';
|
||||
export * from './userAgent';
|
||||
export * from './zipFile';
|
||||
|
41
packages/playwright-core/src/utils/traceUtils.ts
Normal file
41
packages/playwright-core/src/utils/traceUtils.ts
Normal file
@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { ClientSideCallMetadata, StackFrame } from '@protocol/channels';
|
||||
import type { SerializedClientSideCallMetadata } from '@trace/traceUtils';
|
||||
|
||||
export function serializeClientSideCallMetadata(metadatas: ClientSideCallMetadata[]): SerializedClientSideCallMetadata {
|
||||
const stackFrames = new Map<string, number>();
|
||||
const frames: StackFrame[] = [];
|
||||
const stacks: [number, number[]][] = [];
|
||||
for (const m of metadatas) {
|
||||
if (!m.stack || !m.stack.length)
|
||||
continue;
|
||||
const stack: number[] = [];
|
||||
for (const frame of m.stack) {
|
||||
const key = `${frame.file}:${frame.line || 0}:${frame.column || 0}`;
|
||||
let ordinal = stackFrames.get(key);
|
||||
if (typeof ordinal !== 'number') {
|
||||
ordinal = stackFrames.size;
|
||||
stackFrames.set(key, ordinal);
|
||||
frames.push(frame);
|
||||
}
|
||||
stack.push(ordinal);
|
||||
}
|
||||
stacks.push([m.id, stack]);
|
||||
}
|
||||
return { frames, stacks };
|
||||
}
|
@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Point, StackFrame, SerializedError } from './channels';
|
||||
import type { Point, SerializedError } from './channels';
|
||||
|
||||
export type CallMetadata = {
|
||||
id: string;
|
||||
@ -33,7 +33,7 @@ export type CallMetadata = {
|
||||
// Service-side is making a call to itself, this metadata does not go
|
||||
// through the dispatcher, so is always excluded from inspector / tracing.
|
||||
isServerSide?: boolean;
|
||||
stack?: StackFrame[];
|
||||
location?: { file: string, line?: number, column?: number };
|
||||
log: string[];
|
||||
afterSnapshot?: string;
|
||||
snapshots: { title: string, snapshotName: string }[];
|
||||
|
@ -143,11 +143,20 @@ export type StackFrame = {
|
||||
};
|
||||
|
||||
export type Metadata = {
|
||||
stack?: StackFrame[],
|
||||
location?: {
|
||||
file: string,
|
||||
line?: number,
|
||||
column?: number,
|
||||
},
|
||||
apiName?: string,
|
||||
internal?: boolean,
|
||||
};
|
||||
|
||||
export type ClientSideCallMetadata = {
|
||||
id: number,
|
||||
stack?: StackFrame[],
|
||||
};
|
||||
|
||||
export type Point = {
|
||||
x: number,
|
||||
y: number,
|
||||
@ -394,6 +403,9 @@ export interface LocalUtilsChannel extends LocalUtilsEventTarget, Channel {
|
||||
export type LocalUtilsZipParams = {
|
||||
zipFile: string,
|
||||
entries: NameValue[],
|
||||
mode: 'write' | 'append',
|
||||
metadata: ClientSideCallMetadata[],
|
||||
includeSources: boolean,
|
||||
};
|
||||
export type LocalUtilsZipOptions = {
|
||||
|
||||
@ -3741,14 +3753,14 @@ export type TracingTracingStartChunkOptions = {
|
||||
};
|
||||
export type TracingTracingStartChunkResult = void;
|
||||
export type TracingTracingStopChunkParams = {
|
||||
mode: 'doNotSave' | 'compressTrace' | 'compressTraceAndSources',
|
||||
mode: 'archive' | 'discard' | 'entries',
|
||||
};
|
||||
export type TracingTracingStopChunkOptions = {
|
||||
|
||||
};
|
||||
export type TracingTracingStopChunkResult = {
|
||||
artifact?: ArtifactChannel,
|
||||
sourceEntries?: NameValue[],
|
||||
entries?: NameValue[],
|
||||
};
|
||||
export type TracingTracingStopParams = {};
|
||||
export type TracingTracingStopOptions = {};
|
||||
|
@ -25,12 +25,22 @@ StackFrame:
|
||||
Metadata:
|
||||
type: object
|
||||
properties:
|
||||
stack:
|
||||
type: array?
|
||||
items: StackFrame
|
||||
location:
|
||||
type: object?
|
||||
properties:
|
||||
file: string
|
||||
line: number?
|
||||
column: number?
|
||||
apiName: string?
|
||||
internal: boolean?
|
||||
|
||||
ClientSideCallMetadata:
|
||||
type: object
|
||||
properties:
|
||||
id: number
|
||||
stack:
|
||||
type: array?
|
||||
items: StackFrame
|
||||
|
||||
Point:
|
||||
type: object
|
||||
@ -488,6 +498,15 @@ LocalUtils:
|
||||
entries:
|
||||
type: array
|
||||
items: NameValue
|
||||
mode:
|
||||
type: enum
|
||||
literals:
|
||||
- write
|
||||
- append
|
||||
metadata:
|
||||
type: array
|
||||
items: ClientSideCallMetadata
|
||||
includeSources: boolean
|
||||
|
||||
harOpen:
|
||||
parameters:
|
||||
@ -2911,13 +2930,15 @@ Tracing:
|
||||
mode:
|
||||
type: enum
|
||||
literals:
|
||||
- doNotSave
|
||||
- compressTrace
|
||||
- compressTraceAndSources
|
||||
- archive
|
||||
- discard
|
||||
- entries
|
||||
returns:
|
||||
# The artifact may be missing if the browser closes while tracing is beeing stopped.
|
||||
# The artifact may be missing if the browser closes while tracing is being stopped.
|
||||
# Or it can be missing if client-side compression is taking place.
|
||||
artifact: Artifact?
|
||||
sourceEntries:
|
||||
# For local mode, these are all entries.
|
||||
entries:
|
||||
type: array?
|
||||
items: NameValue
|
||||
|
||||
|
@ -16,6 +16,7 @@
|
||||
|
||||
import type { CallMetadata } from '@protocol/callMetadata';
|
||||
import type * as trace from '@trace/trace';
|
||||
import { parseClientSideCallMetadata } from '@trace/traceUtils';
|
||||
import type zip from '@zip.js/zip.js';
|
||||
// @ts-ignore
|
||||
import zipImport from '@zip.js/zip.js/dist/zip-no-worker-inflate.min.js';
|
||||
@ -52,11 +53,14 @@ export class TraceModel {
|
||||
{ useWebWorkers: false }) as zip.ZipReader;
|
||||
let traceEntry: zip.Entry | undefined;
|
||||
let networkEntry: zip.Entry | undefined;
|
||||
let stacksEntry: zip.Entry | undefined;
|
||||
for (const entry of await this._zipReader.getEntries({ onprogress: progress })) {
|
||||
if (entry.filename.endsWith('.trace'))
|
||||
traceEntry = entry;
|
||||
if (entry.filename.endsWith('.network'))
|
||||
networkEntry = entry;
|
||||
if (entry.filename.endsWith('.stacks'))
|
||||
stacksEntry = entry;
|
||||
if (entry.filename.includes('src@'))
|
||||
this.contextEntry.hasSource = true;
|
||||
this._entries.set(entry.filename, entry);
|
||||
@ -77,6 +81,15 @@ export class TraceModel {
|
||||
for (const line of (await networkWriter.getData()).split('\n'))
|
||||
this.appendEvent(line);
|
||||
}
|
||||
|
||||
if (stacksEntry) {
|
||||
const writer = new zipjs.TextWriter();
|
||||
await stacksEntry.getData!(writer);
|
||||
const metadataMap = parseClientSideCallMetadata(JSON.parse(await writer.getData()));
|
||||
for (const action of this.contextEntry.actions)
|
||||
action.metadata.stack = metadataMap.get(action.metadata.id);
|
||||
}
|
||||
|
||||
this._build();
|
||||
}
|
||||
|
||||
|
@ -31,6 +31,7 @@ export default defineConfig({
|
||||
alias: {
|
||||
'@isomorphic': path.resolve(__dirname, '../playwright-core/src/server/isomorphic'),
|
||||
'@protocol': path.resolve(__dirname, '../protocol/src'),
|
||||
'@trace': path.resolve(__dirname, '../trace/src'),
|
||||
'@web': path.resolve(__dirname, '../web/src'),
|
||||
},
|
||||
},
|
||||
|
@ -15,6 +15,7 @@
|
||||
*/
|
||||
|
||||
import type { CallMetadata } from '@protocol/callMetadata';
|
||||
import type { StackFrame } from '@protocol/channels';
|
||||
import type { Language } from '../../playwright-core/src/server/isomorphic/locatorGenerators';
|
||||
import type { FrameSnapshot, ResourceSnapshot } from './snapshot';
|
||||
|
||||
@ -53,7 +54,7 @@ export type ScreencastFrameTraceEvent = {
|
||||
|
||||
export type ActionTraceEvent = {
|
||||
type: 'action' | 'event',
|
||||
metadata: CallMetadata,
|
||||
metadata: CallMetadata & { stack?: StackFrame[] },
|
||||
};
|
||||
|
||||
export type ResourceSnapshotTraceEvent = {
|
||||
|
32
packages/trace/src/traceUtils.ts
Normal file
32
packages/trace/src/traceUtils.ts
Normal file
@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { StackFrame } from '@protocol/channels';
|
||||
|
||||
export type SerializedClientSideCallMetadata = {
|
||||
frames: StackFrame[];
|
||||
stacks: [number, number[]][];
|
||||
};
|
||||
|
||||
export function parseClientSideCallMetadata(data: SerializedClientSideCallMetadata): Map<string, StackFrame[]> {
|
||||
const result = new Map<string, StackFrame[]>();
|
||||
const { frames, stacks } = data;
|
||||
for (const s of stacks) {
|
||||
const [id, ff] = s;
|
||||
result.set(`call@${id}`, ff.map((f: number) => frames[f]));
|
||||
}
|
||||
return result;
|
||||
}
|
@ -16,6 +16,8 @@
|
||||
|
||||
import type { Frame, Page } from 'playwright-core';
|
||||
import { ZipFile } from '../../packages/playwright-core/lib/utils/zipFile';
|
||||
import type { StackFrame } from '@protocol/channels';
|
||||
import { parseClientSideCallMetadata } from '../../packages/trace/src/traceUtils';
|
||||
|
||||
export async function attachFrame(page: Page, frameId: string, url: string): Promise<Frame> {
|
||||
const handle = await page.evaluateHandle(async ({ frameId, url }) => {
|
||||
@ -91,7 +93,7 @@ export function suppressCertificateWarning() {
|
||||
};
|
||||
}
|
||||
|
||||
export async function parseTrace(file: string): Promise<{ events: any[], resources: Map<string, Buffer>, actions: string[] }> {
|
||||
export async function parseTrace(file: string): Promise<{ events: any[], resources: Map<string, Buffer>, actions: string[], stacks: Map<string, StackFrame[]> }> {
|
||||
const zipFS = new ZipFile(file);
|
||||
const resources = new Map<string, Buffer>();
|
||||
for (const entry of await zipFS.entries())
|
||||
@ -103,14 +105,18 @@ export async function parseTrace(file: string): Promise<{ events: any[], resourc
|
||||
if (line)
|
||||
events.push(JSON.parse(line));
|
||||
}
|
||||
|
||||
for (const line of resources.get('trace.network').toString().split('\n')) {
|
||||
if (line)
|
||||
events.push(JSON.parse(line));
|
||||
}
|
||||
|
||||
const stacks = parseClientSideCallMetadata(JSON.parse(resources.get('trace.stacks').toString()));
|
||||
return {
|
||||
events,
|
||||
resources,
|
||||
actions: eventsToActions(events)
|
||||
actions: eventsToActions(events),
|
||||
stacks,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -19,6 +19,7 @@ import { jpegjs } from 'playwright-core/lib/utilsBundle';
|
||||
import path from 'path';
|
||||
import { browserTest, contextTest as test, expect } from '../config/browserTest';
|
||||
import { parseTrace } from '../config/utils';
|
||||
import type { StackFrame } from '@protocol/channels';
|
||||
|
||||
test.skip(({ trace }) => trace === 'on');
|
||||
|
||||
@ -208,8 +209,8 @@ test('should not include trace resources from the provious chunks', async ({ con
|
||||
// 1 network resource should be preserved.
|
||||
expect(names.filter(n => n.endsWith('.html')).length).toBe(1);
|
||||
expect(names.filter(n => n.endsWith('.jpeg')).length).toBe(0);
|
||||
// 1 source file for the test.
|
||||
expect(names.filter(n => n.endsWith('.txt')).length).toBe(1);
|
||||
// 0 source file for the second test.
|
||||
expect(names.filter(n => n.endsWith('.txt')).length).toBe(0);
|
||||
}
|
||||
});
|
||||
|
||||
@ -474,7 +475,7 @@ test('should hide internal stack frames', async ({ context, page }, testInfo) =>
|
||||
const actions = trace.events.filter(e => e.type === 'action' && !e.metadata.apiName.startsWith('tracing.'));
|
||||
expect(actions).toHaveLength(4);
|
||||
for (const action of actions)
|
||||
expect(relativeStack(action)).toEqual(['tracing.spec.ts']);
|
||||
expect(relativeStack(action, trace.stacks)).toEqual(['tracing.spec.ts']);
|
||||
});
|
||||
|
||||
test('should hide internal stack frames in expect', async ({ context, page }, testInfo) => {
|
||||
@ -495,7 +496,7 @@ test('should hide internal stack frames in expect', async ({ context, page }, te
|
||||
const actions = trace.events.filter(e => e.type === 'action' && !e.metadata.apiName.startsWith('tracing.'));
|
||||
expect(actions).toHaveLength(5);
|
||||
for (const action of actions)
|
||||
expect(relativeStack(action)).toEqual(['tracing.spec.ts']);
|
||||
expect(relativeStack(action, trace.stacks)).toEqual(['tracing.spec.ts']);
|
||||
});
|
||||
|
||||
test('should record global request trace', async ({ request, context, server }, testInfo) => {
|
||||
@ -605,6 +606,7 @@ function expectBlue(pixels: Buffer, offset: number) {
|
||||
expect(a).toBe(255);
|
||||
}
|
||||
|
||||
function relativeStack(action: any): string[] {
|
||||
return action.metadata.stack.map(f => f.file.replace(__dirname + path.sep, ''));
|
||||
function relativeStack(action: any, stacks: Map<string, StackFrame[]>): string[] {
|
||||
const stack = stacks.get(action.metadata.id) || [];
|
||||
return stack.map(f => f.file.replace(__dirname + path.sep, ''));
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user