base 64 encode/decode urls with s3

This commit is contained in:
Edward Jibson 2025-01-02 15:58:35 +00:00
parent ee8208beda
commit 7f25d1ac1a
8 changed files with 224 additions and 104 deletions

View file

@ -23,7 +23,7 @@
navigator.serviceWorker.register('sw.bundle.js');
if (!navigator.serviceWorker.controller)
await new Promise(f => navigator.serviceWorker.oncontrollerchange = f);
const traceUrl = new URL(location.href).searchParams.get('trace');
let traceUrl = new URL(location.href).searchParams.get('trace');
const params = new URLSearchParams();
params.set('trace', traceUrl);
await fetch('contexts?' + params.toString()).then(r => r.json());

View file

@ -18,7 +18,11 @@ import { splitProgress } from './progress';
import { unwrapPopoutUrl } from './snapshotRenderer';
import { SnapshotServer } from './snapshotServer';
import { TraceModel } from './traceModel';
import { FetchTraceModelBackend, TraceViewerServer, ZipTraceModelBackend } from './traceModelBackends';
import {
FetchTraceModelBackend,
TraceViewerServer,
ZipTraceModelBackend,
} from './traceModelBackends';
import { TraceVersionError } from './traceModernizer';
// @ts-ignore
@ -34,20 +38,47 @@ self.addEventListener('activate', function(event: any) {
const scopePath = new URL(self.registration.scope).pathname;
const loadedTraces = new Map<string, { traceModel: TraceModel, snapshotServer: SnapshotServer }>();
const loadedTraces = new Map<
string,
{ traceModel: TraceModel; snapshotServer: SnapshotServer }
>();
const clientIdToTraceUrls = new Map<string, { limit: number | undefined, traceUrls: Set<string>, traceViewerServer: TraceViewerServer }>();
const clientIdToTraceUrls = new Map<
string,
{
limit: number | undefined;
traceUrls: Set<string>;
traceViewerServer: TraceViewerServer;
}
>();
async function loadTrace(traceUrl: string, traceFileName: string | null, client: any | undefined, limit: number | undefined, progress: (done: number, total: number) => undefined): Promise<TraceModel> {
async function loadTrace(
traceUrl: string,
traceFileName: string | null,
client: any | undefined,
limit: number | undefined,
progress: (done: number, total: number) => undefined
): Promise<TraceModel> {
await gc();
const clientId = client?.id ?? '';
let data = clientIdToTraceUrls.get(clientId);
if (!data) {
let traceViewerServerBaseUrl = new URL('../', client?.url ?? self.registration.scope);
if (traceViewerServerBaseUrl.searchParams.has('server'))
traceViewerServerBaseUrl = new URL(traceViewerServerBaseUrl.searchParams.get('server')!, traceViewerServerBaseUrl);
let traceViewerServerBaseUrl = new URL(
'../',
client?.url ?? self.registration.scope
);
if (traceViewerServerBaseUrl.searchParams.has('server')) {
traceViewerServerBaseUrl = new URL(
traceViewerServerBaseUrl.searchParams.get('server')!,
traceViewerServerBaseUrl
);
}
data = { limit, traceUrls: new Set(), traceViewerServer: new TraceViewerServer(traceViewerServerBaseUrl) };
data = {
limit,
traceUrls: new Set(),
traceViewerServer: new TraceViewerServer(traceViewerServerBaseUrl),
};
clientIdToTraceUrls.set(clientId, data);
}
data.traceUrls.add(traceUrl);
@ -55,21 +86,49 @@ async function loadTrace(traceUrl: string, traceFileName: string | null, client:
const traceModel = new TraceModel();
try {
// Allow 10% to hop from sw to page.
const [fetchProgress, unzipProgress] = splitProgress(progress, [0.5, 0.4, 0.1]);
const backend = traceUrl.endsWith('json') ? new FetchTraceModelBackend(traceUrl, data.traceViewerServer) : new ZipTraceModelBackend(traceUrl, data.traceViewerServer, fetchProgress);
const [fetchProgress, unzipProgress] = splitProgress(
progress,
[0.5, 0.4, 0.1]
);
const backend = traceUrl.endsWith('json')
? new FetchTraceModelBackend(traceUrl, data.traceViewerServer)
: new ZipTraceModelBackend(
traceUrl,
data.traceViewerServer,
fetchProgress
);
await traceModel.load(backend, unzipProgress);
} catch (error: any) {
// eslint-disable-next-line no-console
console.error(error);
if (error?.message?.includes('Cannot find .trace file') && await traceModel.hasEntry('index.html'))
throw new Error('Could not load trace. Did you upload a Playwright HTML report instead? Make sure to extract the archive first and then double-click the index.html file or put it on a web server.');
if (error instanceof TraceVersionError)
throw new Error(`Could not load trace from ${traceFileName || traceUrl}. ${error.message}`);
if (traceFileName)
throw new Error(`Could not load trace from ${traceFileName}. Make sure to upload a valid Playwright trace.`);
throw new Error(`Could not load trace from ${traceUrl}. Make sure a valid Playwright Trace is accessible over this url.`);
if (
error?.message?.includes('Cannot find .trace file') &&
(await traceModel.hasEntry('index.html'))
) {
throw new Error(
'Could not load trace. Did you upload a Playwright HTML report instead? Make sure to extract the archive first and then double-click the index.html file or put it on a web server.'
);
}
const snapshotServer = new SnapshotServer(traceModel.storage(), sha1 => traceModel.resourceForSha1(sha1));
if (error instanceof TraceVersionError) {
throw new Error(
`Could not load trace from ${traceFileName || traceUrl}. ${
error.message
}`
);
}
if (traceFileName) {
throw new Error(
`Could not load trace from ${traceFileName}. Make sure to upload a valid Playwright trace.`
);
}
throw new Error(
`Could not load trace from ${traceUrl}. Make sure a valid Playwright Trace is accessible over this url.`
);
}
const snapshotServer = new SnapshotServer(traceModel.storage(), sha1 =>
traceModel.resourceForSha1(sha1)
);
loadedTraces.set(traceUrl, { traceModel, snapshotServer });
return traceModel;
}
@ -98,28 +157,43 @@ async function doFetch(event: FetchEvent): Promise<Response> {
return new Response(null, { status: 200 });
}
const traceUrl = url.searchParams.get('trace');
let traceUrl = '';
try {
traceUrl = atob(url.searchParams.get('trace') ?? '');
} catch (error) {
traceUrl = url.searchParams.get('trace') ?? '';
}
if (relativePath === '/contexts') {
try {
const limit = url.searchParams.has('limit') ? +url.searchParams.get('limit')! : undefined;
const traceModel = await loadTrace(traceUrl!, url.searchParams.get('traceFileName'), client, limit, (done: number, total: number) => {
const limit = url.searchParams.has('limit')
? +url.searchParams.get('limit')!
: undefined;
const traceModel = await loadTrace(
traceUrl!,
url.searchParams.get('traceFileName'),
client,
limit,
(done: number, total: number) => {
client.postMessage({ method: 'progress', params: { done, total } });
});
}
);
return new Response(JSON.stringify(traceModel!.contextEntries), {
status: 200,
headers: { 'Content-Type': 'application/json' }
headers: { 'Content-Type': 'application/json' },
});
} catch (error: any) {
return new Response(JSON.stringify({ error: error?.message }), {
status: 500,
headers: { 'Content-Type': 'application/json' }
headers: { 'Content-Type': 'application/json' },
});
}
}
if (relativePath.startsWith('/snapshotInfo/')) {
const { snapshotServer } = loadedTraces.get(traceUrl!) || {};
if (!snapshotServer)
return new Response(null, { status: 404 });
return snapshotServer.serveSnapshotInfo(relativePath, url.searchParams);
@ -129,9 +203,17 @@ async function doFetch(event: FetchEvent): Promise<Response> {
const { snapshotServer } = loadedTraces.get(traceUrl!) || {};
if (!snapshotServer)
return new Response(null, { status: 404 });
const response = snapshotServer.serveSnapshot(relativePath, url.searchParams, url.href);
if (isDeployedAsHttps)
response.headers.set('Content-Security-Policy', 'upgrade-insecure-requests');
const response = snapshotServer.serveSnapshot(
relativePath,
url.searchParams,
url.href
);
if (isDeployedAsHttps) {
response.headers.set(
'Content-Security-Policy',
'upgrade-insecure-requests'
);
}
return response;
}
@ -139,7 +221,10 @@ async function doFetch(event: FetchEvent): Promise<Response> {
const { snapshotServer } = loadedTraces.get(traceUrl!) || {};
if (!snapshotServer)
return new Response(null, { status: 404 });
return snapshotServer.serveClosestScreenshot(relativePath, url.searchParams);
return snapshotServer.serveClosestScreenshot(
relativePath,
url.searchParams
);
}
if (relativePath.startsWith('/sha1/')) {
@ -147,15 +232,21 @@ async function doFetch(event: FetchEvent): Promise<Response> {
const sha1 = relativePath.slice('/sha1/'.length);
for (const trace of loadedTraces.values()) {
const blob = await trace.traceModel.resourceForSha1(sha1);
if (blob)
return new Response(blob, { status: 200, headers: downloadHeaders(url.searchParams) });
if (blob) {
return new Response(blob, {
status: 200,
headers: downloadHeaders(url.searchParams),
});
}
}
return new Response(null, { status: 404 });
}
if (relativePath.startsWith('/file/')) {
const path = url.searchParams.get('path')!;
const traceViewerServer = clientIdToTraceUrls.get(event.clientId ?? '')?.traceViewerServer;
const traceViewerServer = clientIdToTraceUrls.get(
event.clientId ?? ''
)?.traceViewerServer;
if (!traceViewerServer)
throw new Error('client is not initialized');
const response = await traceViewerServer.readFile(path);
@ -186,7 +277,12 @@ function downloadHeaders(searchParams: URLSearchParams): Headers | undefined {
if (!name)
return;
const headers = new Headers();
headers.set('Content-Disposition', `attachment; filename="attachment"; filename*=UTF-8''${encodeURIComponent(name)}`);
headers.set(
'Content-Disposition',
`attachment; filename="attachment"; filename*=UTF-8''${encodeURIComponent(
name
)}`
);
if (contentType)
headers.set('Content-Type', contentType);
return headers;
@ -214,6 +310,7 @@ async function gc() {
if (!usedTraces.has(traceUrl))
loadedTraces.delete(traceUrl);
}
}
// @ts-ignore

View file

@ -14,6 +14,7 @@
limitations under the License.
*/
import type { Language } from '@isomorphic/locatorGenerators';
import type * as actionTypes from '@recorder/actions';
import { SourceChooser } from '@web/components/sourceChooser';
import { SplitView } from '@web/components/splitView';
@ -31,12 +32,10 @@ import type * as modelUtil from '../modelUtil';
import type { SourceLocation } from '../modelUtil';
import { NetworkTab, useNetworkTabModel } from '../networkTab';
import { collectSnapshots, extendSnapshot, SnapshotView } from '../snapshotTab';
import { SourceTab } from '../sourceTab';
import { ModelContext, ModelProvider } from './modelContext';
import './recorderView.css';
import { ActionListView } from './actionListView';
import { BackendContext, BackendProvider } from './backendContext';
import type { Language } from '@isomorphic/locatorGenerators';
import { ModelContext, ModelProvider } from './modelContext';
import './recorderView.css';
export const RecorderView: React.FunctionComponent = () => {
const searchParams = new URLSearchParams(window.location.search);
@ -219,15 +218,15 @@ const PropertiesView: React.FunctionComponent<{
setHighlightedLocator={setHighlightedLocator} />,
};
const sourceTab: TabbedPaneTabModel = {
id: 'source',
title: 'Source',
render: () => <SourceTab
sources={sourceModel.current}
stackFrameLocation={'right'}
fallbackLocation={sourceLocation}
/>
};
// const sourceTab: TabbedPaneTabModel = {
// id: 'source',
// title: 'Source',
// render: () => <SourceTab
// sources={sourceModel.current}
// stackFrameLocation={'right'}
// fallbackLocation={sourceLocation}
// />
// };
const consoleTab: TabbedPaneTabModel = {
id: 'console',
title: 'Console',
@ -242,7 +241,6 @@ const PropertiesView: React.FunctionComponent<{
};
const tabs: TabbedPaneTabModel[] = [
sourceTab,
inspectorTab,
consoleTab,
networkTab,
@ -283,6 +281,7 @@ const TraceView: React.FunctionComponent<{
return snapshot ? extendSnapshot(snapshot) : undefined;
}, [snapshot]);
return <SnapshotView
sdkLanguage={sdkLanguage}
testIdAttributeName='data-testid'

View file

@ -14,22 +14,22 @@
* limitations under the License.
*/
import './snapshotTab.css';
import * as React from 'react';
import ConsoleAPI from '@injected/consoleApi';
import { InjectedScript } from '@injected/injectedScript';
import { Recorder } from '@injected/recorder/recorder';
import type { Language } from '@isomorphic/locatorGenerators';
import { asLocator } from '@isomorphic/locatorGenerators';
import { locatorOrSelectorAsSelector } from '@isomorphic/locatorParser';
import type { ElementInfo } from '@recorder/recorderTypes';
import type { ActionTraceEvent } from '@trace/trace';
import { context, type MultiTraceModel, prevInList } from './modelUtil';
import { TabbedPaneTab } from '@web/components/tabbedPane';
import { Toolbar } from '@web/components/toolbar';
import { ToolbarButton } from '@web/components/toolbarButton';
import { clsx, useMeasure } from '@web/uiUtils';
import { InjectedScript } from '@injected/injectedScript';
import { Recorder } from '@injected/recorder/recorder';
import ConsoleAPI from '@injected/consoleApi';
import { asLocator } from '@isomorphic/locatorGenerators';
import type { Language } from '@isomorphic/locatorGenerators';
import { locatorOrSelectorAsSelector } from '@isomorphic/locatorParser';
import { TabbedPaneTab } from '@web/components/tabbedPane';
import * as React from 'react';
import { BrowserFrame } from './browserFrame';
import type { ElementInfo } from '@recorder/recorderTypes';
import { context, type MultiTraceModel, prevInList } from './modelUtil';
import './snapshotTab.css';
export const SnapshotTabsView: React.FunctionComponent<{
action: ActionTraceEvent | undefined,
@ -329,7 +329,7 @@ const serverParam = new URLSearchParams(window.location.search).get('server');
export function extendSnapshot(snapshot: Snapshot): SnapshotUrls {
const params = new URLSearchParams();
params.set('trace', context(snapshot.action).traceUrl);
params.set('trace', btoa(context(snapshot.action).traceUrl));
params.set('name', snapshot.snapshotName);
if (isUnderTest)
params.set('isUnderTest', 'true');
@ -360,6 +360,7 @@ export async function fetchSnapshotInfo(snapshotInfoUrl: string | undefined) {
const result = { url: '', viewport: kDefaultViewport, timestamp: undefined, wallTime: undefined };
if (snapshotInfoUrl) {
const response = await fetch(snapshotInfoUrl);
const info = await response.json();
if (!info.error) {
result.url = info.url;

View file

@ -14,18 +14,18 @@
* limitations under the License.
*/
import type { StackFrame } from '@protocol/channels';
import type { SourceHighlight } from '@web/components/codeMirrorWrapper';
import { CodeMirrorWrapper } from '@web/components/codeMirrorWrapper';
import { SplitView } from '@web/components/splitView';
import * as React from 'react';
import { Toolbar } from '@web/components/toolbar';
import { ToolbarButton } from '@web/components/toolbarButton';
import { useAsyncMemo } from '@web/uiUtils';
import * as React from 'react';
import { CopyToClipboard } from './copyToClipboard';
import type { SourceLocation, SourceModel } from './modelUtil';
import './sourceTab.css';
import { StackTraceView } from './stackTrace';
import { CodeMirrorWrapper } from '@web/components/codeMirrorWrapper';
import type { SourceHighlight } from '@web/components/codeMirrorWrapper';
import type { SourceLocation, SourceModel } from './modelUtil';
import type { StackFrame } from '@protocol/channels';
import { CopyToClipboard } from './copyToClipboard';
import { ToolbarButton } from '@web/components/toolbarButton';
import { Toolbar } from '@web/components/toolbar';
export const SourceTab: React.FunctionComponent<{
stack?: StackFrame[],
@ -98,7 +98,7 @@ export const SourceTab: React.FunctionComponent<{
const showStackFrames = (stack?.length ?? 0) > 1;
const shortFileName = getFileName(fileName);
return null;
return <SplitView
sidebarSize={200}
orientation={stackFrameLocation === 'bottom' ? 'vertical' : 'horizontal'}

View file

@ -14,34 +14,33 @@
limitations under the License.
*/
import type { Entry } from '@trace/har';
import type { AfterActionTraceEventAttachment } from '@trace/trace';
import { SplitView } from '@web/components/splitView';
import type { TabbedPaneTabModel } from '@web/components/tabbedPane';
import { TabbedPane } from '@web/components/tabbedPane';
import { ToolbarButton } from '@web/components/toolbarButton';
import { clsx, msToString, useSetting } from '@web/uiUtils';
import * as React from 'react';
import { ActionList } from './actionList';
import { AnnotationsTab } from './annotationsTab';
import { AttachmentsTab } from './attachmentsTab';
import { CallTab } from './callTab';
import { LogTab } from './logTab';
import { ErrorsTab, useErrorsTabModel } from './errorsTab';
import type { ErrorDescription } from './errorsTab';
import type { ConsoleEntry } from './consoleTab';
import { ConsoleTab, useConsoleTabModel } from './consoleTab';
import type { ErrorDescription } from './errorsTab';
import { ErrorsTab, useErrorsTabModel } from './errorsTab';
import type { Boundaries } from './geometry';
import { InspectorTab } from './inspectorTab';
import { LogTab } from './logTab';
import { MetadataView } from './metadataView';
import type * as modelUtil from './modelUtil';
import { NetworkTab, useNetworkTabModel } from './networkTab';
import { SnapshotTabsView } from './snapshotTab';
import { SourceTab } from './sourceTab';
import { TabbedPane } from '@web/components/tabbedPane';
import type { TabbedPaneTabModel } from '@web/components/tabbedPane';
import { Timeline } from './timeline';
import { MetadataView } from './metadataView';
import { AttachmentsTab } from './attachmentsTab';
import { AnnotationsTab } from './annotationsTab';
import type { Boundaries } from './geometry';
import { InspectorTab } from './inspectorTab';
import { ToolbarButton } from '@web/components/toolbarButton';
import { useSetting, msToString, clsx } from '@web/uiUtils';
import type { Entry } from '@trace/har';
import './workbench.css';
import { testStatusIcon, testStatusText } from './testUtils';
import type { UITestStatus } from './testUtils';
import type { AfterActionTraceEventAttachment } from '@trace/trace';
import { testStatusIcon, testStatusText } from './testUtils';
import { Timeline } from './timeline';
import './workbench.css';
export const Workbench: React.FunctionComponent<{
model?: modelUtil.MultiTraceModel,
@ -202,19 +201,19 @@ export const Workbench: React.FunctionComponent<{
if (!selectedAction && fallbackLocation)
fallbackSourceErrorCount = fallbackLocation.source?.errors.length;
const sourceTab: TabbedPaneTabModel = {
id: 'source',
title: 'Source',
errorCount: fallbackSourceErrorCount,
render: () => <SourceTab
stack={revealedStack}
sources={sources}
rootDir={rootDir}
stackFrameLocation={sidebarLocation === 'bottom' ? 'right' : 'bottom'}
fallbackLocation={fallbackLocation}
onOpenExternally={onOpenExternally}
/>
};
// const sourceTab: TabbedPaneTabModel = {
// id: 'source',
// title: 'Source',
// errorCount: fallbackSourceErrorCount,
// render: () => <SourceTab
// stack={revealedStack}
// sources={sources}
// rootDir={rootDir}
// stackFrameLocation={sidebarLocation === 'bottom' ? 'right' : 'bottom'}
// fallbackLocation={fallbackLocation}
// onOpenExternally={onOpenExternally}
// />
// };
const consoleTab: TabbedPaneTabModel = {
id: 'console',
title: 'Console',
@ -247,7 +246,6 @@ export const Workbench: React.FunctionComponent<{
errorsTab,
consoleTab,
networkTab,
sourceTab,
attachmentsTab,
];

View file

@ -73,6 +73,28 @@ body.dark-mode .drop-target {
z-index: 10;
}
.loading-inset {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.8);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.spinner {
width: 50px;
height: 50px;
border: 5px solid rgba(255, 255, 255, 0.3);
border-top-color: white;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.inner-progress {
background-color: var(--vscode-progressBar-background);
height: 100%;

View file

@ -14,14 +14,14 @@
limitations under the License.
*/
import { TestServerConnection, WebSocketTestServerTransport } from '@testIsomorphic/testServerConnection';
import { ToolbarButton } from '@web/components/toolbarButton';
import { toggleTheme } from '@web/theme';
import * as React from 'react';
import type { ContextEntry } from '../types/entries';
import { MultiTraceModel } from './modelUtil';
import './workbenchLoader.css';
import { toggleTheme } from '@web/theme';
import { Workbench } from './workbench';
import { TestServerConnection, WebSocketTestServerTransport } from '@testIsomorphic/testServerConnection';
import './workbenchLoader.css';
export const WorkbenchLoader: React.FunctionComponent<{
}> = () => {
@ -166,6 +166,9 @@ export const WorkbenchLoader: React.FunctionComponent<{
<div className='progress'>
<div className='inner-progress' style={{ width: progress.total ? (100 * progress.done / progress.total) + '%' : 0 }}></div>
</div>
{progress.done < progress.total && <div className='loading-inset'>
<div className='spinner'></div>
</div>}
<Workbench model={model} inert={showFileUploadDropArea} />
{fileForLocalModeError && <div className='drop-target'>
<div>Trace Viewer uses Service Workers to show traces. To view trace:</div>