chore: make trace server generic (#5616)
This commit is contained in:
parent
1cd398e700
commit
af89ab7a6f
|
|
@ -37,7 +37,6 @@ export type ContextCreatedTraceEvent = {
|
||||||
isMobile: boolean,
|
isMobile: boolean,
|
||||||
viewportSize?: { width: number, height: number },
|
viewportSize?: { width: number, height: number },
|
||||||
debugName?: string,
|
debugName?: string,
|
||||||
snapshotScript: string,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ContextDestroyedTraceEvent = {
|
export type ContextDestroyedTraceEvent = {
|
||||||
|
|
|
||||||
|
|
@ -427,59 +427,3 @@ export function frameSnapshotStreamer() {
|
||||||
|
|
||||||
(window as any)[kSnapshotStreamer] = new Streamer();
|
(window as any)[kSnapshotStreamer] = new Streamer();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function snapshotScript() {
|
|
||||||
function applyPlaywrightAttributes(shadowAttribute: string, scrollTopAttribute: string, scrollLeftAttribute: string) {
|
|
||||||
const scrollTops: Element[] = [];
|
|
||||||
const scrollLefts: Element[] = [];
|
|
||||||
|
|
||||||
const visit = (root: Document | ShadowRoot) => {
|
|
||||||
// Collect all scrolled elements for later use.
|
|
||||||
for (const e of root.querySelectorAll(`[${scrollTopAttribute}]`))
|
|
||||||
scrollTops.push(e);
|
|
||||||
for (const e of root.querySelectorAll(`[${scrollLeftAttribute}]`))
|
|
||||||
scrollLefts.push(e);
|
|
||||||
|
|
||||||
for (const iframe of root.querySelectorAll('iframe')) {
|
|
||||||
const src = iframe.getAttribute('src') || '';
|
|
||||||
if (src.startsWith('data:text/html'))
|
|
||||||
continue;
|
|
||||||
// Rewrite iframes to use snapshot url (relative to window.location)
|
|
||||||
// instead of begin relative to the <base> tag.
|
|
||||||
const index = location.pathname.lastIndexOf('/');
|
|
||||||
if (index === -1)
|
|
||||||
continue;
|
|
||||||
const pathname = location.pathname.substring(0, index + 1) + src;
|
|
||||||
const href = location.href.substring(0, location.href.indexOf(location.pathname)) + pathname;
|
|
||||||
iframe.setAttribute('src', href);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const element of root.querySelectorAll(`template[${shadowAttribute}]`)) {
|
|
||||||
const template = element as HTMLTemplateElement;
|
|
||||||
const shadowRoot = template.parentElement!.attachShadow({ mode: 'open' });
|
|
||||||
shadowRoot.appendChild(template.content);
|
|
||||||
template.remove();
|
|
||||||
visit(shadowRoot);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
visit(document);
|
|
||||||
|
|
||||||
const onLoad = () => {
|
|
||||||
window.removeEventListener('load', onLoad);
|
|
||||||
for (const element of scrollTops) {
|
|
||||||
element.scrollTop = +element.getAttribute(scrollTopAttribute)!;
|
|
||||||
element.removeAttribute(scrollTopAttribute);
|
|
||||||
}
|
|
||||||
for (const element of scrollLefts) {
|
|
||||||
element.scrollLeft = +element.getAttribute(scrollLeftAttribute)!;
|
|
||||||
element.removeAttribute(scrollLeftAttribute);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
window.addEventListener('load', onLoad);
|
|
||||||
}
|
|
||||||
|
|
||||||
const kShadowAttribute = '__playwright_shadow_root_';
|
|
||||||
const kScrollTopAttribute = '__playwright_scroll_top_';
|
|
||||||
const kScrollLeftAttribute = '__playwright_scroll_left_';
|
|
||||||
return `\n(${applyPlaywrightAttributes.toString()})('${kShadowAttribute}', '${kScrollTopAttribute}', '${kScrollLeftAttribute}')`;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,6 @@ import { Snapshotter } from './snapshotter';
|
||||||
import { helper, RegisteredListener } from '../../helper';
|
import { helper, RegisteredListener } from '../../helper';
|
||||||
import { Dialog } from '../../dialog';
|
import { Dialog } from '../../dialog';
|
||||||
import { Frame, NavigationEvent } from '../../frames';
|
import { Frame, NavigationEvent } from '../../frames';
|
||||||
import { snapshotScript } from './snapshotterInjected';
|
|
||||||
import { CallMetadata, InstrumentationListener, SdkObject } from '../../instrumentation';
|
import { CallMetadata, InstrumentationListener, SdkObject } from '../../instrumentation';
|
||||||
|
|
||||||
const fsWriteFileAsync = util.promisify(fs.writeFile.bind(fs));
|
const fsWriteFileAsync = util.promisify(fs.writeFile.bind(fs));
|
||||||
|
|
@ -102,7 +101,6 @@ class ContextTracer implements SnapshotterDelegate {
|
||||||
deviceScaleFactor: context._options.deviceScaleFactor || 1,
|
deviceScaleFactor: context._options.deviceScaleFactor || 1,
|
||||||
viewportSize: context._options.viewport || undefined,
|
viewportSize: context._options.viewport || undefined,
|
||||||
debugName: context._options._debugName,
|
debugName: context._options._debugName,
|
||||||
snapshotScript: snapshotScript(),
|
|
||||||
};
|
};
|
||||||
this._appendTraceEvent(event);
|
this._appendTraceEvent(event);
|
||||||
this._snapshotter = new Snapshotter(context, this);
|
this._snapshotter = new Snapshotter(context, this);
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as trace from '../common/traceEvents';
|
import * as trace from '../common/traceEvents';
|
||||||
import { ContextEntry, ContextResources } from './traceModel';
|
import { ContextResources } from './traceModel';
|
||||||
export * as trace from '../common/traceEvents';
|
export * as trace from '../common/traceEvents';
|
||||||
|
|
||||||
export type SerializedFrameSnapshot = {
|
export type SerializedFrameSnapshot = {
|
||||||
|
|
@ -26,13 +26,11 @@ export type SerializedFrameSnapshot = {
|
||||||
export class FrameSnapshot {
|
export class FrameSnapshot {
|
||||||
private _snapshots: trace.FrameSnapshotTraceEvent[];
|
private _snapshots: trace.FrameSnapshotTraceEvent[];
|
||||||
private _index: number;
|
private _index: number;
|
||||||
private _contextEntry: ContextEntry;
|
|
||||||
private _contextResources: ContextResources;
|
private _contextResources: ContextResources;
|
||||||
private _frameId: string;
|
private _frameId: string;
|
||||||
|
|
||||||
constructor(frameId: string, contextEntry: ContextEntry, contextResources: ContextResources, events: trace.FrameSnapshotTraceEvent[], index: number) {
|
constructor(frameId: string, contextResources: ContextResources, events: trace.FrameSnapshotTraceEvent[], index: number) {
|
||||||
this._frameId = frameId;
|
this._frameId = frameId;
|
||||||
this._contextEntry = contextEntry;
|
|
||||||
this._contextResources = contextResources;
|
this._contextResources = contextResources;
|
||||||
this._snapshots = events;
|
this._snapshots = events;
|
||||||
this._index = index;
|
this._index = index;
|
||||||
|
|
@ -82,7 +80,7 @@ export class FrameSnapshot {
|
||||||
let html = visit(snapshot.html, this._index);
|
let html = visit(snapshot.html, this._index);
|
||||||
if (snapshot.doctype)
|
if (snapshot.doctype)
|
||||||
html = `<!DOCTYPE ${snapshot.doctype}>` + html;
|
html = `<!DOCTYPE ${snapshot.doctype}>` + html;
|
||||||
html += `<script>${this._contextEntry.created.snapshotScript}</script>`;
|
html += `<script>${snapshotScript}</script>`;
|
||||||
|
|
||||||
const resources: { [key: string]: { resourceId: string, sha1?: string } } = {};
|
const resources: { [key: string]: { resourceId: string, sha1?: string } } = {};
|
||||||
for (const [url, contextResources] of this._contextResources) {
|
for (const [url, contextResources] of this._contextResources) {
|
||||||
|
|
@ -125,3 +123,59 @@ function snapshotNodes(snapshot: trace.FrameSnapshot): trace.NodeSnapshot[] {
|
||||||
}
|
}
|
||||||
return (snapshot as any)._nodes;
|
return (snapshot as any)._nodes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function snapshotScript() {
|
||||||
|
function applyPlaywrightAttributes(shadowAttribute: string, scrollTopAttribute: string, scrollLeftAttribute: string) {
|
||||||
|
const scrollTops: Element[] = [];
|
||||||
|
const scrollLefts: Element[] = [];
|
||||||
|
|
||||||
|
const visit = (root: Document | ShadowRoot) => {
|
||||||
|
// Collect all scrolled elements for later use.
|
||||||
|
for (const e of root.querySelectorAll(`[${scrollTopAttribute}]`))
|
||||||
|
scrollTops.push(e);
|
||||||
|
for (const e of root.querySelectorAll(`[${scrollLeftAttribute}]`))
|
||||||
|
scrollLefts.push(e);
|
||||||
|
|
||||||
|
for (const iframe of root.querySelectorAll('iframe')) {
|
||||||
|
const src = iframe.getAttribute('src') || '';
|
||||||
|
if (src.startsWith('data:text/html'))
|
||||||
|
continue;
|
||||||
|
// Rewrite iframes to use snapshot url (relative to window.location)
|
||||||
|
// instead of begin relative to the <base> tag.
|
||||||
|
const index = location.pathname.lastIndexOf('/');
|
||||||
|
if (index === -1)
|
||||||
|
continue;
|
||||||
|
const pathname = location.pathname.substring(0, index + 1) + src;
|
||||||
|
const href = location.href.substring(0, location.href.indexOf(location.pathname)) + pathname;
|
||||||
|
iframe.setAttribute('src', href);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const element of root.querySelectorAll(`template[${shadowAttribute}]`)) {
|
||||||
|
const template = element as HTMLTemplateElement;
|
||||||
|
const shadowRoot = template.parentElement!.attachShadow({ mode: 'open' });
|
||||||
|
shadowRoot.appendChild(template.content);
|
||||||
|
template.remove();
|
||||||
|
visit(shadowRoot);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
visit(document);
|
||||||
|
|
||||||
|
const onLoad = () => {
|
||||||
|
window.removeEventListener('load', onLoad);
|
||||||
|
for (const element of scrollTops) {
|
||||||
|
element.scrollTop = +element.getAttribute(scrollTopAttribute)!;
|
||||||
|
element.removeAttribute(scrollTopAttribute);
|
||||||
|
}
|
||||||
|
for (const element of scrollLefts) {
|
||||||
|
element.scrollLeft = +element.getAttribute(scrollLeftAttribute)!;
|
||||||
|
element.removeAttribute(scrollLeftAttribute);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('load', onLoad);
|
||||||
|
}
|
||||||
|
|
||||||
|
const kShadowAttribute = '__playwright_shadow_root_';
|
||||||
|
const kScrollTopAttribute = '__playwright_scroll_top_';
|
||||||
|
const kScrollLeftAttribute = '__playwright_scroll_left_';
|
||||||
|
return `\n(${applyPlaywrightAttributes.toString()})('${kShadowAttribute}', '${kScrollTopAttribute}', '${kScrollLeftAttribute}')`;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,25 +15,22 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as http from 'http';
|
import * as http from 'http';
|
||||||
import fs from 'fs';
|
|
||||||
import path from 'path';
|
|
||||||
import querystring from 'querystring';
|
import querystring from 'querystring';
|
||||||
import { TraceServer } from './traceServer';
|
|
||||||
import type { FrameSnapshot, SerializedFrameSnapshot } from './frameSnapshot';
|
|
||||||
import type { NetworkResourceTraceEvent } from '../common/traceEvents';
|
import type { NetworkResourceTraceEvent } from '../common/traceEvents';
|
||||||
|
import type { FrameSnapshot, SerializedFrameSnapshot } from './frameSnapshot';
|
||||||
|
import { HttpServer } from '../../../utils/httpServer';
|
||||||
|
|
||||||
export interface SnapshotStorage {
|
export interface SnapshotStorage {
|
||||||
|
resourceContent(sha1: string): Buffer;
|
||||||
resourceById(resourceId: string): NetworkResourceTraceEvent;
|
resourceById(resourceId: string): NetworkResourceTraceEvent;
|
||||||
snapshotByName(snapshotName: string): FrameSnapshot | undefined;
|
snapshotByName(snapshotName: string): FrameSnapshot | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SnapshotServer {
|
export class SnapshotServer {
|
||||||
private _resourcesDir: string | undefined;
|
|
||||||
private _urlPrefix: string;
|
private _urlPrefix: string;
|
||||||
private _snapshotStorage: SnapshotStorage;
|
private _snapshotStorage: SnapshotStorage;
|
||||||
|
|
||||||
constructor(server: TraceServer, snapshotStorage: SnapshotStorage, resourcesDir: string | undefined) {
|
constructor(server: HttpServer, snapshotStorage: SnapshotStorage) {
|
||||||
this._resourcesDir = resourcesDir;
|
|
||||||
this._urlPrefix = server.urlPrefix();
|
this._urlPrefix = server.urlPrefix();
|
||||||
this._snapshotStorage = snapshotStorage;
|
this._snapshotStorage = snapshotStorage;
|
||||||
|
|
||||||
|
|
@ -212,9 +209,6 @@ export class SnapshotServer {
|
||||||
}
|
}
|
||||||
|
|
||||||
private _serveResource(request: http.IncomingMessage, response: http.ServerResponse): boolean {
|
private _serveResource(request: http.IncomingMessage, response: http.ServerResponse): boolean {
|
||||||
if (!this._resourcesDir)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// - /resources/<resourceId>
|
// - /resources/<resourceId>
|
||||||
// - /resources/<resourceId>/override/<overrideSha1>
|
// - /resources/<resourceId>/override/<overrideSha1>
|
||||||
const parts = request.url!.split('/');
|
const parts = request.url!.split('/');
|
||||||
|
|
@ -239,7 +233,7 @@ export class SnapshotServer {
|
||||||
const resource = this._snapshotStorage.resourceById(resourceId);
|
const resource = this._snapshotStorage.resourceById(resourceId);
|
||||||
const sha1 = overrideSha1 || resource.responseSha1;
|
const sha1 = overrideSha1 || resource.responseSha1;
|
||||||
try {
|
try {
|
||||||
const content = fs.readFileSync(path.join(this._resourcesDir, sha1));
|
const content = this._snapshotStorage.resourceContent(sha1);
|
||||||
response.statusCode = 200;
|
response.statusCode = 200;
|
||||||
let contentType = resource.contentType;
|
let contentType = resource.contentType;
|
||||||
const isTextEncoding = /^text\/|^application\/(javascript|json)/.test(contentType);
|
const isTextEncoding = /^text\/|^application\/(javascript|json)/.test(contentType);
|
||||||
|
|
|
||||||
|
|
@ -157,7 +157,7 @@ export class TraceModel {
|
||||||
const frameSnapshots = pageEntry.snapshotsByFrameId[frameId];
|
const frameSnapshots = pageEntry.snapshotsByFrameId[frameId];
|
||||||
for (let index = 0; index < frameSnapshots.length; index++) {
|
for (let index = 0; index < frameSnapshots.length; index++) {
|
||||||
if (frameSnapshots[index].snapshotId === snapshotId)
|
if (frameSnapshots[index].snapshotId === snapshotId)
|
||||||
return new FrameSnapshot(frameId, contextEntry, this.contextResources.get(contextEntry.created.contextId)!, frameSnapshots, index);
|
return new FrameSnapshot(frameId, this.contextResources.get(contextEntry.created.contextId)!, frameSnapshots, index);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -170,7 +170,7 @@ export class TraceModel {
|
||||||
if (timestamp && snapshot.timestamp <= timestamp)
|
if (timestamp && snapshot.timestamp <= timestamp)
|
||||||
snapshotIndex = index;
|
snapshotIndex = index;
|
||||||
}
|
}
|
||||||
return snapshotIndex >= 0 ? new FrameSnapshot(frameId, contextEntry, this.contextResources.get(contextEntry.created.contextId)!, frameSnapshots, snapshotIndex) : undefined;
|
return snapshotIndex >= 0 ? new FrameSnapshot(frameId, this.contextResources.get(contextEntry.created.contextId)!, frameSnapshots, snapshotIndex) : undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ import { ScreenshotGenerator } from './screenshotGenerator';
|
||||||
import { TraceModel } from './traceModel';
|
import { TraceModel } from './traceModel';
|
||||||
import { NetworkResourceTraceEvent, TraceEvent } from '../common/traceEvents';
|
import { NetworkResourceTraceEvent, TraceEvent } from '../common/traceEvents';
|
||||||
import { SnapshotServer, SnapshotStorage } from './snapshotServer';
|
import { SnapshotServer, SnapshotStorage } from './snapshotServer';
|
||||||
import { ServerRouteHandler, TraceServer } from './traceServer';
|
import { ServerRouteHandler, HttpServer } from '../../../utils/httpServer';
|
||||||
import { FrameSnapshot } from './frameSnapshot';
|
import { FrameSnapshot } from './frameSnapshot';
|
||||||
|
|
||||||
const fsReadFileAsync = util.promisify(fs.readFile.bind(fs));
|
const fsReadFileAsync = util.promisify(fs.readFile.bind(fs));
|
||||||
|
|
@ -32,8 +32,6 @@ type TraceViewerDocument = {
|
||||||
model: TraceModel;
|
model: TraceModel;
|
||||||
};
|
};
|
||||||
|
|
||||||
const emptyModel: TraceModel = new TraceModel();
|
|
||||||
|
|
||||||
class TraceViewer implements SnapshotStorage {
|
class TraceViewer implements SnapshotStorage {
|
||||||
private _document: TraceViewerDocument | undefined;
|
private _document: TraceViewerDocument | undefined;
|
||||||
|
|
||||||
|
|
@ -74,8 +72,17 @@ class TraceViewer implements SnapshotStorage {
|
||||||
// - "/snapshot/service-worker.js" - service worker that intercepts snapshot resources
|
// - "/snapshot/service-worker.js" - service worker that intercepts snapshot resources
|
||||||
// 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 HttpServer();
|
||||||
const snapshotServer = new SnapshotServer(server, this, this._document ? this._document.resourcesDir : undefined);
|
|
||||||
|
const traceModelHandler: ServerRouteHandler = (request, response) => {
|
||||||
|
response.statusCode = 200;
|
||||||
|
response.setHeader('Content-Type', 'application/json');
|
||||||
|
response.end(JSON.stringify(Array.from(this._document!.model.contextEntries.values())));
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
server.routePath('/contexts', traceModelHandler);
|
||||||
|
|
||||||
|
const snapshotServer = new SnapshotServer(server, this);
|
||||||
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) => {
|
||||||
|
|
@ -145,6 +152,10 @@ class TraceViewer implements SnapshotStorage {
|
||||||
const snapshot = parsed.snapshotId ? traceModel.findSnapshotById(parsed.pageId, parsed.frameId, parsed.snapshotId) : traceModel.findSnapshotByTime(parsed.pageId, parsed.frameId, parsed.timestamp!);
|
const snapshot = parsed.snapshotId ? traceModel.findSnapshotById(parsed.pageId, parsed.frameId, parsed.snapshotId) : traceModel.findSnapshotByTime(parsed.pageId, parsed.frameId, parsed.timestamp!);
|
||||||
return snapshot;
|
return snapshot;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resourceContent(sha1: string): Buffer {
|
||||||
|
return fs.readFileSync(path.join(this._document!.resourcesDir, sha1));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function showTraceViewer(traceDir: string) {
|
export async function showTraceViewer(traceDir: string) {
|
||||||
|
|
|
||||||
|
|
@ -17,27 +17,16 @@
|
||||||
import * as http from 'http';
|
import * as http from 'http';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import type { TraceModel } from './traceModel';
|
|
||||||
|
|
||||||
export type ServerRouteHandler = (request: http.IncomingMessage, response: http.ServerResponse) => boolean;
|
export type ServerRouteHandler = (request: http.IncomingMessage, response: http.ServerResponse) => boolean;
|
||||||
|
|
||||||
export class TraceServer {
|
export class HttpServer {
|
||||||
private _traceModel: TraceModel;
|
|
||||||
private _server: http.Server | undefined;
|
private _server: http.Server | undefined;
|
||||||
private _urlPrefix: string;
|
private _urlPrefix: string;
|
||||||
private _routes: { prefix?: string, exact?: string, needsReferrer: boolean, handler: ServerRouteHandler }[] = [];
|
private _routes: { prefix?: string, exact?: string, needsReferrer: boolean, handler: ServerRouteHandler }[] = [];
|
||||||
|
|
||||||
constructor(traceModel: TraceModel) {
|
constructor() {
|
||||||
this._traceModel = traceModel;
|
|
||||||
this._urlPrefix = '';
|
this._urlPrefix = '';
|
||||||
|
|
||||||
const traceModelHandler: ServerRouteHandler = (request, response) => {
|
|
||||||
response.statusCode = 200;
|
|
||||||
response.setHeader('Content-Type', 'application/json');
|
|
||||||
response.end(JSON.stringify(Array.from(this._traceModel.contextEntries.values())));
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
this.routePath('/contexts', traceModelHandler);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
routePrefix(prefix: string, handler: ServerRouteHandler, skipReferrerCheck?: boolean) {
|
routePrefix(prefix: string, handler: ServerRouteHandler, skipReferrerCheck?: boolean) {
|
||||||
Loading…
Reference in a new issue