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.
|
|
|
|
*/
|
|
|
|
|
2023-05-05 15:12:18 -07:00
|
|
|
import { parseClientSideCallMetadata } from '../../../packages/playwright-core/src/utils/isomorphic/traceUtils';
|
2024-09-20 15:25:49 -07:00
|
|
|
import type { ActionEntry, ContextEntry } from './entries';
|
2022-04-06 13:57:14 -08:00
|
|
|
import { createEmptyContext } from './entries';
|
2023-03-22 09:32:21 -07:00
|
|
|
import { SnapshotStorage } from './snapshotStorage';
|
2024-05-07 11:42:28 -07:00
|
|
|
import { TraceModernizer } from './traceModernizer';
|
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;
|
|
|
|
}
|
2023-08-25 12:10:28 -07:00
|
|
|
|
2021-10-12 13:42:50 -08:00
|
|
|
export class TraceModel {
|
2023-02-27 22:31:47 -08:00
|
|
|
contextEntries: ContextEntry[] = [];
|
2023-03-22 09:32:21 -07:00
|
|
|
private _snapshotStorage: SnapshotStorage | undefined;
|
2023-03-15 11:17:03 -07:00
|
|
|
private _backend!: TraceModelBackend;
|
2023-08-29 18:05:01 +02:00
|
|
|
private _resourceToContentType = new Map<string, string>();
|
2021-10-12 13:42:50 -08:00
|
|
|
|
|
|
|
constructor() {
|
|
|
|
}
|
|
|
|
|
2024-09-20 15:25:49 -07:00
|
|
|
async load(backend: TraceModelBackend, isRecorderMode: boolean, unzipProgress: (done: number, total: number) => void) {
|
2023-05-05 15:12:18 -07:00
|
|
|
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-05-05 15:12:18 -07:00
|
|
|
contextEntry.traceUrl = backend.traceURL();
|
2023-02-27 22:31:47 -08:00
|
|
|
contextEntry.hasSource = hasSource;
|
2024-07-31 06:20:36 -07:00
|
|
|
const modernizer = new TraceModernizer(contextEntry, this._snapshotStorage);
|
2023-02-27 22:31:47 -08:00
|
|
|
|
2023-04-20 08:19:00 -07:00
|
|
|
const trace = await this._backend.readText(ordinal + '.trace') || '';
|
2024-05-07 11:42:28 -07:00
|
|
|
modernizer.appendTrace(trace);
|
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') || '';
|
2024-05-07 11:42:28 -07:00
|
|
|
modernizer.appendTrace(network);
|
2023-04-05 13:03:25 -07:00
|
|
|
unzipProgress(++done, total);
|
2023-03-15 22:33:40 -07:00
|
|
|
|
2024-09-20 15:25:49 -07:00
|
|
|
const actions = modernizer.actions().sort((a1, a2) => a1.startTime - a2.startTime);
|
|
|
|
contextEntry.actions = isRecorderMode ? collapseActionsForRecorder(actions) : actions;
|
2024-09-17 18:26:44 -07:00
|
|
|
|
2023-05-23 09:36:35 -07:00
|
|
|
if (!backend.isLive()) {
|
|
|
|
// Terminate actions w/o after event gracefully.
|
|
|
|
// This would close after hooks event that has not been closed because
|
|
|
|
// the trace is usually saved before after hooks complete.
|
|
|
|
for (const action of contextEntry.actions.slice().reverse()) {
|
|
|
|
if (!action.endTime && !action.error) {
|
|
|
|
for (const a of contextEntry.actions) {
|
|
|
|
if (a.parentId === action.callId && action.endTime < a.endTime)
|
|
|
|
action.endTime = a.endTime;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
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-08-29 18:05:01 +02:00
|
|
|
for (const resource of contextEntry.resources) {
|
|
|
|
if (resource.request.postData?._sha1)
|
|
|
|
this._resourceToContentType.set(resource.request.postData._sha1, stripEncodingFromContentType(resource.request.postData.mimeType));
|
|
|
|
if (resource.response.content?._sha1)
|
|
|
|
this._resourceToContentType.set(resource.response.content._sha1, stripEncodingFromContentType(resource.response.content.mimeType));
|
|
|
|
}
|
|
|
|
|
2023-02-27 22:31:47 -08:00
|
|
|
this.contextEntries.push(contextEntry);
|
2023-02-22 21:08:47 -08:00
|
|
|
}
|
2023-07-10 20:04:48 -07:00
|
|
|
|
|
|
|
this._snapshotStorage!.finalize();
|
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-08-29 18:05:01 +02:00
|
|
|
const blob = await this._backend.readBlob('resources/' + sha1);
|
2024-08-19 10:29:51 -07:00
|
|
|
const contentType = this._resourceToContentType.get(sha1);
|
|
|
|
// "x-unknown" in the har means "no content type".
|
|
|
|
if (!blob || contentType === undefined || contentType === 'x-unknown')
|
|
|
|
return blob;
|
|
|
|
return new Blob([blob], { type: contentType });
|
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-08-29 18:05:01 +02:00
|
|
|
|
|
|
|
function stripEncodingFromContentType(contentType: string) {
|
|
|
|
const charset = contentType.match(/^(.*);\s*charset=.*$/);
|
|
|
|
if (charset)
|
|
|
|
return charset[1];
|
|
|
|
return contentType;
|
|
|
|
}
|
2024-09-20 15:25:49 -07:00
|
|
|
|
|
|
|
function collapseActionsForRecorder(actions: ActionEntry[]): ActionEntry[] {
|
|
|
|
const result: ActionEntry[] = [];
|
|
|
|
for (const action of actions) {
|
|
|
|
const lastAction = result[result.length - 1];
|
|
|
|
const isSameAction = lastAction && lastAction.method === action.method && lastAction.pageId === action.pageId;
|
|
|
|
const isSameSelector = lastAction && 'selector' in lastAction.params && 'selector' in action.params && action.params.selector === lastAction.params.selector;
|
|
|
|
const shouldMerge = isSameAction && (action.method === 'goto' || (action.method === 'fill' && isSameSelector));
|
|
|
|
if (!shouldMerge) {
|
|
|
|
result.push(action);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
result[result.length - 1] = action;
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|