chore: encapsulate parsed snapshot id in the trace viewer (#5607)

This commit is contained in:
Pavel Feldman 2021-02-24 19:29:16 -08:00 committed by GitHub
parent ca8998b11e
commit f72b098a04
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 74 additions and 75 deletions

View file

@ -411,9 +411,11 @@ export class Frame extends SdkObject {
private _setContentCounter = 0; private _setContentCounter = 0;
readonly _detachedPromise: Promise<void>; readonly _detachedPromise: Promise<void>;
private _detachedCallback = () => {}; private _detachedCallback = () => {};
readonly traceId: string;
constructor(page: Page, id: string, parentFrame: Frame | null) { constructor(page: Page, id: string, parentFrame: Frame | null) {
super(page); super(page);
this.traceId = parentFrame ? `frame@${id}` : page.traceId;
this.attribution.frame = this; this.attribution.frame = this;
this._id = id; this._id = id;
this._page = page; this._page = page;

View file

@ -28,7 +28,7 @@ import { ConsoleMessage } from './console';
import * as accessibility from './accessibility'; import * as accessibility from './accessibility';
import { FileChooser } from './fileChooser'; import { FileChooser } from './fileChooser';
import { ProgressController } from './progress'; import { ProgressController } from './progress';
import { assert, isError } from '../utils/utils'; import { assert, createGuid, isError } from '../utils/utils';
import { debugLogger } from '../utils/debugLogger'; import { debugLogger } from '../utils/debugLogger';
import { Selectors } from './selectors'; import { Selectors } from './selectors';
import { CallMetadata, SdkObject } from './instrumentation'; import { CallMetadata, SdkObject } from './instrumentation';
@ -147,9 +147,11 @@ export class Page extends SdkObject {
_ownedContext: BrowserContext | undefined; _ownedContext: BrowserContext | undefined;
readonly selectors: Selectors; readonly selectors: Selectors;
_video: Video | null = null; _video: Video | null = null;
readonly traceId: string;
constructor(delegate: PageDelegate, browserContext: BrowserContext) { constructor(delegate: PageDelegate, browserContext: BrowserContext) {
super(browserContext); super(browserContext);
this.traceId = 'page@' + createGuid();
this.attribution.page = this; this.attribution.page = this;
this._delegate = delegate; this._delegate = delegate;
this._closedCallback = () => {}; this._closedCallback = () => {};

View file

@ -46,8 +46,6 @@ export interface SnapshotterDelegate {
onBlob(blob: SnapshotterBlob): void; onBlob(blob: SnapshotterBlob): void;
onResource(resource: SnapshotterResource): void; onResource(resource: SnapshotterResource): void;
onFrameSnapshot(frame: Frame, frameUrl: string, snapshot: FrameSnapshot, snapshotId?: string): void; onFrameSnapshot(frame: Frame, frameUrl: string, snapshot: FrameSnapshot, snapshotId?: string): void;
pageId(page: Page): string;
frameId(frame: Frame): string;
} }
export class Snapshotter { export class Snapshotter {
@ -116,7 +114,7 @@ export class Snapshotter {
const context = await parent._mainContext(); const context = await parent._mainContext();
await context.evaluateInternal(({ kSnapshotStreamer, frameElement, frameId }) => { await context.evaluateInternal(({ kSnapshotStreamer, frameElement, frameId }) => {
(window as any)[kSnapshotStreamer].markIframe(frameElement, frameId); (window as any)[kSnapshotStreamer].markIframe(frameElement, frameId);
}, { kSnapshotStreamer, frameElement, frameId: this._delegate.frameId(frame) }); }, { kSnapshotStreamer, frameElement, frameId: frame.traceId });
frameElement.dispose(); frameElement.dispose();
} catch (e) { } catch (e) {
// Ignore // Ignore
@ -149,8 +147,8 @@ export class Snapshotter {
const body = await response.body().catch(e => debugLogger.log('error', e)); const body = await response.body().catch(e => debugLogger.log('error', e));
const responseSha1 = body ? calculateSha1(body) : 'none'; const responseSha1 = body ? calculateSha1(body) : 'none';
const resource: SnapshotterResource = { const resource: SnapshotterResource = {
pageId: this._delegate.pageId(page), pageId: page.traceId,
frameId: this._delegate.frameId(response.frame()), frameId: response.frame().traceId,
url, url,
contentType, contentType,
responseHeaders: response.headers(), responseHeaders: response.headers(),

View file

@ -68,7 +68,6 @@ export class Tracer implements InstrumentationListener {
} }
} }
const pageIdSymbol = Symbol('pageId');
const snapshotsSymbol = Symbol('snapshots'); const snapshotsSymbol = Symbol('snapshots');
// This is an official way to pass snapshots between onBefore/AfterInputAction and onAfterCall. // This is an official way to pass snapshots between onBefore/AfterInputAction and onAfterCall.
@ -79,7 +78,6 @@ function snapshotsForMetadata(metadata: CallMetadata): { name: string, snapshotI
} }
class ContextTracer implements SnapshotterDelegate { class ContextTracer implements SnapshotterDelegate {
private _context: BrowserContext;
private _contextId: string; private _contextId: string;
private _traceStoragePromise: Promise<string>; private _traceStoragePromise: Promise<string>;
private _appendEventChain: Promise<string>; private _appendEventChain: Promise<string>;
@ -90,7 +88,6 @@ class ContextTracer implements SnapshotterDelegate {
private _traceFile: string; private _traceFile: string;
constructor(context: BrowserContext, traceStorageDir: string, traceFile: string) { constructor(context: BrowserContext, traceStorageDir: string, traceFile: string) {
this._context = context;
this._contextId = 'context@' + createGuid(); this._contextId = 'context@' + createGuid();
this._traceFile = traceFile; this._traceFile = traceFile;
this._traceStoragePromise = mkdirIfNeeded(path.join(traceStorageDir, 'sha1')).then(() => traceStorageDir); this._traceStoragePromise = mkdirIfNeeded(path.join(traceStorageDir, 'sha1')).then(() => traceStorageDir);
@ -143,8 +140,8 @@ class ContextTracer implements SnapshotterDelegate {
timestamp: monotonicTime(), timestamp: monotonicTime(),
type: 'snapshot', type: 'snapshot',
contextId: this._contextId, contextId: this._contextId,
pageId: this.pageId(frame._page), pageId: frame._page.traceId,
frameId: this.frameId(frame), frameId: frame.traceId,
snapshot: snapshot, snapshot: snapshot,
frameUrl, frameUrl,
snapshotId, snapshotId,
@ -152,14 +149,6 @@ class ContextTracer implements SnapshotterDelegate {
this._appendTraceEvent(event); this._appendTraceEvent(event);
} }
pageId(page: Page): string {
return (page as any)[pageIdSymbol];
}
frameId(frame: Frame): string {
return frame._page.mainFrame() === frame ? this.pageId(frame._page) : frame._id;
}
async onActionCheckpoint(name: string, sdkObject: SdkObject, metadata: CallMetadata): Promise<void> { async onActionCheckpoint(name: string, sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
if (!sdkObject.attribution.page) if (!sdkObject.attribution.page)
return; return;
@ -175,7 +164,7 @@ class ContextTracer implements SnapshotterDelegate {
timestamp: monotonicTime(), timestamp: monotonicTime(),
type: 'action', type: 'action',
contextId: this._contextId, contextId: this._contextId,
pageId: this.pageId(sdkObject.attribution.page), pageId: sdkObject.attribution.page.traceId,
objectType: metadata.type, objectType: metadata.type,
method: metadata.method, method: metadata.method,
// FIXME: filter out evaluation snippets, binary // FIXME: filter out evaluation snippets, binary
@ -191,8 +180,7 @@ class ContextTracer implements SnapshotterDelegate {
} }
private _onPage(page: Page) { private _onPage(page: Page) {
const pageId = 'page@' + createGuid(); const pageId = page.traceId;
(page as any)[pageIdSymbol] = pageId;
const event: trace.PageCreatedTraceEvent = { const event: trace.PageCreatedTraceEvent = {
timestamp: monotonicTime(), timestamp: monotonicTime(),

View file

@ -18,20 +18,25 @@ import * as http from 'http';
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import querystring from 'querystring'; import querystring from 'querystring';
import type { TraceModel } from './traceModel';
import { TraceServer } from './traceServer'; import { TraceServer } from './traceServer';
import type { SerializedFrameSnapshot } from './frameSnapshot'; import type { FrameSnapshot, SerializedFrameSnapshot } from './frameSnapshot';
import type { NetworkResourceTraceEvent } from '../common/traceEvents';
export interface SnapshotStorage {
resourceById(resourceId: string): NetworkResourceTraceEvent;
snapshotByName(snapshotName: string): FrameSnapshot | undefined;
}
export class SnapshotServer { export class SnapshotServer {
private _resourcesDir: string | undefined; private _resourcesDir: string | undefined;
private _server: TraceServer; private _urlPrefix: string;
private _traceModel: TraceModel; private _snapshotStorage: SnapshotStorage;
constructor(server: TraceServer, traceModel: TraceModel, resourcesDir: string | undefined) { constructor(server: TraceServer, snapshotStorage: SnapshotStorage, resourcesDir: string | undefined) {
this._resourcesDir = resourcesDir; this._resourcesDir = resourcesDir;
this._server = server; this._urlPrefix = server.urlPrefix();
this._snapshotStorage = snapshotStorage;
this._traceModel = traceModel;
server.routePath('/snapshot/', this._serveSnapshotRoot.bind(this), true); server.routePath('/snapshot/', this._serveSnapshotRoot.bind(this), true);
server.routePath('/snapshot/service-worker.js', this._serveServiceWorker.bind(this)); server.routePath('/snapshot/service-worker.js', this._serveServiceWorker.bind(this));
server.routePath('/snapshot-data', this._serveSnapshot.bind(this)); server.routePath('/snapshot-data', this._serveSnapshot.bind(this));
@ -39,15 +44,15 @@ export class SnapshotServer {
} }
snapshotRootUrl() { snapshotRootUrl() {
return this._server.urlPrefix() + '/snapshot/'; return this._urlPrefix + '/snapshot/';
} }
snapshotUrl(pageId: string, snapshotId?: string, timestamp?: number) { snapshotUrl(pageId: string, snapshotId?: string, timestamp?: number) {
// Prefer snapshotId over timestamp. // Prefer snapshotId over timestamp.
if (snapshotId) if (snapshotId)
return this._server.urlPrefix() + `/snapshot/pageId/${pageId}/snapshotId/${snapshotId}/main`; return this._urlPrefix + `/snapshot/pageId/${pageId}/snapshotId/${snapshotId}/main`;
if (timestamp) if (timestamp)
return this._server.urlPrefix() + `/snapshot/pageId/${pageId}/timestamp/${timestamp}/main`; return this._urlPrefix + `/snapshot/pageId/${pageId}/timestamp/${timestamp}/main`;
return 'data:text/html,Snapshot is not available'; return 'data:text/html,Snapshot is not available';
} }
@ -114,25 +119,6 @@ export class SnapshotServer {
event.waitUntil(self.clients.claim()); event.waitUntil(self.clients.claim());
}); });
function parseUrl(urlString: string): { pageId: string, frameId: string, timestamp?: number, snapshotId?: string } {
const url = new URL(urlString);
const parts = url.pathname.split('/');
if (!parts[0])
parts.shift();
if (!parts[parts.length - 1])
parts.pop();
// - /snapshot/pageId/<pageId>/snapshotId/<snapshotId>/<frameId>
// - /snapshot/pageId/<pageId>/timestamp/<timestamp>/<frameId>
if (parts.length !== 6 || parts[0] !== 'snapshot' || parts[1] !== 'pageId' || (parts[3] !== 'snapshotId' && parts[3] !== 'timestamp'))
throw new Error(`Unexpected url "${urlString}"`);
return {
pageId: parts[2],
frameId: parts[5] === 'main' ? parts[2] : parts[5],
snapshotId: (parts[3] === 'snapshotId' ? parts[4] : undefined),
timestamp: (parts[3] === 'timestamp' ? +parts[4] : undefined),
};
}
function respond404(): Response { function respond404(): Response {
return new Response(null, { status: 404 }); return new Response(null, { status: 404 });
} }
@ -152,33 +138,29 @@ export class SnapshotServer {
} }
async function doFetch(event: any /* FetchEvent */): Promise<Response> { async function doFetch(event: any /* FetchEvent */): Promise<Response> {
try {
const pathname = new URL(event.request.url).pathname;
if (pathname === '/snapshot/service-worker.js' || pathname === '/snapshot/')
return fetch(event.request);
} catch (e) {
}
const request = event.request; const request = event.request;
let parsed: { pageId: string, frameId: string, timestamp?: number, snapshotId?: string }; const pathname = new URL(request.url).pathname;
if (pathname === '/snapshot/service-worker.js' || pathname === '/snapshot/')
return fetch(event.request);
let snapshotId: string;
if (request.mode === 'navigate') { if (request.mode === 'navigate') {
parsed = parseUrl(request.url); snapshotId = pathname;
} else { } else {
const client = (await self.clients.get(event.clientId))!; const client = (await self.clients.get(event.clientId))!;
parsed = parseUrl(client.url); snapshotId = new URL(client.url).pathname;
} }
if (request.mode === 'navigate') { if (request.mode === 'navigate') {
const htmlResponse = await fetch(`/snapshot-data?pageId=${parsed.pageId}&snapshotId=${parsed.snapshotId || ''}&timestamp=${parsed.timestamp || ''}&frameId=${parsed.frameId || ''}`); const htmlResponse = await fetch(`/snapshot-data?snapshotName=${snapshotId}`);
const { html, resources }: SerializedFrameSnapshot = await htmlResponse.json(); const { html, resources }: SerializedFrameSnapshot = await htmlResponse.json();
if (!html) if (!html)
return respondNotAvailable(); return respondNotAvailable();
snapshotResources.set(parsed.snapshotId + '@' + parsed.timestamp, resources); snapshotResources.set(snapshotId, resources);
const response = new Response(html, { status: 200, headers: { 'Content-Type': 'text/html' } }); const response = new Response(html, { status: 200, headers: { 'Content-Type': 'text/html' } });
return response; return response;
} }
const resources = snapshotResources.get(parsed.snapshotId + '@' + parsed.timestamp)!; const resources = snapshotResources.get(snapshotId)!;
const urlWithoutHash = removeHash(request.url); const urlWithoutHash = removeHash(request.url);
const resource = resources[urlWithoutHash]; const resource = resources[urlWithoutHash];
if (!resource) if (!resource)
@ -223,9 +205,7 @@ export class SnapshotServer {
response.setHeader('Cache-Control', 'public, max-age=31536000'); response.setHeader('Cache-Control', 'public, max-age=31536000');
response.setHeader('Content-Type', 'application/json'); response.setHeader('Content-Type', 'application/json');
const parsed: any = querystring.parse(request.url!.substring(request.url!.indexOf('?') + 1)); const parsed: any = querystring.parse(request.url!.substring(request.url!.indexOf('?') + 1));
const snapshot = parsed.snapshotId ? const snapshot = this._snapshotStorage.snapshotByName(parsed.snapshotName);
this._traceModel.findSnapshotById(parsed.pageId, parsed.frameId, parsed.snapshotId) :
this._traceModel.findSnapshotByTime(parsed.pageId, parsed.frameId, parsed.timestamp!);
const snapshotData: any = snapshot ? snapshot.serialize() : { html: '' }; const snapshotData: any = snapshot ? snapshot.serialize() : { html: '' };
response.end(JSON.stringify(snapshotData)); response.end(JSON.stringify(snapshotData));
return true; return true;
@ -256,9 +236,7 @@ export class SnapshotServer {
return false; return false;
} }
const resource = this._traceModel.resourceById.get(resourceId); const resource = this._snapshotStorage.resourceById(resourceId);
if (!resource)
return false;
const sha1 = overrideSha1 || resource.responseSha1; const sha1 = overrideSha1 || resource.responseSha1;
try { try {
const content = fs.readFileSync(path.join(this._resourcesDir, sha1)); const content = fs.readFileSync(path.join(this._resourcesDir, sha1));

View file

@ -20,9 +20,10 @@ import * as playwright from '../../../..';
import * as util from 'util'; import * as util from 'util';
import { ScreenshotGenerator } from './screenshotGenerator'; import { ScreenshotGenerator } from './screenshotGenerator';
import { TraceModel } from './traceModel'; import { TraceModel } from './traceModel';
import type { TraceEvent } from '../common/traceEvents'; import { NetworkResourceTraceEvent, TraceEvent } from '../common/traceEvents';
import { SnapshotServer } from './snapshotServer'; import { SnapshotServer, SnapshotStorage } from './snapshotServer';
import { ServerRouteHandler, TraceServer } from './traceServer'; import { ServerRouteHandler, TraceServer } from './traceServer';
import { FrameSnapshot } from './frameSnapshot';
const fsReadFileAsync = util.promisify(fs.readFile.bind(fs)); const fsReadFileAsync = util.promisify(fs.readFile.bind(fs));
@ -33,7 +34,7 @@ type TraceViewerDocument = {
const emptyModel: TraceModel = new TraceModel(); const emptyModel: TraceModel = new TraceModel();
class TraceViewer { class TraceViewer implements SnapshotStorage {
private _document: TraceViewerDocument | undefined; private _document: TraceViewerDocument | undefined;
async load(traceDir: string) { async load(traceDir: string) {
@ -74,7 +75,7 @@ class TraceViewer {
// and translates them into "/resources/<resourceId>". // and translates them into "/resources/<resourceId>".
const server = new TraceServer(this._document ? this._document.model : emptyModel); const server = new TraceServer(this._document ? this._document.model : emptyModel);
const snapshotServer = new SnapshotServer(server, this._document ? this._document.model : emptyModel, this._document ? this._document.resourcesDir : undefined); const snapshotServer = new SnapshotServer(server, this, this._document ? this._document.resourcesDir : undefined);
const screenshotGenerator = this._document ? new ScreenshotGenerator(snapshotServer, this._document.resourcesDir, this._document.model) : undefined; const screenshotGenerator = this._document ? new ScreenshotGenerator(snapshotServer, this._document.resourcesDir, this._document.model) : undefined;
const traceViewerHandler: ServerRouteHandler = (request, response) => { const traceViewerHandler: ServerRouteHandler = (request, response) => {
@ -132,6 +133,18 @@ class TraceViewer {
uiPage.on('close', () => process.exit(0)); uiPage.on('close', () => process.exit(0));
await uiPage.goto(urlPrefix + '/traceviewer/traceViewer/index.html'); await uiPage.goto(urlPrefix + '/traceviewer/traceViewer/index.html');
} }
resourceById(resourceId: string): NetworkResourceTraceEvent {
const traceModel = this._document!.model;
return traceModel.resourceById.get(resourceId)!;
}
snapshotByName(snapshotName: string): FrameSnapshot | undefined {
const traceModel = this._document!.model;
const parsed = parseSnapshotName(snapshotName);
const snapshot = parsed.snapshotId ? traceModel.findSnapshotById(parsed.pageId, parsed.frameId, parsed.snapshotId) : traceModel.findSnapshotByTime(parsed.pageId, parsed.frameId, parsed.timestamp!);
return snapshot;
}
} }
export async function showTraceViewer(traceDir: string) { export async function showTraceViewer(traceDir: string) {
@ -140,3 +153,21 @@ export async function showTraceViewer(traceDir: string) {
await traceViewer.load(traceDir); await traceViewer.load(traceDir);
await traceViewer.show(); await traceViewer.show();
} }
function parseSnapshotName(pathname: string): { pageId: string, frameId: string, timestamp?: number, snapshotId?: string } {
const parts = pathname.split('/');
if (!parts[0])
parts.shift();
if (!parts[parts.length - 1])
parts.pop();
// - /snapshot/pageId/<pageId>/snapshotId/<snapshotId>/<frameId>
// - /snapshot/pageId/<pageId>/timestamp/<timestamp>/<frameId>
if (parts.length !== 6 || parts[0] !== 'snapshot' || parts[1] !== 'pageId' || (parts[3] !== 'snapshotId' && parts[3] !== 'timestamp'))
throw new Error(`Unexpected path "${pathname}"`);
return {
pageId: parts[2],
frameId: parts[5] === 'main' ? parts[2] : parts[5],
snapshotId: (parts[3] === 'snapshotId' ? parts[4] : undefined),
timestamp: (parts[3] === 'timestamp' ? +parts[4] : undefined),
};
}