2021-07-01 20:46:56 -07: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-10-18 22:23:40 -04:00
|
|
|
import type { Language } from '@isomorphic/locatorGenerators';
|
2022-09-20 18:41:51 -07:00
|
|
|
import type { ResourceSnapshot } from '@trace/snapshot';
|
|
|
|
import type * as trace from '@trace/trace';
|
2023-09-19 16:21:09 -07:00
|
|
|
import type { ActionTraceEvent } from '@trace/trace';
|
2024-09-26 14:50:09 -07:00
|
|
|
import type { ActionEntry, ContextEntry, PageEntry } from '../types/entries';
|
2023-12-22 14:19:53 -08:00
|
|
|
import type { StackFrame } from '@protocol/channels';
|
2025-03-05 15:37:25 +01:00
|
|
|
import { kTopLevelAttachmentPrefix } from '@testIsomorphic/util';
|
2021-07-01 20:46:56 -07:00
|
|
|
|
|
|
|
const contextSymbol = Symbol('context');
|
2023-03-17 20:20:35 -07:00
|
|
|
const nextInContextSymbol = Symbol('next');
|
|
|
|
const prevInListSymbol = Symbol('prev');
|
2021-07-01 20:46:56 -07:00
|
|
|
const eventsSymbol = Symbol('events');
|
|
|
|
|
2023-05-08 18:51:27 -07:00
|
|
|
export type SourceLocation = {
|
|
|
|
file: string;
|
|
|
|
line: number;
|
2024-07-29 07:32:13 -07:00
|
|
|
column: number;
|
|
|
|
source?: SourceModel;
|
2023-05-08 18:51:27 -07:00
|
|
|
};
|
|
|
|
|
2023-03-16 10:01:17 -07:00
|
|
|
export type SourceModel = {
|
2023-05-08 18:51:27 -07:00
|
|
|
errors: { line: number, message: string }[];
|
2023-03-16 10:01:17 -07:00
|
|
|
content: string | undefined;
|
|
|
|
};
|
|
|
|
|
2024-09-26 14:50:09 -07:00
|
|
|
export type ActionTraceEventInContext = ActionEntry & {
|
2023-05-19 15:18:18 -07:00
|
|
|
context: ContextEntry;
|
|
|
|
};
|
|
|
|
|
2023-07-05 11:20:28 -07:00
|
|
|
export type ActionTreeItem = {
|
|
|
|
id: string;
|
|
|
|
children: ActionTreeItem[];
|
|
|
|
parent: ActionTreeItem | undefined;
|
|
|
|
action?: ActionTraceEventInContext;
|
|
|
|
};
|
|
|
|
|
2025-03-04 17:20:36 +01:00
|
|
|
export type ErrorDescription = {
|
2023-12-22 14:19:53 -08:00
|
|
|
action?: ActionTraceEventInContext;
|
|
|
|
stack?: StackFrame[];
|
|
|
|
message: string;
|
2025-03-04 17:20:36 +01:00
|
|
|
prompt?: trace.AfterActionTraceEventAttachment & { traceUrl: string };
|
2023-12-22 14:19:53 -08:00
|
|
|
};
|
|
|
|
|
2025-03-05 16:35:18 +01:00
|
|
|
export type Attachment = trace.AfterActionTraceEventAttachment & { traceUrl: string };
|
|
|
|
|
2022-02-08 12:27:29 -08:00
|
|
|
export class MultiTraceModel {
|
|
|
|
readonly startTime: number;
|
|
|
|
readonly endTime: number;
|
|
|
|
readonly browserName: string;
|
2023-09-11 23:06:56 +02:00
|
|
|
readonly channel?: string;
|
2022-02-08 12:27:29 -08:00
|
|
|
readonly platform?: string;
|
|
|
|
readonly wallTime?: number;
|
|
|
|
readonly title?: string;
|
|
|
|
readonly options: trace.BrowserContextEventOptions;
|
|
|
|
readonly pages: PageEntry[];
|
2023-05-19 15:18:18 -07:00
|
|
|
readonly actions: ActionTraceEventInContext[];
|
2025-03-05 16:35:18 +01:00
|
|
|
readonly attachments: Attachment[];
|
|
|
|
readonly visibleAttachments: Attachment[];
|
2023-09-19 16:21:09 -07:00
|
|
|
readonly events: (trace.EventTraceEvent | trace.ConsoleMessageTraceEvent)[];
|
2023-07-10 18:36:28 -07:00
|
|
|
readonly stdio: trace.StdioTraceEvent[];
|
2023-11-16 11:37:57 -08:00
|
|
|
readonly errors: trace.ErrorTraceEvent[];
|
2023-12-22 14:19:53 -08:00
|
|
|
readonly errorDescriptors: ErrorDescription[];
|
2022-02-08 12:27:29 -08:00
|
|
|
readonly hasSource: boolean;
|
2023-12-22 14:19:53 -08:00
|
|
|
readonly hasStepData: boolean;
|
2022-10-18 22:23:40 -04:00
|
|
|
readonly sdkLanguage: Language | undefined;
|
2023-02-17 11:19:53 -08:00
|
|
|
readonly testIdAttributeName: string | undefined;
|
2023-03-16 10:01:17 -07:00
|
|
|
readonly sources: Map<string, SourceModel>;
|
2023-07-10 12:56:56 -07:00
|
|
|
resources: ResourceSnapshot[];
|
2023-03-16 10:01:17 -07:00
|
|
|
|
2022-02-08 12:27:29 -08:00
|
|
|
|
|
|
|
constructor(contexts: ContextEntry[]) {
|
|
|
|
contexts.forEach(contextEntry => indexModel(contextEntry));
|
2024-05-09 17:28:39 -07:00
|
|
|
const libraryContext = contexts.find(context => context.origin === 'library');
|
|
|
|
|
|
|
|
this.browserName = libraryContext?.browserName || '';
|
|
|
|
this.sdkLanguage = libraryContext?.sdkLanguage;
|
|
|
|
this.channel = libraryContext?.channel;
|
|
|
|
this.testIdAttributeName = libraryContext?.testIdAttributeName;
|
|
|
|
this.platform = libraryContext?.platform || '';
|
|
|
|
this.title = libraryContext?.title || '';
|
|
|
|
this.options = libraryContext?.options || {};
|
|
|
|
// Next call updates all timestamps for all events in library contexts, so it must be done first.
|
2024-03-12 16:32:58 -07:00
|
|
|
this.actions = mergeActionsAndUpdateTiming(contexts);
|
|
|
|
this.pages = ([] as PageEntry[]).concat(...contexts.map(c => c.pages));
|
2022-02-08 12:27:29 -08:00
|
|
|
this.wallTime = contexts.map(c => c.wallTime).reduce((prev, cur) => Math.min(prev || Number.MAX_VALUE, cur!), Number.MAX_VALUE);
|
|
|
|
this.startTime = contexts.map(c => c.startTime).reduce((prev, cur) => Math.min(prev, cur), Number.MAX_VALUE);
|
|
|
|
this.endTime = contexts.map(c => c.endTime).reduce((prev, cur) => Math.max(prev, cur), Number.MIN_VALUE);
|
2023-09-19 16:21:09 -07:00
|
|
|
this.events = ([] as (trace.EventTraceEvent | trace.ConsoleMessageTraceEvent)[]).concat(...contexts.map(c => c.events));
|
2023-07-10 18:36:28 -07:00
|
|
|
this.stdio = ([] as trace.StdioTraceEvent[]).concat(...contexts.map(c => c.stdio));
|
2023-11-16 11:37:57 -08:00
|
|
|
this.errors = ([] as trace.ErrorTraceEvent[]).concat(...contexts.map(c => c.errors));
|
2022-02-08 12:27:29 -08:00
|
|
|
this.hasSource = contexts.some(c => c.hasSource);
|
2024-05-09 17:28:39 -07:00
|
|
|
this.hasStepData = contexts.some(context => context.origin === 'testRunner');
|
2023-07-10 12:56:56 -07:00
|
|
|
this.resources = [...contexts.map(c => c.resources)].flat();
|
2025-03-05 16:35:18 +01:00
|
|
|
this.attachments = this.actions.flatMap(action => action.attachments?.map(attachment => ({ ...attachment, traceUrl: action.context.traceUrl })) ?? []);
|
|
|
|
this.visibleAttachments = this.attachments.filter(attachment => !attachment.name.startsWith('_'));
|
2022-02-08 12:27:29 -08:00
|
|
|
|
2023-02-27 15:29:20 -08:00
|
|
|
this.events.sort((a1, a2) => a1.time - a2.time);
|
2023-08-16 16:30:17 -07:00
|
|
|
this.resources.sort((a1, a2) => a1._monotonicTime! - a2._monotonicTime!);
|
2023-12-22 14:19:53 -08:00
|
|
|
this.errorDescriptors = this.hasStepData ? this._errorDescriptorsFromTestRunner() : this._errorDescriptorsFromActions();
|
|
|
|
this.sources = collectSources(this.actions, this.errorDescriptors);
|
2022-02-08 12:27:29 -08:00
|
|
|
}
|
2023-09-01 13:48:15 -07:00
|
|
|
|
|
|
|
failedAction() {
|
|
|
|
// This find innermost action for nested ones.
|
|
|
|
return this.actions.findLast(a => a.error);
|
|
|
|
}
|
2023-12-22 14:19:53 -08:00
|
|
|
|
|
|
|
private _errorDescriptorsFromActions(): ErrorDescription[] {
|
|
|
|
const errors: ErrorDescription[] = [];
|
|
|
|
for (const action of this.actions || []) {
|
|
|
|
if (!action.error?.message)
|
|
|
|
continue;
|
|
|
|
errors.push({
|
|
|
|
action,
|
|
|
|
stack: action.stack,
|
|
|
|
message: action.error.message,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
return errors;
|
|
|
|
}
|
|
|
|
|
|
|
|
private _errorDescriptorsFromTestRunner(): ErrorDescription[] {
|
2025-03-04 17:20:36 +01:00
|
|
|
return this.errors.filter(e => !!e.message).map((error, i) => ({
|
|
|
|
stack: error.stack,
|
|
|
|
message: error.message,
|
2025-03-05 16:35:18 +01:00
|
|
|
prompt: this.attachments.find(a => a.name === `_prompt-${i}`),
|
2025-03-04 17:20:36 +01:00
|
|
|
}));
|
2023-12-22 14:19:53 -08:00
|
|
|
}
|
2022-02-08 12:27:29 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
function indexModel(context: ContextEntry) {
|
2021-10-15 14:22:49 -08:00
|
|
|
for (const page of context.pages)
|
2021-07-01 20:46:56 -07:00
|
|
|
(page as any)[contextSymbol] = context;
|
2021-10-15 14:22:49 -08:00
|
|
|
for (let i = 0; i < context.actions.length; ++i) {
|
|
|
|
const action = context.actions[i] as any;
|
|
|
|
action[contextSymbol] = context;
|
2023-06-06 17:38:44 -07:00
|
|
|
}
|
|
|
|
let lastNonRouteAction = undefined;
|
|
|
|
for (let i = context.actions.length - 1; i >= 0; i--) {
|
|
|
|
const action = context.actions[i] as any;
|
|
|
|
action[nextInContextSymbol] = lastNonRouteAction;
|
|
|
|
if (!action.apiName.includes('route.'))
|
|
|
|
lastNonRouteAction = action;
|
2021-07-01 20:46:56 -07:00
|
|
|
}
|
2021-10-15 14:22:49 -08:00
|
|
|
for (const event of context.events)
|
|
|
|
(event as any)[contextSymbol] = context;
|
2024-05-15 16:29:26 -07:00
|
|
|
for (const resource of context.resources)
|
|
|
|
(resource as any)[contextSymbol] = context;
|
2021-07-01 20:46:56 -07:00
|
|
|
}
|
|
|
|
|
2024-03-12 16:32:58 -07:00
|
|
|
function mergeActionsAndUpdateTiming(contexts: ContextEntry[]) {
|
2024-05-08 17:33:31 -07:00
|
|
|
const traceFileToContexts = new Map<string, ContextEntry[]>();
|
|
|
|
for (const context of contexts) {
|
|
|
|
const traceFile = context.traceUrl;
|
|
|
|
let list = traceFileToContexts.get(traceFile);
|
|
|
|
if (!list) {
|
|
|
|
list = [];
|
|
|
|
traceFileToContexts.set(traceFile, list);
|
|
|
|
}
|
|
|
|
list.push(context);
|
|
|
|
}
|
|
|
|
|
|
|
|
const result: ActionTraceEventInContext[] = [];
|
|
|
|
let traceFileId = 0;
|
|
|
|
for (const [, contexts] of traceFileToContexts) {
|
|
|
|
// Action ids are unique only within a trace file. If there are
|
|
|
|
// traces from more than one file we make the ids unique across the
|
|
|
|
// files. The code does not update snapshot ids as they are always
|
|
|
|
// retrieved from a particular trace file.
|
2024-05-09 09:33:16 -07:00
|
|
|
if (traceFileToContexts.size > 1)
|
2024-05-08 17:33:31 -07:00
|
|
|
makeCallIdsUniqueAcrossTraceFiles(contexts, ++traceFileId);
|
|
|
|
// Align action times across runner and library contexts within each trace file.
|
2024-07-19 11:18:22 -07:00
|
|
|
const actions = mergeActionsAndUpdateTimingSameTrace(contexts);
|
|
|
|
result.push(...actions);
|
2024-05-08 17:33:31 -07:00
|
|
|
}
|
|
|
|
result.sort((a1, a2) => {
|
|
|
|
if (a2.parentId === a1.callId)
|
|
|
|
return -1;
|
|
|
|
if (a1.parentId === a2.callId)
|
|
|
|
return 1;
|
2024-05-09 15:31:23 -07:00
|
|
|
return a1.startTime - a2.startTime;
|
2024-05-08 17:33:31 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
for (let i = 1; i < result.length; ++i)
|
|
|
|
(result[i] as any)[prevInListSymbol] = result[i - 1];
|
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
function makeCallIdsUniqueAcrossTraceFiles(contexts: ContextEntry[], traceFileId: number) {
|
|
|
|
for (const context of contexts) {
|
|
|
|
for (const action of context.actions) {
|
|
|
|
if (action.callId)
|
|
|
|
action.callId = `${traceFileId}:${action.callId}`;
|
|
|
|
if (action.parentId)
|
|
|
|
action.parentId = `${traceFileId}:${action.parentId}`;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-07-19 11:18:22 -07:00
|
|
|
function mergeActionsAndUpdateTimingSameTrace(contexts: ContextEntry[]): ActionTraceEventInContext[] {
|
2023-05-19 15:18:18 -07:00
|
|
|
const map = new Map<string, ActionTraceEventInContext>();
|
2023-05-05 15:12:18 -07:00
|
|
|
|
2024-05-09 17:28:39 -07:00
|
|
|
const libraryContexts = contexts.filter(context => context.origin === 'library');
|
|
|
|
const testRunnerContexts = contexts.filter(context => context.origin === 'testRunner');
|
2023-05-05 15:12:18 -07:00
|
|
|
|
2024-07-19 11:18:22 -07:00
|
|
|
// With library-only or test-runner-only traces there is nothing to match.
|
|
|
|
if (!testRunnerContexts.length || !libraryContexts.length) {
|
|
|
|
return contexts.map(context => {
|
|
|
|
return context.actions.map(action => ({ ...action, context }));
|
|
|
|
}).flat();
|
|
|
|
}
|
|
|
|
|
2024-05-14 12:10:46 -07:00
|
|
|
// Library actions are replaced with corresponding test runner steps. Matching with
|
|
|
|
// the test runner steps enables us to find parent steps.
|
|
|
|
// - In the newer versions the actions are matched by explicit step id stored in the
|
|
|
|
// library context actions.
|
|
|
|
// - In the older versions the step id is not stored and the match is perfomed based on
|
|
|
|
// action name and wallTime.
|
2024-07-19 11:18:22 -07:00
|
|
|
const matchByStepId = libraryContexts.some(c => c.actions.some(a => !!a.stepId));
|
2024-05-14 12:10:46 -07:00
|
|
|
|
2024-05-09 17:28:39 -07:00
|
|
|
for (const context of libraryContexts) {
|
2024-05-14 12:10:46 -07:00
|
|
|
for (const action of context.actions) {
|
|
|
|
const key = matchByStepId ? action.stepId! : `${action.apiName}@${(action as any).wallTime}`;
|
|
|
|
map.set(key, { ...action, context });
|
|
|
|
}
|
2023-02-28 13:26:23 -08:00
|
|
|
}
|
2023-05-05 15:12:18 -07:00
|
|
|
|
2024-05-09 17:28:39 -07:00
|
|
|
// Protocol call aka library contexts have startTime/endTime as server-side times.
|
|
|
|
// Step aka test runner contexts have startTime/endTime as client-side times.
|
|
|
|
// Adjust startTime/endTime on the library contexts to align them with the test
|
2024-05-09 15:31:23 -07:00
|
|
|
// runner steps.
|
2024-05-14 12:10:46 -07:00
|
|
|
const delta = monotonicTimeDeltaBetweenLibraryAndRunner(testRunnerContexts, map, matchByStepId);
|
2024-05-09 15:31:23 -07:00
|
|
|
if (delta)
|
2024-05-09 17:28:39 -07:00
|
|
|
adjustMonotonicTime(libraryContexts, delta);
|
2024-05-09 15:31:23 -07:00
|
|
|
|
2023-06-06 17:38:44 -07:00
|
|
|
const nonPrimaryIdToPrimaryId = new Map<string, string>();
|
2024-05-09 17:28:39 -07:00
|
|
|
for (const context of testRunnerContexts) {
|
2023-05-05 15:12:18 -07:00
|
|
|
for (const action of context.actions) {
|
2024-05-14 12:10:46 -07:00
|
|
|
const key = matchByStepId ? action.callId : `${action.apiName}@${(action as any).wallTime}`;
|
2023-05-06 10:25:32 -07:00
|
|
|
const existing = map.get(key);
|
2024-05-09 15:31:23 -07:00
|
|
|
if (existing) {
|
2023-06-06 17:38:44 -07:00
|
|
|
nonPrimaryIdToPrimaryId.set(action.callId, existing.callId);
|
2023-05-05 15:12:18 -07:00
|
|
|
if (action.error)
|
|
|
|
existing.error = action.error;
|
|
|
|
if (action.attachments)
|
|
|
|
existing.attachments = action.attachments;
|
2025-01-31 15:45:57 -08:00
|
|
|
if (action.annotations)
|
|
|
|
existing.annotations = action.annotations;
|
2023-05-09 14:50:28 -07:00
|
|
|
if (action.parentId)
|
2023-06-06 17:38:44 -07:00
|
|
|
existing.parentId = nonPrimaryIdToPrimaryId.get(action.parentId) ?? action.parentId;
|
2024-05-09 15:31:23 -07:00
|
|
|
// For the events that are present in the test runner context, always take
|
|
|
|
// their time from the test runner context to preserve client side order.
|
|
|
|
existing.startTime = action.startTime;
|
|
|
|
existing.endTime = action.endTime;
|
2023-05-05 15:12:18 -07:00
|
|
|
continue;
|
|
|
|
}
|
2023-06-14 09:37:19 -07:00
|
|
|
if (action.parentId)
|
|
|
|
action.parentId = nonPrimaryIdToPrimaryId.get(action.parentId) ?? action.parentId;
|
2023-05-19 15:18:18 -07:00
|
|
|
map.set(key, { ...action, context });
|
2023-02-28 13:26:23 -08:00
|
|
|
}
|
|
|
|
}
|
2024-07-19 11:18:22 -07:00
|
|
|
return [...map.values()];
|
2024-05-09 15:31:23 -07:00
|
|
|
}
|
2023-02-28 13:26:23 -08:00
|
|
|
|
2024-05-09 15:31:23 -07:00
|
|
|
function adjustMonotonicTime(contexts: ContextEntry[], monotonicTimeDelta: number) {
|
|
|
|
for (const context of contexts) {
|
|
|
|
context.startTime += monotonicTimeDelta;
|
|
|
|
context.endTime += monotonicTimeDelta;
|
|
|
|
for (const action of context.actions) {
|
|
|
|
if (action.startTime)
|
|
|
|
action.startTime += monotonicTimeDelta;
|
|
|
|
if (action.endTime)
|
|
|
|
action.endTime += monotonicTimeDelta;
|
|
|
|
}
|
2024-03-12 16:32:58 -07:00
|
|
|
for (const event of context.events)
|
2024-05-09 15:31:23 -07:00
|
|
|
event.time += monotonicTimeDelta;
|
|
|
|
for (const event of context.stdio)
|
|
|
|
event.timestamp += monotonicTimeDelta;
|
2024-03-12 16:32:58 -07:00
|
|
|
for (const page of context.pages) {
|
|
|
|
for (const frame of page.screencastFrames)
|
2024-05-09 15:31:23 -07:00
|
|
|
frame.timestamp += monotonicTimeDelta;
|
2024-03-12 16:32:58 -07:00
|
|
|
}
|
2024-06-19 15:06:20 -07:00
|
|
|
for (const resource of context.resources) {
|
|
|
|
if (resource._monotonicTime)
|
|
|
|
resource._monotonicTime += monotonicTimeDelta;
|
|
|
|
}
|
2024-03-12 16:32:58 -07:00
|
|
|
}
|
2024-05-09 15:31:23 -07:00
|
|
|
}
|
|
|
|
|
2024-05-14 12:10:46 -07:00
|
|
|
function monotonicTimeDeltaBetweenLibraryAndRunner(nonPrimaryContexts: ContextEntry[], libraryActions: Map<string, ActionTraceEventInContext>, matchByStepId: boolean) {
|
2024-05-09 15:31:23 -07:00
|
|
|
// We cannot rely on wall time or monotonic time to be the in sync
|
|
|
|
// between library and test runner contexts. So we find first action
|
|
|
|
// that is present in both runner and library contexts and use it
|
|
|
|
// to calculate the time delta, assuming the two events happened at the
|
|
|
|
// same instant.
|
|
|
|
for (const context of nonPrimaryContexts) {
|
|
|
|
for (const action of context.actions) {
|
|
|
|
if (!action.startTime)
|
|
|
|
continue;
|
2024-09-17 11:14:15 -07:00
|
|
|
const key = matchByStepId ? action.callId! : `${action.apiName}@${(action as any).wallTime}`;
|
2024-05-09 15:31:23 -07:00
|
|
|
const libraryAction = libraryActions.get(key);
|
|
|
|
if (libraryAction)
|
|
|
|
return action.startTime - libraryAction.startTime;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return 0;
|
2023-02-28 13:26:23 -08:00
|
|
|
}
|
|
|
|
|
2023-07-05 11:20:28 -07:00
|
|
|
export function buildActionTree(actions: ActionTraceEventInContext[]): { rootItem: ActionTreeItem, itemMap: Map<string, ActionTreeItem> } {
|
|
|
|
const itemMap = new Map<string, ActionTreeItem>();
|
|
|
|
|
|
|
|
for (const action of actions) {
|
|
|
|
itemMap.set(action.callId, {
|
|
|
|
id: action.callId,
|
|
|
|
parent: undefined,
|
|
|
|
children: [],
|
|
|
|
action,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
const rootItem: ActionTreeItem = { id: '', parent: undefined, children: [] };
|
|
|
|
for (const item of itemMap.values()) {
|
2025-03-05 15:37:25 +01:00
|
|
|
if (item.action?.apiName.startsWith(kTopLevelAttachmentPrefix))
|
|
|
|
continue;
|
2023-07-05 11:20:28 -07:00
|
|
|
const parent = item.action!.parentId ? itemMap.get(item.action!.parentId) || rootItem : rootItem;
|
|
|
|
parent.children.push(item);
|
|
|
|
item.parent = parent;
|
|
|
|
}
|
|
|
|
return { rootItem, itemMap };
|
|
|
|
}
|
|
|
|
|
2024-05-15 16:29:26 -07:00
|
|
|
export function context(action: ActionTraceEvent | trace.EventTraceEvent | ResourceSnapshot): ContextEntry {
|
2021-07-01 20:46:56 -07:00
|
|
|
return (action as any)[contextSymbol];
|
|
|
|
}
|
|
|
|
|
2023-03-17 20:20:35 -07:00
|
|
|
function nextInContext(action: ActionTraceEvent): ActionTraceEvent {
|
|
|
|
return (action as any)[nextInContextSymbol];
|
|
|
|
}
|
|
|
|
|
|
|
|
export function prevInList(action: ActionTraceEvent): ActionTraceEvent {
|
|
|
|
return (action as any)[prevInListSymbol];
|
2021-07-01 20:46:56 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
export function stats(action: ActionTraceEvent): { errors: number, warnings: number } {
|
|
|
|
let errors = 0;
|
|
|
|
let warnings = 0;
|
|
|
|
for (const event of eventsForAction(action)) {
|
2023-09-19 16:21:09 -07:00
|
|
|
if (event.type === 'console') {
|
|
|
|
const type = event.messageType;
|
2021-07-01 20:46:56 -07:00
|
|
|
if (type === 'warning')
|
|
|
|
++warnings;
|
|
|
|
else if (type === 'error')
|
|
|
|
++errors;
|
|
|
|
}
|
2023-09-19 16:21:09 -07:00
|
|
|
if (event.type === 'event' && event.method === 'pageError')
|
2021-07-01 20:46:56 -07:00
|
|
|
++errors;
|
|
|
|
}
|
|
|
|
return { errors, warnings };
|
|
|
|
}
|
|
|
|
|
2023-09-19 16:21:09 -07:00
|
|
|
export function eventsForAction(action: ActionTraceEvent): (trace.EventTraceEvent | trace.ConsoleMessageTraceEvent)[] {
|
|
|
|
let result: (trace.EventTraceEvent | trace.ConsoleMessageTraceEvent)[] = (action as any)[eventsSymbol];
|
2021-07-01 20:46:56 -07:00
|
|
|
if (result)
|
|
|
|
return result;
|
|
|
|
|
2023-03-17 20:20:35 -07:00
|
|
|
const nextAction = nextInContext(action);
|
2021-10-15 14:22:49 -08:00
|
|
|
result = context(action).events.filter(event => {
|
2023-02-27 15:29:20 -08:00
|
|
|
return event.time >= action.startTime && (!nextAction || event.time < nextAction.startTime);
|
2021-07-01 20:46:56 -07:00
|
|
|
});
|
|
|
|
(action as any)[eventsSymbol] = result;
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2023-12-22 14:19:53 -08:00
|
|
|
function collectSources(actions: trace.ActionTraceEvent[], errorDescriptors: ErrorDescription[]): Map<string, SourceModel> {
|
2023-03-16 10:01:17 -07:00
|
|
|
const result = new Map<string, SourceModel>();
|
|
|
|
for (const action of actions) {
|
|
|
|
for (const frame of action.stack || []) {
|
|
|
|
let source = result.get(frame.file);
|
|
|
|
if (!source) {
|
|
|
|
source = { errors: [], content: undefined };
|
|
|
|
result.set(frame.file, source);
|
|
|
|
}
|
|
|
|
}
|
2023-12-22 14:19:53 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
for (const error of errorDescriptors) {
|
|
|
|
const { action, stack, message } = error;
|
|
|
|
if (!action || !stack)
|
|
|
|
continue;
|
|
|
|
result.get(stack[0].file)?.errors.push({
|
|
|
|
line: stack[0].line || 0,
|
|
|
|
message
|
|
|
|
});
|
2023-03-16 10:01:17 -07:00
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
2024-08-01 05:36:19 -07:00
|
|
|
|
|
|
|
const kRouteMethods = new Set([
|
2024-08-01 21:02:47 +02:00
|
|
|
'page.route',
|
|
|
|
'page.routefromhar',
|
|
|
|
'page.unroute',
|
|
|
|
'page.unrouteall',
|
|
|
|
'browsercontext.route',
|
|
|
|
'browsercontext.routefromhar',
|
|
|
|
'browsercontext.unroute',
|
|
|
|
'browsercontext.unrouteall',
|
2024-08-01 05:36:19 -07:00
|
|
|
]);
|
2024-08-01 21:02:47 +02:00
|
|
|
{
|
|
|
|
// .NET adds async suffix.
|
|
|
|
for (const method of [...kRouteMethods])
|
|
|
|
kRouteMethods.add(method + 'async');
|
|
|
|
// Python methods which contain underscores.
|
|
|
|
for (const method of [
|
|
|
|
'page.route_from_har',
|
|
|
|
'page.unroute_all',
|
|
|
|
'context.route_from_har',
|
|
|
|
'context.unroute_all',
|
|
|
|
])
|
|
|
|
kRouteMethods.add(method);
|
|
|
|
}
|
2024-08-01 05:36:19 -07:00
|
|
|
export function isRouteAction(action: ActionTraceEventInContext) {
|
|
|
|
return action.class === 'Route' || kRouteMethods.has(action.apiName.toLowerCase());
|
|
|
|
}
|