chore: do not show stale source in the trace (#23163)

This commit is contained in:
Pavel Feldman 2023-05-19 15:18:18 -07:00 committed by GitHub
parent bd090e67df
commit 0e1aeaaecf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 40 additions and 31 deletions

View file

@ -116,8 +116,12 @@ async function doFetch(event: FetchEvent): Promise<Response> {
} }
if (relativePath.startsWith('/sha1/')) { if (relativePath.startsWith('/sha1/')) {
// Sha1 is unique, load it from either of the models for simplicity. // Sha1 for sources is based on the file path, can't load it of a random model.
for (const { traceModel } of loadedTraces.values()) { const traceUrls = clientIdToTraceUrls.get(event.clientId);
for (const [trace, { traceModel }] of loadedTraces) {
// We will accept explicit ?trace= value as well as the clientId associated with the trace.
if (traceUrl !== trace && !traceUrls.includes(trace))
continue;
const blob = await traceModel!.resourceForSha1(relativePath.slice('/sha1/'.length)); const blob = await traceModel!.resourceForSha1(relativePath.slice('/sha1/'.length));
if (blob) if (blob)
return new Response(blob, { status: 200 }); return new Response(blob, { status: 200 });

View file

@ -23,13 +23,14 @@ import { asLocator } from '@isomorphic/locatorGenerators';
import type { Language } from '@isomorphic/locatorGenerators'; import type { Language } from '@isomorphic/locatorGenerators';
import type { TreeState } from '@web/components/treeView'; import type { TreeState } from '@web/components/treeView';
import { TreeView } from '@web/components/treeView'; import { TreeView } from '@web/components/treeView';
import type { ActionTraceEventInContext } from './modelUtil';
export interface ActionListProps { export interface ActionListProps {
actions: ActionTraceEvent[], actions: ActionTraceEventInContext[],
selectedAction: ActionTraceEvent | undefined, selectedAction: ActionTraceEventInContext | undefined,
sdkLanguage: Language | undefined; sdkLanguage: Language | undefined;
onSelected: (action: ActionTraceEvent) => void, onSelected: (action: ActionTraceEventInContext) => void,
onHighlighted: (action: ActionTraceEvent | undefined) => void, onHighlighted: (action: ActionTraceEventInContext | undefined) => void,
revealConsole: () => void, revealConsole: () => void,
isLive?: boolean, isLive?: boolean,
} }
@ -38,7 +39,7 @@ type ActionTreeItem = {
id: string; id: string;
children: ActionTreeItem[]; children: ActionTreeItem[];
parent: ActionTreeItem | undefined; parent: ActionTreeItem | undefined;
action?: ActionTraceEvent; action?: ActionTraceEventInContext;
}; };
const ActionTreeView = TreeView<ActionTreeItem>; const ActionTreeView = TreeView<ActionTreeItem>;

View file

@ -14,39 +14,40 @@
* limitations under the License. * limitations under the License.
*/ */
import type { ActionTraceEvent } from '@trace/trace';
import * as React from 'react'; import * as React from 'react';
import './attachmentsTab.css'; import './attachmentsTab.css';
import { ImageDiffView } from '@web/components/imageDiffView'; import { ImageDiffView } from '@web/components/imageDiffView';
import type { TestAttachment } from '@web/components/imageDiffView'; import type { TestAttachment } from '@web/components/imageDiffView';
import type { ActionTraceEventInContext } from './modelUtil';
export const AttachmentsTab: React.FunctionComponent<{ export const AttachmentsTab: React.FunctionComponent<{
action: ActionTraceEvent | undefined, action: ActionTraceEventInContext | undefined,
}> = ({ action }) => { }> = ({ action }) => {
if (!action) if (!action)
return null; return null;
const expected = action.attachments?.find(a => a.name.endsWith('-expected.png') && (a.path || a.sha1)) as TestAttachment | undefined; const expected = action.attachments?.find(a => a.name.endsWith('-expected.png') && (a.path || a.sha1)) as TestAttachment | undefined;
const actual = action.attachments?.find(a => a.name.endsWith('-actual.png') && (a.path || a.sha1)) as TestAttachment | undefined; const actual = action.attachments?.find(a => a.name.endsWith('-actual.png') && (a.path || a.sha1)) as TestAttachment | undefined;
const diff = action.attachments?.find(a => a.name.endsWith('-diff.png') && (a.path || a.sha1)) as TestAttachment | undefined; const diff = action.attachments?.find(a => a.name.endsWith('-diff.png') && (a.path || a.sha1)) as TestAttachment | undefined;
const traceUrl = action.context.traceUrl;
return <div className='attachments-tab'> return <div className='attachments-tab'>
{expected && actual && <div className='attachments-section'>Image diff</div>} {expected && actual && <div className='attachments-section'>Image diff</div>}
{expected && actual && <ImageDiffView imageDiff={{ {expected && actual && <ImageDiffView imageDiff={{
name: 'Image diff', name: 'Image diff',
expected: { attachment: { ...expected, path: attachmentURL(expected) }, title: 'Expected' }, expected: { attachment: { ...expected, path: attachmentURL(traceUrl, expected) }, title: 'Expected' },
actual: { attachment: { ...actual, path: attachmentURL(actual) } }, actual: { attachment: { ...actual, path: attachmentURL(traceUrl, actual) } },
diff: diff ? { attachment: { ...diff, path: attachmentURL(diff) } } : undefined, diff: diff ? { attachment: { ...diff, path: attachmentURL(traceUrl, diff) } } : undefined,
}} />} }} />}
{<div className='attachments-section'>Attachments</div>} {<div className='attachments-section'>Attachments</div>}
{action.attachments?.map(a => { {action.attachments?.map(a => {
return <div className='attachment-item'> return <div className='attachment-item'>
<a target='_blank' href={`sha1/${a.sha1}`}>{a.name}</a> <a target='_blank' href={attachmentURL(traceUrl, a)}>{a.name}</a>
</div>; </div>;
})} })}
</div>; </div>;
}; };
function attachmentURL(attachment: { function attachmentURL(traceUrl: string, attachment: {
name: string; name: string;
contentType: string; contentType: string;
path?: string; path?: string;
@ -54,6 +55,6 @@ function attachmentURL(attachment: {
body?: string; body?: string;
}) { }) {
if (attachment.sha1) if (attachment.sha1)
return 'sha1/' + attachment.sha1; return 'sha1/' + attachment.sha1 + '?trace=' + traceUrl;
return 'file?path=' + attachment.path; return 'file?path=' + attachment.path;
} }

View file

@ -37,6 +37,10 @@ export type SourceModel = {
content: string | undefined; content: string | undefined;
}; };
export type ActionTraceEventInContext = ActionTraceEvent & {
context: ContextEntry;
};
export class MultiTraceModel { export class MultiTraceModel {
readonly startTime: number; readonly startTime: number;
readonly endTime: number; readonly endTime: number;
@ -46,7 +50,7 @@ export class MultiTraceModel {
readonly title?: string; readonly title?: string;
readonly options: trace.BrowserContextEventOptions; readonly options: trace.BrowserContextEventOptions;
readonly pages: PageEntry[]; readonly pages: PageEntry[];
readonly actions: trace.ActionTraceEvent[]; readonly actions: ActionTraceEventInContext[];
readonly events: trace.EventTraceEvent[]; readonly events: trace.EventTraceEvent[];
readonly hasSource: boolean; readonly hasSource: boolean;
readonly sdkLanguage: Language | undefined; readonly sdkLanguage: Language | undefined;
@ -89,7 +93,7 @@ function indexModel(context: ContextEntry) {
} }
function mergeActions(contexts: ContextEntry[]) { function mergeActions(contexts: ContextEntry[]) {
const map = new Map<string, ActionTraceEvent>(); const map = new Map<string, ActionTraceEventInContext>();
// Protocol call aka isPrimary contexts have startTime/endTime as server-side times. // Protocol call aka isPrimary contexts have startTime/endTime as server-side times.
// Step aka non-isPrimary contexts have startTime/endTime are client-side times. // Step aka non-isPrimary contexts have startTime/endTime are client-side times.
@ -100,7 +104,7 @@ function mergeActions(contexts: ContextEntry[]) {
for (const context of primaryContexts) { for (const context of primaryContexts) {
for (const action of context.actions) for (const action of context.actions)
map.set(`${action.apiName}@${action.wallTime}`, action); map.set(`${action.apiName}@${action.wallTime}`, { ...action, context });
if (!offset && context.actions.length) if (!offset && context.actions.length)
offset = context.actions[0].startTime - context.actions[0].wallTime; offset = context.actions[0].startTime - context.actions[0].wallTime;
} }
@ -126,7 +130,7 @@ function mergeActions(contexts: ContextEntry[]) {
existing.parentId = action.parentId; existing.parentId = action.parentId;
continue; continue;
} }
map.set(key, action); map.set(key, { ...action, context });
} }
} }

View file

@ -15,16 +15,16 @@
limitations under the License. limitations under the License.
*/ */
import type { ActionTraceEvent, EventTraceEvent } from '@trace/trace'; import type { EventTraceEvent } from '@trace/trace';
import { msToString, useMeasure } from '@web/uiUtils'; import { msToString, useMeasure } from '@web/uiUtils';
import * as React from 'react'; import * as React from 'react';
import type { Boundaries } from '../geometry'; import type { Boundaries } from '../geometry';
import { FilmStrip } from './filmStrip'; import { FilmStrip } from './filmStrip';
import type { MultiTraceModel } from './modelUtil'; import type { ActionTraceEventInContext, MultiTraceModel } from './modelUtil';
import './timeline.css'; import './timeline.css';
type TimelineBar = { type TimelineBar = {
action?: ActionTraceEvent; action?: ActionTraceEventInContext;
event?: EventTraceEvent; event?: EventTraceEvent;
leftPosition: number; leftPosition: number;
rightPosition: number; rightPosition: number;
@ -38,8 +38,8 @@ type TimelineBar = {
export const Timeline: React.FunctionComponent<{ export const Timeline: React.FunctionComponent<{
model: MultiTraceModel | undefined, model: MultiTraceModel | undefined,
selectedAction: ActionTraceEvent | undefined, selectedAction: ActionTraceEventInContext | undefined,
onSelected: (action: ActionTraceEvent) => void, onSelected: (action: ActionTraceEventInContext) => void,
hideTimelineBars?: boolean, hideTimelineBars?: boolean,
}> = ({ model, selectedAction, onSelected, hideTimelineBars }) => { }> = ({ model, selectedAction, onSelected, hideTimelineBars }) => {
const [measure, ref] = useMeasure<HTMLDivElement>(); const [measure, ref] = useMeasure<HTMLDivElement>();

View file

@ -14,14 +14,13 @@
limitations under the License. limitations under the License.
*/ */
import type { ActionTraceEvent } from '@trace/trace';
import { SplitView } from '@web/components/splitView'; import { SplitView } from '@web/components/splitView';
import * as React from 'react'; import * as React from 'react';
import { ActionList } from './actionList'; import { ActionList } from './actionList';
import { CallTab } from './callTab'; import { CallTab } from './callTab';
import { ConsoleTab } from './consoleTab'; import { ConsoleTab } from './consoleTab';
import * as modelUtil from './modelUtil'; import * as modelUtil from './modelUtil';
import type { MultiTraceModel } from './modelUtil'; import type { ActionTraceEventInContext, MultiTraceModel } from './modelUtil';
import { NetworkTab } from './networkTab'; import { NetworkTab } from './networkTab';
import { SnapshotTab } from './snapshotTab'; import { SnapshotTab } from './snapshotTab';
import { SourceTab } from './sourceTab'; import { SourceTab } from './sourceTab';
@ -39,12 +38,12 @@ export const Workbench: React.FunctionComponent<{
showSourcesFirst?: boolean, showSourcesFirst?: boolean,
rootDir?: string, rootDir?: string,
fallbackLocation?: modelUtil.SourceLocation, fallbackLocation?: modelUtil.SourceLocation,
initialSelection?: ActionTraceEvent, initialSelection?: ActionTraceEventInContext,
onSelectionChanged?: (action: ActionTraceEvent) => void, onSelectionChanged?: (action: ActionTraceEventInContext) => void,
isLive?: boolean, isLive?: boolean,
}> = ({ model, hideTimelineBars, hideStackFrames, showSourcesFirst, rootDir, fallbackLocation, initialSelection, onSelectionChanged, isLive }) => { }> = ({ model, hideTimelineBars, hideStackFrames, showSourcesFirst, rootDir, fallbackLocation, initialSelection, onSelectionChanged, isLive }) => {
const [selectedAction, setSelectedAction] = React.useState<ActionTraceEvent | undefined>(undefined); const [selectedAction, setSelectedAction] = React.useState<ActionTraceEventInContext | undefined>(undefined);
const [highlightedAction, setHighlightedAction] = React.useState<ActionTraceEvent | undefined>(); const [highlightedAction, setHighlightedAction] = React.useState<ActionTraceEventInContext | undefined>();
const [selectedNavigatorTab, setSelectedNavigatorTab] = React.useState<string>('actions'); const [selectedNavigatorTab, setSelectedNavigatorTab] = React.useState<string>('actions');
const [selectedPropertiesTab, setSelectedPropertiesTab] = React.useState<string>(showSourcesFirst ? 'source' : 'call'); const [selectedPropertiesTab, setSelectedPropertiesTab] = React.useState<string>(showSourcesFirst ? 'source' : 'call');
const activeAction = model ? highlightedAction || selectedAction : undefined; const activeAction = model ? highlightedAction || selectedAction : undefined;
@ -63,7 +62,7 @@ export const Workbench: React.FunctionComponent<{
setSelectedAction(model.actions[model.actions.length - 1]); setSelectedAction(model.actions[model.actions.length - 1]);
}, [model, selectedAction, setSelectedAction, setSelectedPropertiesTab, initialSelection]); }, [model, selectedAction, setSelectedAction, setSelectedPropertiesTab, initialSelection]);
const onActionSelected = React.useCallback((action: ActionTraceEvent) => { const onActionSelected = React.useCallback((action: ActionTraceEventInContext) => {
setSelectedAction(action); setSelectedAction(action);
onSelectionChanged?.(action); onSelectionChanged?.(action);
}, [setSelectedAction, onSelectionChanged]); }, [setSelectedAction, onSelectionChanged]);