2021-10-12 13:42:50 -08:00
|
|
|
/**
|
|
|
|
* 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.
|
|
|
|
*/
|
|
|
|
|
2022-09-20 18:41:51 -07:00
|
|
|
import type * as trace from '@trace/trace';
|
2023-02-27 15:29:20 -08:00
|
|
|
import type * as traceV3 from './versions/traceV3';
|
2023-05-05 15:12:18 -07:00
|
|
|
import { parseClientSideCallMetadata } from '../../../packages/playwright-core/src/utils/isomorphic/traceUtils';
|
2022-04-06 13:57:14 -08:00
|
|
|
import type { ContextEntry, PageEntry } from './entries';
|
|
|
|
import { createEmptyContext } from './entries';
|
2023-03-22 09:32:21 -07:00
|
|
|
import { SnapshotStorage } from './snapshotStorage';
|
2021-10-12 13:42:50 -08:00
|
|
|
|
2023-05-05 15:12:18 -07:00
|
|
|
export interface TraceModelBackend {
|
|
|
|
entryNames(): Promise<string[]>;
|
|
|
|
hasEntry(entryName: string): Promise<boolean>;
|
|
|
|
readText(entryName: string): Promise<string | undefined>;
|
|
|
|
readBlob(entryName: string): Promise<Blob | undefined>;
|
|
|
|
isLive(): boolean;
|
|
|
|
traceURL(): string;
|
|
|
|
}
|
2021-10-12 13:42:50 -08:00
|
|
|
export class TraceModel {
|
2023-02-27 22:31:47 -08:00
|
|
|
contextEntries: ContextEntry[] = [];
|
2021-10-12 13:42:50 -08:00
|
|
|
pageEntries = new Map<string, PageEntry>();
|
2023-03-22 09:32:21 -07:00
|
|
|
private _snapshotStorage: SnapshotStorage | undefined;
|
2021-10-12 13:42:50 -08:00
|
|
|
private _version: number | undefined;
|
2023-03-15 11:17:03 -07:00
|
|
|
private _backend!: TraceModelBackend;
|
2021-10-12 13:42:50 -08:00
|
|
|
|
|
|
|
constructor() {
|
|
|
|
}
|
|
|
|
|
2023-05-05 15:12:18 -07:00
|
|
|
async load(backend: TraceModelBackend, unzipProgress: (done: number, total: number) => void) {
|
|
|
|
this._backend = backend;
|
2023-02-27 22:31:47 -08:00
|
|
|
|
|
|
|
const ordinals: string[] = [];
|
|
|
|
let hasSource = false;
|
2023-03-15 11:17:03 -07:00
|
|
|
for (const entryName of await this._backend.entryNames()) {
|
2023-04-20 08:19:00 -07:00
|
|
|
const match = entryName.match(/(.+)\.trace/);
|
2023-02-27 22:31:47 -08:00
|
|
|
if (match)
|
|
|
|
ordinals.push(match[1] || '');
|
2023-03-15 11:17:03 -07:00
|
|
|
if (entryName.includes('src@'))
|
2023-02-27 22:31:47 -08:00
|
|
|
hasSource = true;
|
2021-10-12 13:42:50 -08:00
|
|
|
}
|
2023-02-27 22:31:47 -08:00
|
|
|
if (!ordinals.length)
|
2022-09-26 20:57:05 +02:00
|
|
|
throw new Error('Cannot find .trace file');
|
|
|
|
|
2023-03-22 09:32:21 -07:00
|
|
|
this._snapshotStorage = new SnapshotStorage();
|
2021-10-12 13:42:50 -08:00
|
|
|
|
2023-04-05 13:03:25 -07:00
|
|
|
// 3 * ordinals progress increments below.
|
|
|
|
const total = ordinals.length * 3;
|
|
|
|
let done = 0;
|
2023-02-27 22:31:47 -08:00
|
|
|
for (const ordinal of ordinals) {
|
|
|
|
const contextEntry = createEmptyContext();
|
2023-03-15 22:33:40 -07:00
|
|
|
const actionMap = new Map<string, trace.ActionTraceEvent>();
|
2023-05-05 15:12:18 -07:00
|
|
|
contextEntry.traceUrl = backend.traceURL();
|
2023-02-27 22:31:47 -08:00
|
|
|
contextEntry.hasSource = hasSource;
|
|
|
|
|
2023-04-20 08:19:00 -07:00
|
|
|
const trace = await this._backend.readText(ordinal + '.trace') || '';
|
2023-03-15 11:17:03 -07:00
|
|
|
for (const line of trace.split('\n'))
|
2023-03-15 22:33:40 -07:00
|
|
|
this.appendEvent(contextEntry, actionMap, line);
|
2023-04-05 13:03:25 -07:00
|
|
|
unzipProgress(++done, total);
|
2021-10-12 13:42:50 -08:00
|
|
|
|
2023-04-20 08:19:00 -07:00
|
|
|
const network = await this._backend.readText(ordinal + '.network') || '';
|
2023-03-15 11:17:03 -07:00
|
|
|
for (const line of network.split('\n'))
|
2023-03-15 22:33:40 -07:00
|
|
|
this.appendEvent(contextEntry, actionMap, line);
|
2023-04-05 13:03:25 -07:00
|
|
|
unzipProgress(++done, total);
|
2023-03-15 22:33:40 -07:00
|
|
|
|
|
|
|
contextEntry.actions = [...actionMap.values()].sort((a1, a2) => a1.startTime - a2.startTime);
|
2023-04-20 08:19:00 -07:00
|
|
|
const stacks = await this._backend.readText(ordinal + '.stacks');
|
2023-03-15 11:17:03 -07:00
|
|
|
if (stacks) {
|
|
|
|
const callMetadata = parseClientSideCallMetadata(JSON.parse(stacks));
|
2023-02-27 22:31:47 -08:00
|
|
|
for (const action of contextEntry.actions)
|
2023-03-15 11:17:03 -07:00
|
|
|
action.stack = action.stack || callMetadata.get(action.callId);
|
2023-02-27 22:31:47 -08:00
|
|
|
}
|
2023-04-05 13:03:25 -07:00
|
|
|
unzipProgress(++done, total);
|
2023-02-22 21:08:47 -08:00
|
|
|
|
2023-02-27 22:31:47 -08:00
|
|
|
this.contextEntries.push(contextEntry);
|
2023-02-22 21:08:47 -08:00
|
|
|
}
|
2021-10-12 13:42:50 -08:00
|
|
|
}
|
|
|
|
|
2022-09-26 20:57:05 +02:00
|
|
|
async hasEntry(filename: string): Promise<boolean> {
|
2023-03-15 11:17:03 -07:00
|
|
|
return this._backend.hasEntry(filename);
|
2022-09-26 20:57:05 +02:00
|
|
|
}
|
|
|
|
|
2021-10-12 13:42:50 -08:00
|
|
|
async resourceForSha1(sha1: string): Promise<Blob | undefined> {
|
2023-03-15 11:17:03 -07:00
|
|
|
return this._backend.readBlob('resources/' + sha1);
|
2021-10-12 13:42:50 -08:00
|
|
|
}
|
|
|
|
|
2023-03-22 09:32:21 -07:00
|
|
|
storage(): SnapshotStorage {
|
2021-10-12 13:42:50 -08:00
|
|
|
return this._snapshotStorage!;
|
|
|
|
}
|
|
|
|
|
2023-02-27 22:31:47 -08:00
|
|
|
private _pageEntry(contextEntry: ContextEntry, pageId: string): PageEntry {
|
2021-10-12 13:42:50 -08:00
|
|
|
let pageEntry = this.pageEntries.get(pageId);
|
|
|
|
if (!pageEntry) {
|
|
|
|
pageEntry = {
|
|
|
|
screencastFrames: [],
|
|
|
|
};
|
|
|
|
this.pageEntries.set(pageId, pageEntry);
|
2023-02-27 22:31:47 -08:00
|
|
|
contextEntry.pages.push(pageEntry);
|
2021-10-12 13:42:50 -08:00
|
|
|
}
|
|
|
|
return pageEntry;
|
|
|
|
}
|
|
|
|
|
2023-03-15 22:33:40 -07:00
|
|
|
appendEvent(contextEntry: ContextEntry, actionMap: Map<string, trace.ActionTraceEvent>, line: string) {
|
2021-10-12 13:42:50 -08:00
|
|
|
if (!line)
|
|
|
|
return;
|
|
|
|
const event = this._modernize(JSON.parse(line));
|
2023-02-27 15:29:20 -08:00
|
|
|
if (!event)
|
|
|
|
return;
|
2021-10-12 13:42:50 -08:00
|
|
|
switch (event.type) {
|
|
|
|
case 'context-options': {
|
2023-02-28 16:49:14 -08:00
|
|
|
this._version = event.version;
|
2023-05-05 15:12:18 -07:00
|
|
|
contextEntry.isPrimary = true;
|
2023-02-27 22:31:47 -08:00
|
|
|
contextEntry.browserName = event.browserName;
|
|
|
|
contextEntry.title = event.title;
|
|
|
|
contextEntry.platform = event.platform;
|
|
|
|
contextEntry.wallTime = event.wallTime;
|
|
|
|
contextEntry.sdkLanguage = event.sdkLanguage;
|
|
|
|
contextEntry.options = event.options;
|
|
|
|
contextEntry.testIdAttributeName = event.testIdAttributeName;
|
2021-10-12 13:42:50 -08:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
case 'screencast-frame': {
|
2023-02-27 22:31:47 -08:00
|
|
|
this._pageEntry(contextEntry, event.pageId).screencastFrames.push(event);
|
2021-10-12 13:42:50 -08:00
|
|
|
break;
|
|
|
|
}
|
2023-03-15 22:33:40 -07:00
|
|
|
case 'before': {
|
|
|
|
actionMap.set(event.callId, { ...event, type: 'action', endTime: 0, log: [] });
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case 'input': {
|
|
|
|
const existing = actionMap.get(event.callId);
|
|
|
|
existing!.inputSnapshot = event.inputSnapshot;
|
|
|
|
existing!.point = event.point;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case 'after': {
|
|
|
|
const existing = actionMap.get(event.callId);
|
|
|
|
existing!.afterSnapshot = event.afterSnapshot;
|
|
|
|
existing!.endTime = event.endTime;
|
|
|
|
existing!.log = event.log;
|
|
|
|
existing!.result = event.result;
|
|
|
|
existing!.error = event.error;
|
2023-04-25 17:38:12 -07:00
|
|
|
existing!.attachments = event.attachments;
|
2023-03-15 22:33:40 -07:00
|
|
|
break;
|
|
|
|
}
|
2021-10-12 13:42:50 -08:00
|
|
|
case 'action': {
|
2023-03-15 22:33:40 -07:00
|
|
|
actionMap.set(event.callId, event);
|
2021-10-12 13:42:50 -08:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
case 'event': {
|
2023-02-27 22:31:47 -08:00
|
|
|
contextEntry!.events.push(event);
|
2023-02-27 15:29:20 -08:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
case 'object': {
|
2023-02-27 22:31:47 -08:00
|
|
|
contextEntry!.initializers[event.guid] = event.initializer;
|
2021-10-12 13:42:50 -08:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
case 'resource-snapshot':
|
|
|
|
this._snapshotStorage!.addResource(event.snapshot);
|
2023-02-27 22:31:47 -08:00
|
|
|
contextEntry.resources.push(event.snapshot);
|
2021-10-12 13:42:50 -08:00
|
|
|
break;
|
|
|
|
case 'frame-snapshot':
|
|
|
|
this._snapshotStorage!.addFrameSnapshot(event.snapshot);
|
|
|
|
break;
|
|
|
|
}
|
2023-03-15 22:33:40 -07:00
|
|
|
if (event.type === 'action' || event.type === 'before')
|
2023-02-27 22:31:47 -08:00
|
|
|
contextEntry.startTime = Math.min(contextEntry.startTime, event.startTime);
|
2023-03-15 22:33:40 -07:00
|
|
|
if (event.type === 'action' || event.type === 'after')
|
2023-02-27 22:31:47 -08:00
|
|
|
contextEntry.endTime = Math.max(contextEntry.endTime, event.endTime);
|
2023-02-27 15:29:20 -08:00
|
|
|
if (event.type === 'event') {
|
2023-02-27 22:31:47 -08:00
|
|
|
contextEntry.startTime = Math.min(contextEntry.startTime, event.time);
|
|
|
|
contextEntry.endTime = Math.max(contextEntry.endTime, event.time);
|
2021-10-12 13:42:50 -08:00
|
|
|
}
|
2022-03-31 08:45:45 -08:00
|
|
|
if (event.type === 'screencast-frame') {
|
2023-02-27 22:31:47 -08:00
|
|
|
contextEntry.startTime = Math.min(contextEntry.startTime, event.timestamp);
|
|
|
|
contextEntry.endTime = Math.max(contextEntry.endTime, event.timestamp);
|
2022-03-31 08:45:45 -08:00
|
|
|
}
|
2021-10-12 13:42:50 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
private _modernize(event: any): trace.TraceEvent {
|
|
|
|
if (this._version === undefined)
|
|
|
|
return event;
|
2023-02-28 16:49:14 -08:00
|
|
|
const lastVersion: trace.VERSION = 4;
|
|
|
|
for (let version = this._version; version < lastVersion; ++version)
|
2021-10-12 13:42:50 -08:00
|
|
|
event = (this as any)[`_modernize_${version}_to_${version + 1}`].call(this, event);
|
|
|
|
return event;
|
|
|
|
}
|
|
|
|
|
|
|
|
_modernize_0_to_1(event: any): any {
|
|
|
|
if (event.type === 'action') {
|
|
|
|
if (typeof event.metadata.error === 'string')
|
|
|
|
event.metadata.error = { error: { name: 'Error', message: event.metadata.error } };
|
|
|
|
}
|
|
|
|
return event;
|
|
|
|
}
|
|
|
|
|
|
|
|
_modernize_1_to_2(event: any): any {
|
|
|
|
if (event.type === 'frame-snapshot' && event.snapshot.isMainFrame) {
|
|
|
|
// Old versions had completely wrong viewport.
|
2023-02-27 22:31:47 -08:00
|
|
|
event.snapshot.viewport = this.contextEntries[0]?.options?.viewport || { width: 1280, height: 720 };
|
2021-10-12 13:42:50 -08:00
|
|
|
}
|
|
|
|
return event;
|
|
|
|
}
|
|
|
|
|
|
|
|
_modernize_2_to_3(event: any): any {
|
|
|
|
if (event.type === 'resource-snapshot' && !event.snapshot.request) {
|
|
|
|
// Migrate from old ResourceSnapshot to new har entry format.
|
|
|
|
const resource = event.snapshot;
|
|
|
|
event.snapshot = {
|
|
|
|
_frameref: resource.frameId,
|
|
|
|
request: {
|
|
|
|
url: resource.url,
|
|
|
|
method: resource.method,
|
|
|
|
headers: resource.requestHeaders,
|
|
|
|
postData: resource.requestSha1 ? { _sha1: resource.requestSha1 } : undefined,
|
|
|
|
},
|
|
|
|
response: {
|
|
|
|
status: resource.status,
|
|
|
|
headers: resource.responseHeaders,
|
|
|
|
content: {
|
|
|
|
mimeType: resource.contentType,
|
|
|
|
_sha1: resource.responseSha1,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
_monotonicTime: resource.timestamp,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
return event;
|
|
|
|
}
|
2023-02-27 15:29:20 -08:00
|
|
|
|
|
|
|
_modernize_3_to_4(event: traceV3.TraceEvent): trace.TraceEvent | null {
|
2023-02-28 16:49:14 -08:00
|
|
|
if (event.type !== 'action' && event.type !== 'event') {
|
2023-02-27 15:29:20 -08:00
|
|
|
return event as traceV3.ContextCreatedTraceEvent |
|
|
|
|
traceV3.ScreencastFrameTraceEvent |
|
|
|
|
traceV3.ResourceSnapshotTraceEvent |
|
|
|
|
traceV3.FrameSnapshotTraceEvent;
|
|
|
|
}
|
|
|
|
|
|
|
|
const metadata = event.metadata;
|
|
|
|
if (metadata.internal || metadata.method.startsWith('tracing'))
|
|
|
|
return null;
|
2023-02-28 16:49:14 -08:00
|
|
|
|
|
|
|
if (event.type === 'event') {
|
2023-02-27 15:29:20 -08:00
|
|
|
if (metadata.method === '__create__' && metadata.type === 'ConsoleMessage') {
|
|
|
|
return {
|
|
|
|
type: 'object',
|
|
|
|
class: metadata.type,
|
|
|
|
guid: metadata.params.guid,
|
|
|
|
initializer: metadata.params.initializer,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
return {
|
|
|
|
type: 'event',
|
|
|
|
time: metadata.startTime,
|
|
|
|
class: metadata.type,
|
|
|
|
method: metadata.method,
|
|
|
|
params: metadata.params,
|
|
|
|
pageId: metadata.pageId,
|
|
|
|
};
|
|
|
|
}
|
2023-02-28 16:49:14 -08:00
|
|
|
|
2023-02-27 15:29:20 -08:00
|
|
|
return {
|
|
|
|
type: 'action',
|
|
|
|
callId: metadata.id,
|
|
|
|
startTime: metadata.startTime,
|
|
|
|
endTime: metadata.endTime,
|
|
|
|
apiName: metadata.apiName || metadata.type + '.' + metadata.method,
|
|
|
|
class: metadata.type,
|
|
|
|
method: metadata.method,
|
|
|
|
params: metadata.params,
|
|
|
|
wallTime: metadata.wallTime || Date.now(),
|
|
|
|
log: metadata.log,
|
2023-03-23 12:49:53 -07:00
|
|
|
beforeSnapshot: metadata.snapshots.find(s => s.title === 'before')?.snapshotName,
|
|
|
|
inputSnapshot: metadata.snapshots.find(s => s.title === 'input')?.snapshotName,
|
|
|
|
afterSnapshot: metadata.snapshots.find(s => s.title === 'after')?.snapshotName,
|
2023-02-27 15:29:20 -08:00
|
|
|
error: metadata.error?.error,
|
|
|
|
result: metadata.result,
|
|
|
|
point: metadata.point,
|
|
|
|
pageId: metadata.pageId,
|
|
|
|
};
|
|
|
|
}
|
2021-10-12 13:42:50 -08:00
|
|
|
}
|