From 7f25d1ac1afaf487b93a08707e2083923dc3faaf Mon Sep 17 00:00:00 2001 From: Edward Jibson Date: Thu, 2 Jan 2025 15:58:35 +0000 Subject: [PATCH] base 64 encode/decode urls with s3 --- packages/trace-viewer/snapshot.html | 2 +- packages/trace-viewer/src/sw/main.ts | 163 ++++++++++++++---- .../src/ui/recorder/recorderView.tsx | 27 ++- packages/trace-viewer/src/ui/snapshotTab.tsx | 25 +-- packages/trace-viewer/src/ui/sourceTab.tsx | 18 +- packages/trace-viewer/src/ui/workbench.tsx | 62 ++++--- .../trace-viewer/src/ui/workbenchLoader.css | 22 +++ .../trace-viewer/src/ui/workbenchLoader.tsx | 9 +- 8 files changed, 224 insertions(+), 104 deletions(-) diff --git a/packages/trace-viewer/snapshot.html b/packages/trace-viewer/snapshot.html index 3f27586af5..3685081fe6 100644 --- a/packages/trace-viewer/snapshot.html +++ b/packages/trace-viewer/snapshot.html @@ -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()); diff --git a/packages/trace-viewer/src/sw/main.ts b/packages/trace-viewer/src/sw/main.ts index 4d01ef2a61..bac616792f 100644 --- a/packages/trace-viewer/src/sw/main.ts +++ b/packages/trace-viewer/src/sw/main.ts @@ -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(); +const loadedTraces = new Map< + string, + { traceModel: TraceModel; snapshotServer: SnapshotServer } +>(); -const clientIdToTraceUrls = new Map, traceViewerServer: TraceViewerServer }>(); +const clientIdToTraceUrls = new Map< + string, + { + limit: number | undefined; + traceUrls: Set; + traceViewerServer: TraceViewerServer; + } +>(); -async function loadTrace(traceUrl: string, traceFileName: string | null, client: any | undefined, limit: number | undefined, progress: (done: number, total: number) => undefined): Promise { +async function loadTrace( + traceUrl: string, + traceFileName: string | null, + client: any | undefined, + limit: number | undefined, + progress: (done: number, total: number) => undefined +): Promise { 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.' + ); + } + 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)); + 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 { 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) => { - client.postMessage({ method: 'progress', params: { done, total } }); - }); + 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 { 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 { 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 { 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 diff --git a/packages/trace-viewer/src/ui/recorder/recorderView.tsx b/packages/trace-viewer/src/ui/recorder/recorderView.tsx index e9014a6cea..b17dfa0768 100644 --- a/packages/trace-viewer/src/ui/recorder/recorderView.tsx +++ b/packages/trace-viewer/src/ui/recorder/recorderView.tsx @@ -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: () => - }; + // const sourceTab: TabbedPaneTabModel = { + // id: 'source', + // title: 'Source', + // render: () => + // }; 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 1; const shortFileName = getFileName(fileName); - + return null; return - }; + // const sourceTab: TabbedPaneTabModel = { + // id: 'source', + // title: 'Source', + // errorCount: fallbackSourceErrorCount, + // render: () => + // }; const consoleTab: TabbedPaneTabModel = { id: 'console', title: 'Console', @@ -247,7 +246,6 @@ export const Workbench: React.FunctionComponent<{ errorsTab, consoleTab, networkTab, - sourceTab, attachmentsTab, ]; diff --git a/packages/trace-viewer/src/ui/workbenchLoader.css b/packages/trace-viewer/src/ui/workbenchLoader.css index 5adb0401ae..7515a939ed 100644 --- a/packages/trace-viewer/src/ui/workbenchLoader.css +++ b/packages/trace-viewer/src/ui/workbenchLoader.css @@ -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%; diff --git a/packages/trace-viewer/src/ui/workbenchLoader.tsx b/packages/trace-viewer/src/ui/workbenchLoader.tsx index 9f3fe83fc4..fd0c269880 100644 --- a/packages/trace-viewer/src/ui/workbenchLoader.tsx +++ b/packages/trace-viewer/src/ui/workbenchLoader.tsx @@ -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<{
+ {progress.done < progress.total &&
+
+
} {fileForLocalModeError &&
Trace Viewer uses Service Workers to show traces. To view trace: