mirror of
				https://github.com/microsoft/playwright.git
				synced 2025-06-26 21:40:17 +00:00 
			
		
		
		
	chore: move logic from sw to server (#5582)
This commit is contained in:
		
							parent
							
								
									070cfdcdb8
								
							
						
					
					
						commit
						5fb77935ee
					
				@ -110,7 +110,7 @@ class Lock {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  async obtain() {
 | 
					  async obtain() {
 | 
				
			||||||
    while (this._workers === this._maxWorkers)
 | 
					    while (this._workers === this._maxWorkers)
 | 
				
			||||||
      await new Promise(f => this._callbacks.push(f));
 | 
					      await new Promise<void>(f => this._callbacks.push(f));
 | 
				
			||||||
    ++this._workers;
 | 
					    ++this._workers;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -17,20 +17,23 @@
 | 
				
			|||||||
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, trace, ContextEntry } from './traceModel';
 | 
					import querystring from 'querystring';
 | 
				
			||||||
 | 
					import type { TraceModel } from './traceModel';
 | 
				
			||||||
 | 
					import * as trace from '../../server/trace/traceTypes';
 | 
				
			||||||
import { TraceServer } from './traceServer';
 | 
					import { TraceServer } from './traceServer';
 | 
				
			||||||
import { NodeSnapshot } from '../../server/trace/traceTypes';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class SnapshotServer {
 | 
					export class SnapshotServer {
 | 
				
			||||||
  private _resourcesDir: string | undefined;
 | 
					  private _resourcesDir: string | undefined;
 | 
				
			||||||
  private _server: TraceServer;
 | 
					  private _server: TraceServer;
 | 
				
			||||||
  private _resourceById: Map<string, trace.NetworkResourceTraceEvent>;
 | 
					  private _resourceById: Map<string, trace.NetworkResourceTraceEvent>;
 | 
				
			||||||
 | 
					  private _traceModel: TraceModel;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  constructor(server: TraceServer, traceModel: TraceModel, resourcesDir: string | undefined) {
 | 
					  constructor(server: TraceServer, traceModel: TraceModel, resourcesDir: string | undefined) {
 | 
				
			||||||
    this._resourcesDir = resourcesDir;
 | 
					    this._resourcesDir = resourcesDir;
 | 
				
			||||||
    this._server = server;
 | 
					    this._server = server;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this._resourceById = new Map();
 | 
					    this._resourceById = new Map();
 | 
				
			||||||
 | 
					    this._traceModel = traceModel;
 | 
				
			||||||
    for (const contextEntry of traceModel.contexts) {
 | 
					    for (const contextEntry of traceModel.contexts) {
 | 
				
			||||||
      for (const pageEntry of contextEntry.pages) {
 | 
					      for (const pageEntry of contextEntry.pages) {
 | 
				
			||||||
        for (const action of pageEntry.actions)
 | 
					        for (const action of pageEntry.actions)
 | 
				
			||||||
@ -41,6 +44,7 @@ export class SnapshotServer {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    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.routePrefix('/resources/', this._serveResource.bind(this));
 | 
					    server.routePrefix('/resources/', this._serveResource.bind(this));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -109,50 +113,93 @@ export class SnapshotServer {
 | 
				
			|||||||
    return true;
 | 
					    return true;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private _serveServiceWorker(request: http.IncomingMessage, response: http.ServerResponse): boolean {
 | 
					  private _frameSnapshotData(parsed: { pageId: string, frameId: string, snapshotId?: string, timestamp?: number }) {
 | 
				
			||||||
    function serviceWorkerMain(self: any /* ServiceWorkerGlobalScope */) {
 | 
					    let contextEntry;
 | 
				
			||||||
      let traceModel: TraceModel;
 | 
					    let pageEntry;
 | 
				
			||||||
 | 
					    for (const c of this._traceModel.contexts) {
 | 
				
			||||||
      type ContextData = {
 | 
					      for (const p of c.pages) {
 | 
				
			||||||
        resourcesByUrl: Map<string, trace.NetworkResourceTraceEvent[]>,
 | 
					        if (p.created.pageId === parsed.pageId) {
 | 
				
			||||||
        overridenUrls: Set<string>
 | 
					          contextEntry = c;
 | 
				
			||||||
      };
 | 
					          pageEntry = p;
 | 
				
			||||||
      const contextToData = new Map<ContextEntry, ContextData>();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      function preprocessModel() {
 | 
					 | 
				
			||||||
        for (const contextEntry of traceModel.contexts) {
 | 
					 | 
				
			||||||
          const contextData: ContextData = {
 | 
					 | 
				
			||||||
            resourcesByUrl: new Map(),
 | 
					 | 
				
			||||||
            overridenUrls: new Set(),
 | 
					 | 
				
			||||||
          };
 | 
					 | 
				
			||||||
          const appendResource = (event: trace.NetworkResourceTraceEvent) => {
 | 
					 | 
				
			||||||
            let responseEvents = contextData.resourcesByUrl.get(event.url);
 | 
					 | 
				
			||||||
            if (!responseEvents) {
 | 
					 | 
				
			||||||
              responseEvents = [];
 | 
					 | 
				
			||||||
              contextData.resourcesByUrl.set(event.url, responseEvents);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            responseEvents.push(event);
 | 
					 | 
				
			||||||
          };
 | 
					 | 
				
			||||||
          for (const pageEntry of contextEntry.pages) {
 | 
					 | 
				
			||||||
            for (const action of pageEntry.actions)
 | 
					 | 
				
			||||||
              action.resources.forEach(appendResource);
 | 
					 | 
				
			||||||
            pageEntry.resources.forEach(appendResource);
 | 
					 | 
				
			||||||
            for (const snapshots of Object.values(pageEntry.snapshotsByFrameId)) {
 | 
					 | 
				
			||||||
              for (const snapshot of snapshots) {
 | 
					 | 
				
			||||||
                for (const { url } of snapshot.snapshot.resourceOverrides)
 | 
					 | 
				
			||||||
                  contextData.overridenUrls.add(url);
 | 
					 | 
				
			||||||
              }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
          contextToData.set(contextEntry, contextData);
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (!contextEntry || !pageEntry)
 | 
				
			||||||
 | 
					      return { html: ''  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const frameSnapshots = pageEntry.snapshotsByFrameId[parsed.frameId] || [];
 | 
				
			||||||
 | 
					    let snapshotIndex = -1;
 | 
				
			||||||
 | 
					    for (let index = 0; index < frameSnapshots.length; index++) {
 | 
				
			||||||
 | 
					      const current = snapshotIndex === -1 ? undefined : frameSnapshots[snapshotIndex];
 | 
				
			||||||
 | 
					      const snapshot = frameSnapshots[index];
 | 
				
			||||||
 | 
					      // Prefer snapshot with exact id.
 | 
				
			||||||
 | 
					      const exactMatch = parsed.snapshotId && snapshot.snapshotId === parsed.snapshotId;
 | 
				
			||||||
 | 
					      const currentExactMatch = current && parsed.snapshotId && current.snapshotId === parsed.snapshotId;
 | 
				
			||||||
 | 
					      // If not available, prefer the latest snapshot before the timestamp.
 | 
				
			||||||
 | 
					      const timestampMatch = parsed.timestamp && snapshot.timestamp <= parsed.timestamp;
 | 
				
			||||||
 | 
					      if (exactMatch || (timestampMatch && !currentExactMatch))
 | 
				
			||||||
 | 
					        snapshotIndex = index;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    let html = this._serializeSnapshot(frameSnapshots, snapshotIndex);
 | 
				
			||||||
 | 
					    html += `<script>${contextEntry.created.snapshotScript}</script>`;
 | 
				
			||||||
 | 
					    const resourcesByUrl = contextEntry.resourcesByUrl;
 | 
				
			||||||
 | 
					    const overridenUrls = contextEntry.overridenUrls;
 | 
				
			||||||
 | 
					    const resourceOverrides: any = {};
 | 
				
			||||||
 | 
					    for (const o of frameSnapshots[snapshotIndex].snapshot.resourceOverrides)
 | 
				
			||||||
 | 
					      resourceOverrides[o.url] = o.sha1;
 | 
				
			||||||
 | 
					    return { html, resourcesByUrl, overridenUrls, resourceOverrides };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private _serializeSnapshot(snapshots: trace.FrameSnapshotTraceEvent[], initialSnapshotIndex: number): string {
 | 
				
			||||||
 | 
					    const visit = (n: trace.NodeSnapshot, snapshotIndex: number): string => {
 | 
				
			||||||
 | 
					      // Text node.
 | 
				
			||||||
 | 
					      if (typeof n === 'string')
 | 
				
			||||||
 | 
					        return escapeText(n);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (!(n as any)._string) {
 | 
				
			||||||
 | 
					        if (Array.isArray(n[0])) {
 | 
				
			||||||
 | 
					          // Node reference.
 | 
				
			||||||
 | 
					          const referenceIndex = snapshotIndex - n[0][0];
 | 
				
			||||||
 | 
					          if (referenceIndex >= 0 && referenceIndex < snapshotIndex) {
 | 
				
			||||||
 | 
					            const nodes = snapshotNodes(snapshots[referenceIndex].snapshot);
 | 
				
			||||||
 | 
					            const nodeIndex = n[0][1];
 | 
				
			||||||
 | 
					            if (nodeIndex >= 0 && nodeIndex < nodes.length)
 | 
				
			||||||
 | 
					              (n as any)._string = visit(nodes[nodeIndex], referenceIndex);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        } else if (typeof n[0] === 'string') {
 | 
				
			||||||
 | 
					          // Element node.
 | 
				
			||||||
 | 
					          const builder: string[] = [];
 | 
				
			||||||
 | 
					          builder.push('<', n[0]);
 | 
				
			||||||
 | 
					          for (const [attr, value] of Object.entries(n[1] || {}))
 | 
				
			||||||
 | 
					            builder.push(' ', attr, '="', escapeAttribute(value as string), '"');
 | 
				
			||||||
 | 
					          builder.push('>');
 | 
				
			||||||
 | 
					          for (let i = 2; i < n.length; i++)
 | 
				
			||||||
 | 
					            builder.push(visit(n[i], snapshotIndex));
 | 
				
			||||||
 | 
					          if (!autoClosing.has(n[0]))
 | 
				
			||||||
 | 
					            builder.push('</', n[0], '>');
 | 
				
			||||||
 | 
					          (n as any)._string = builder.join('');
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          // Why are we here? Let's not throw, just in case.
 | 
				
			||||||
 | 
					          (n as any)._string = '';
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      return (n as any)._string;
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const snapshot = snapshots[initialSnapshotIndex].snapshot;
 | 
				
			||||||
 | 
					    let html = visit(snapshot.html, initialSnapshotIndex);
 | 
				
			||||||
 | 
					    if (snapshot.doctype)
 | 
				
			||||||
 | 
					      html = `<!DOCTYPE ${snapshot.doctype}>` + html;
 | 
				
			||||||
 | 
					    return html;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private _serveServiceWorker(request: http.IncomingMessage, response: http.ServerResponse): boolean {
 | 
				
			||||||
 | 
					    function serviceWorkerMain(self: any /* ServiceWorkerGlobalScope */) {
 | 
				
			||||||
 | 
					      const pageToResourcesByUrl = new Map<string, { [key: string]: { resourceId: string, frameId: string }[] }>();
 | 
				
			||||||
 | 
					      const pageToOverriddenUrls = new Map<string, { [key: string]: boolean }>();
 | 
				
			||||||
 | 
					      const snapshotToResourceOverrides = new Map<string, { [key: string]: string | undefined }>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      self.addEventListener('install', function(event: any) {
 | 
					      self.addEventListener('install', function(event: any) {
 | 
				
			||||||
        event.waitUntil(fetch('/tracemodel').then(async response => {
 | 
					 | 
				
			||||||
          traceModel = await response.json();
 | 
					 | 
				
			||||||
          preprocessModel();
 | 
					 | 
				
			||||||
        }));
 | 
					 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      self.addEventListener('activate', function(event: any) {
 | 
					      self.addEventListener('activate', function(event: any) {
 | 
				
			||||||
@ -172,7 +219,7 @@ export class SnapshotServer {
 | 
				
			|||||||
          throw new Error(`Unexpected url "${urlString}"`);
 | 
					          throw new Error(`Unexpected url "${urlString}"`);
 | 
				
			||||||
        return {
 | 
					        return {
 | 
				
			||||||
          pageId: parts[2],
 | 
					          pageId: parts[2],
 | 
				
			||||||
          frameId: parts[5] === 'main' ? '' : parts[5],
 | 
					          frameId: parts[5] === 'main' ? parts[2] : parts[5],
 | 
				
			||||||
          snapshotId: (parts[3] === 'snapshotId' ? parts[4] : undefined),
 | 
					          snapshotId: (parts[3] === 'snapshotId' ? parts[4] : undefined),
 | 
				
			||||||
          timestamp: (parts[3] === 'timestamp' ? +parts[4] : undefined),
 | 
					          timestamp: (parts[3] === 'timestamp' ? +parts[4] : undefined),
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
@ -196,93 +243,6 @@ export class SnapshotServer {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const autoClosing = new Set(['AREA', 'BASE', 'BR', 'COL', 'COMMAND', 'EMBED', 'HR', 'IMG', 'INPUT', 'KEYGEN', 'LINK', 'MENUITEM', 'META', 'PARAM', 'SOURCE', 'TRACK', 'WBR']);
 | 
					 | 
				
			||||||
      const escaped = { '&': '&', '<': '<', '>': '>', '"': '"', '\'': ''' };
 | 
					 | 
				
			||||||
      function escapeAttribute(s: string): string {
 | 
					 | 
				
			||||||
        return s.replace(/[&<>"']/ug, char => (escaped as any)[char]);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      function escapeText(s: string): string {
 | 
					 | 
				
			||||||
        return s.replace(/[&<]/ug, char => (escaped as any)[char]);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      function snapshotNodes(snapshot: trace.FrameSnapshot): NodeSnapshot[] {
 | 
					 | 
				
			||||||
        if (!(snapshot as any)._nodes) {
 | 
					 | 
				
			||||||
          const nodes: NodeSnapshot[] = [];
 | 
					 | 
				
			||||||
          const visit = (n: trace.NodeSnapshot) => {
 | 
					 | 
				
			||||||
            if (typeof n === 'string') {
 | 
					 | 
				
			||||||
              nodes.push(n);
 | 
					 | 
				
			||||||
            } else if (typeof n[0] === 'string') {
 | 
					 | 
				
			||||||
              for (let i = 2; i < n.length; i++)
 | 
					 | 
				
			||||||
                visit(n[i]);
 | 
					 | 
				
			||||||
              nodes.push(n);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
          };
 | 
					 | 
				
			||||||
          visit(snapshot.html);
 | 
					 | 
				
			||||||
          (snapshot as any)._nodes = nodes;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return (snapshot as any)._nodes;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      function serializeSnapshot(snapshots: trace.FrameSnapshotTraceEvent[], initialSnapshotIndex: number): string {
 | 
					 | 
				
			||||||
        const visit = (n: trace.NodeSnapshot, snapshotIndex: number): string => {
 | 
					 | 
				
			||||||
          // Text node.
 | 
					 | 
				
			||||||
          if (typeof n === 'string')
 | 
					 | 
				
			||||||
            return escapeText(n);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          if (!(n as any)._string) {
 | 
					 | 
				
			||||||
            if (Array.isArray(n[0])) {
 | 
					 | 
				
			||||||
              // Node reference.
 | 
					 | 
				
			||||||
              const referenceIndex = snapshotIndex - n[0][0];
 | 
					 | 
				
			||||||
              if (referenceIndex >= 0 && referenceIndex < snapshotIndex) {
 | 
					 | 
				
			||||||
                const nodes = snapshotNodes(snapshots[referenceIndex].snapshot);
 | 
					 | 
				
			||||||
                const nodeIndex = n[0][1];
 | 
					 | 
				
			||||||
                if (nodeIndex >= 0 && nodeIndex < nodes.length)
 | 
					 | 
				
			||||||
                  (n as any)._string = visit(nodes[nodeIndex], referenceIndex);
 | 
					 | 
				
			||||||
              }
 | 
					 | 
				
			||||||
            } else if (typeof n[0] === 'string') {
 | 
					 | 
				
			||||||
              // Element node.
 | 
					 | 
				
			||||||
              const builder: string[] = [];
 | 
					 | 
				
			||||||
              builder.push('<', n[0]);
 | 
					 | 
				
			||||||
              for (const [attr, value] of Object.entries(n[1] || {}))
 | 
					 | 
				
			||||||
                builder.push(' ', attr, '="', escapeAttribute(value), '"');
 | 
					 | 
				
			||||||
              builder.push('>');
 | 
					 | 
				
			||||||
              for (let i = 2; i < n.length; i++)
 | 
					 | 
				
			||||||
                builder.push(visit(n[i], snapshotIndex));
 | 
					 | 
				
			||||||
              if (!autoClosing.has(n[0]))
 | 
					 | 
				
			||||||
                builder.push('</', n[0], '>');
 | 
					 | 
				
			||||||
              (n as any)._string = builder.join('');
 | 
					 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
              // Why are we here? Let's not throw, just in case.
 | 
					 | 
				
			||||||
              (n as any)._string = '';
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
          return (n as any)._string;
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const snapshot = snapshots[initialSnapshotIndex].snapshot;
 | 
					 | 
				
			||||||
        let html = visit(snapshot.html, initialSnapshotIndex);
 | 
					 | 
				
			||||||
        if (snapshot.doctype)
 | 
					 | 
				
			||||||
          html = `<!DOCTYPE ${snapshot.doctype}>` + html;
 | 
					 | 
				
			||||||
        return html;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      function findResourceOverride(snapshots: trace.FrameSnapshotTraceEvent[], snapshotIndex: number, url: string): string | undefined {
 | 
					 | 
				
			||||||
        while (true) {
 | 
					 | 
				
			||||||
          const snapshot = snapshots[snapshotIndex].snapshot;
 | 
					 | 
				
			||||||
          const override = snapshot.resourceOverrides.find(o => o.url === url);
 | 
					 | 
				
			||||||
          if (!override)
 | 
					 | 
				
			||||||
            return;
 | 
					 | 
				
			||||||
          if (override.sha1 !== undefined)
 | 
					 | 
				
			||||||
            return override.sha1;
 | 
					 | 
				
			||||||
          if (override.ref === undefined)
 | 
					 | 
				
			||||||
            return;
 | 
					 | 
				
			||||||
          const referenceIndex = snapshotIndex - override.ref!;
 | 
					 | 
				
			||||||
          if (referenceIndex < 0 || referenceIndex >= snapshotIndex)
 | 
					 | 
				
			||||||
            return;
 | 
					 | 
				
			||||||
          snapshotIndex = referenceIndex;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      async function doFetch(event: any /* FetchEvent */): Promise<Response> {
 | 
					      async function doFetch(event: any /* FetchEvent */): Promise<Response> {
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
          const pathname = new URL(event.request.url).pathname;
 | 
					          const pathname = new URL(event.request.url).pathname;
 | 
				
			||||||
@ -292,7 +252,7 @@ export class SnapshotServer {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const request = event.request;
 | 
					        const request = event.request;
 | 
				
			||||||
        let parsed;
 | 
					        let parsed: { pageId: string, frameId: string, timestamp?: number, snapshotId?: string };
 | 
				
			||||||
        if (request.mode === 'navigate') {
 | 
					        if (request.mode === 'navigate') {
 | 
				
			||||||
          parsed = parseUrl(request.url);
 | 
					          parsed = parseUrl(request.url);
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
@ -300,58 +260,28 @@ export class SnapshotServer {
 | 
				
			|||||||
          parsed = parseUrl(client.url);
 | 
					          parsed = parseUrl(client.url);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        let contextEntry;
 | 
					 | 
				
			||||||
        let pageEntry;
 | 
					 | 
				
			||||||
        for (const c of traceModel.contexts) {
 | 
					 | 
				
			||||||
          for (const p of c.pages) {
 | 
					 | 
				
			||||||
            if (p.created.pageId === parsed.pageId) {
 | 
					 | 
				
			||||||
              contextEntry = c;
 | 
					 | 
				
			||||||
              pageEntry = p;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        if (!contextEntry || !pageEntry)
 | 
					 | 
				
			||||||
          return request.mode === 'navigate' ? respondNotAvailable() : respond404();
 | 
					 | 
				
			||||||
        const contextData = contextToData.get(contextEntry)!;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const frameSnapshots = pageEntry.snapshotsByFrameId[parsed.frameId] || [];
 | 
					 | 
				
			||||||
        let snapshotIndex = -1;
 | 
					 | 
				
			||||||
        for (let index = 0; index < frameSnapshots.length; index++) {
 | 
					 | 
				
			||||||
          const current = snapshotIndex === -1 ? undefined : frameSnapshots[snapshotIndex];
 | 
					 | 
				
			||||||
          const snapshot = frameSnapshots[index];
 | 
					 | 
				
			||||||
          // Prefer snapshot with exact id.
 | 
					 | 
				
			||||||
          const exactMatch = parsed.snapshotId && snapshot.snapshotId === parsed.snapshotId;
 | 
					 | 
				
			||||||
          const currentExactMatch = current && parsed.snapshotId && current.snapshotId === parsed.snapshotId;
 | 
					 | 
				
			||||||
          // If not available, prefer the latest snapshot before the timestamp.
 | 
					 | 
				
			||||||
          const timestampMatch = parsed.timestamp && snapshot.timestamp <= parsed.timestamp;
 | 
					 | 
				
			||||||
          if (exactMatch || (timestampMatch && !currentExactMatch))
 | 
					 | 
				
			||||||
            snapshotIndex = index;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        const snapshotEvent = snapshotIndex === -1 ? undefined : frameSnapshots[snapshotIndex];
 | 
					 | 
				
			||||||
        if (!snapshotEvent)
 | 
					 | 
				
			||||||
          return request.mode === 'navigate' ? respondNotAvailable() : respond404();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (request.mode === 'navigate') {
 | 
					        if (request.mode === 'navigate') {
 | 
				
			||||||
          let html = serializeSnapshot(frameSnapshots, snapshotIndex);
 | 
					          const htmlResponse = await fetch(`/snapshot-data?pageId=${parsed.pageId}&snapshotId=${parsed.snapshotId}×tamp=${parsed.timestamp}&frameId=${parsed.frameId}`);
 | 
				
			||||||
          html += `<script>${contextEntry.created.snapshotScript}</script>`;
 | 
					          const { html, resourcesByUrl, overridenUrls, resourceOverrides } = await htmlResponse.json();
 | 
				
			||||||
 | 
					          if (!html)
 | 
				
			||||||
 | 
					            return respondNotAvailable();
 | 
				
			||||||
 | 
					          pageToResourcesByUrl.set(parsed.pageId, resourcesByUrl);
 | 
				
			||||||
 | 
					          pageToOverriddenUrls.set(parsed.pageId, overridenUrls);
 | 
				
			||||||
 | 
					          snapshotToResourceOverrides.set(parsed.snapshotId + '@' + parsed.timestamp, resourceOverrides);
 | 
				
			||||||
          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;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        let resource: trace.NetworkResourceTraceEvent | null = null;
 | 
					        const resourcesByUrl = pageToResourcesByUrl.get(parsed.pageId);
 | 
				
			||||||
 | 
					        const overridenUrls = pageToOverriddenUrls.get(parsed.pageId);
 | 
				
			||||||
 | 
					        const resourceOverrides = snapshotToResourceOverrides.get(parsed.snapshotId + '@' + parsed.timestamp);
 | 
				
			||||||
        const urlWithoutHash = removeHash(request.url);
 | 
					        const urlWithoutHash = removeHash(request.url);
 | 
				
			||||||
        const resourcesWithUrl = contextData.resourcesByUrl.get(urlWithoutHash) || [];
 | 
					        const resourcesWithUrl = resourcesByUrl?.[urlWithoutHash] || [];
 | 
				
			||||||
        for (const resourceEvent of resourcesWithUrl) {
 | 
					        const resource = resourcesWithUrl.find(r => r.frameId === parsed.frameId) || resourcesWithUrl[0];
 | 
				
			||||||
          if (resource && resourceEvent.frameId !== parsed.frameId)
 | 
					 | 
				
			||||||
            continue;
 | 
					 | 
				
			||||||
          resource = resourceEvent;
 | 
					 | 
				
			||||||
          if (resourceEvent.frameId === parsed.frameId)
 | 
					 | 
				
			||||||
            break;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        if (!resource)
 | 
					        if (!resource)
 | 
				
			||||||
          return respond404();
 | 
					          return respond404();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const overrideSha1 = findResourceOverride(frameSnapshots, snapshotIndex, urlWithoutHash);
 | 
					        const overrideSha1 = resourceOverrides?.[urlWithoutHash];
 | 
				
			||||||
        const fetchUrl = overrideSha1 ?
 | 
					        const fetchUrl = overrideSha1 ?
 | 
				
			||||||
          `/resources/${resource.resourceId}/override/${overrideSha1}` :
 | 
					          `/resources/${resource.resourceId}/override/${overrideSha1}` :
 | 
				
			||||||
          `/resources/${resource.resourceId}`;
 | 
					          `/resources/${resource.resourceId}`;
 | 
				
			||||||
@ -362,7 +292,7 @@ export class SnapshotServer {
 | 
				
			|||||||
        // as the original request url.
 | 
					        // as the original request url.
 | 
				
			||||||
        // Response url turns into resource base uri that is used to resolve
 | 
					        // Response url turns into resource base uri that is used to resolve
 | 
				
			||||||
        // relative links, e.g. url(/foo/bar) in style sheets.
 | 
					        // relative links, e.g. url(/foo/bar) in style sheets.
 | 
				
			||||||
        if (contextData.overridenUrls.has(urlWithoutHash)) {
 | 
					        if (overridenUrls?.[urlWithoutHash]) {
 | 
				
			||||||
          // No cache, so that we refetch overridden resources.
 | 
					          // No cache, so that we refetch overridden resources.
 | 
				
			||||||
          headers.set('Cache-Control', 'no-cache');
 | 
					          headers.set('Cache-Control', 'no-cache');
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@ -386,6 +316,16 @@ export class SnapshotServer {
 | 
				
			|||||||
    return true;
 | 
					    return true;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private _serveSnapshot(request: http.IncomingMessage, response: http.ServerResponse): boolean {
 | 
				
			||||||
 | 
					    response.statusCode = 200;
 | 
				
			||||||
 | 
					    response.setHeader('Cache-Control', 'public, max-age=31536000');
 | 
				
			||||||
 | 
					    response.setHeader('Content-Type', 'application/json');
 | 
				
			||||||
 | 
					    const parsed = querystring.parse(request.url!.substring(request.url!.indexOf('?') + 1));
 | 
				
			||||||
 | 
					    const snapshotData = this._frameSnapshotData(parsed as any);
 | 
				
			||||||
 | 
					    response.end(JSON.stringify(snapshotData));
 | 
				
			||||||
 | 
					    return true;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private _serveResource(request: http.IncomingMessage, response: http.ServerResponse): boolean {
 | 
					  private _serveResource(request: http.IncomingMessage, response: http.ServerResponse): boolean {
 | 
				
			||||||
    if (!this._resourcesDir)
 | 
					    if (!this._resourcesDir)
 | 
				
			||||||
      return false;
 | 
					      return false;
 | 
				
			||||||
@ -439,3 +379,31 @@ export class SnapshotServer {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const autoClosing = new Set(['AREA', 'BASE', 'BR', 'COL', 'COMMAND', 'EMBED', 'HR', 'IMG', 'INPUT', 'KEYGEN', 'LINK', 'MENUITEM', 'META', 'PARAM', 'SOURCE', 'TRACK', 'WBR']);
 | 
				
			||||||
 | 
					const escaped = { '&': '&', '<': '<', '>': '>', '"': '"', '\'': ''' };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function escapeAttribute(s: string): string {
 | 
				
			||||||
 | 
					  return s.replace(/[&<>"']/ug, char => (escaped as any)[char]);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					function escapeText(s: string): string {
 | 
				
			||||||
 | 
					  return s.replace(/[&<]/ug, char => (escaped as any)[char]);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function snapshotNodes(snapshot: trace.FrameSnapshot): trace.NodeSnapshot[] {
 | 
				
			||||||
 | 
					  if (!(snapshot as any)._nodes) {
 | 
				
			||||||
 | 
					    const nodes: trace.NodeSnapshot[] = [];
 | 
				
			||||||
 | 
					    const visit = (n: trace.NodeSnapshot) => {
 | 
				
			||||||
 | 
					      if (typeof n === 'string') {
 | 
				
			||||||
 | 
					        nodes.push(n);
 | 
				
			||||||
 | 
					      } else if (typeof n[0] === 'string') {
 | 
				
			||||||
 | 
					        for (let i = 2; i < n.length; i++)
 | 
				
			||||||
 | 
					          visit(n[i]);
 | 
				
			||||||
 | 
					        nodes.push(n);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    visit(snapshot.html);
 | 
				
			||||||
 | 
					    (snapshot as any)._nodes = nodes;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return (snapshot as any)._nodes;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -19,7 +19,7 @@ export * as trace from '../../server/trace/traceTypes';
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export type TraceModel = {
 | 
					export type TraceModel = {
 | 
				
			||||||
  contexts: ContextEntry[];
 | 
					  contexts: ContextEntry[];
 | 
				
			||||||
}
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type ContextEntry = {
 | 
					export type ContextEntry = {
 | 
				
			||||||
  name: string;
 | 
					  name: string;
 | 
				
			||||||
@ -29,6 +29,8 @@ export type ContextEntry = {
 | 
				
			|||||||
  created: trace.ContextCreatedTraceEvent;
 | 
					  created: trace.ContextCreatedTraceEvent;
 | 
				
			||||||
  destroyed: trace.ContextDestroyedTraceEvent;
 | 
					  destroyed: trace.ContextDestroyedTraceEvent;
 | 
				
			||||||
  pages: PageEntry[];
 | 
					  pages: PageEntry[];
 | 
				
			||||||
 | 
					  resourcesByUrl: { [key: string]: { resourceId: string, frameId: string }[] };
 | 
				
			||||||
 | 
					  overridenUrls: { [key: string]: boolean };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type VideoEntry = {
 | 
					export type VideoEntry = {
 | 
				
			||||||
@ -80,6 +82,8 @@ export function readTraceFile(events: trace.TraceEvent[], traceModel: TraceModel
 | 
				
			|||||||
          created: event,
 | 
					          created: event,
 | 
				
			||||||
          destroyed: undefined as any,
 | 
					          destroyed: undefined as any,
 | 
				
			||||||
          pages: [],
 | 
					          pages: [],
 | 
				
			||||||
 | 
					          resourcesByUrl: {},
 | 
				
			||||||
 | 
					          overridenUrls: {}
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
        break;
 | 
					        break;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
@ -155,6 +159,38 @@ export function readTraceFile(events: trace.TraceEvent[], traceModel: TraceModel
 | 
				
			|||||||
    contextEntry.endTime = Math.max(contextEntry.endTime, event.timestamp);
 | 
					    contextEntry.endTime = Math.max(contextEntry.endTime, event.timestamp);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  traceModel.contexts.push(...contextEntries.values());
 | 
					  traceModel.contexts.push(...contextEntries.values());
 | 
				
			||||||
 | 
					  preprocessModel(traceModel);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function preprocessModel(traceModel: TraceModel) {
 | 
				
			||||||
 | 
					  for (const contextEntry of traceModel.contexts) {
 | 
				
			||||||
 | 
					    const appendResource = (event: trace.NetworkResourceTraceEvent) => {
 | 
				
			||||||
 | 
					      let responseEvents = contextEntry.resourcesByUrl[event.url];
 | 
				
			||||||
 | 
					      if (!responseEvents) {
 | 
				
			||||||
 | 
					        responseEvents = [];
 | 
				
			||||||
 | 
					        contextEntry.resourcesByUrl[event.url] = responseEvents;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      responseEvents.push({ frameId: event.frameId, resourceId: event.resourceId });
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    for (const pageEntry of contextEntry.pages) {
 | 
				
			||||||
 | 
					      for (const action of pageEntry.actions)
 | 
				
			||||||
 | 
					        action.resources.forEach(appendResource);
 | 
				
			||||||
 | 
					      pageEntry.resources.forEach(appendResource);
 | 
				
			||||||
 | 
					      for (const snapshots of Object.values(pageEntry.snapshotsByFrameId)) {
 | 
				
			||||||
 | 
					        for (let i = 0; i < snapshots.length; ++i) {
 | 
				
			||||||
 | 
					          const snapshot = snapshots[i];
 | 
				
			||||||
 | 
					          for (const override of snapshot.snapshot.resourceOverrides) {
 | 
				
			||||||
 | 
					            if (override.ref) {
 | 
				
			||||||
 | 
					              const refOverride = snapshots[i - override.ref]?.snapshot.resourceOverrides.find(o => o.url === override.url);
 | 
				
			||||||
 | 
					              override.sha1 = refOverride?.sha1;
 | 
				
			||||||
 | 
					              delete override.ref;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            contextEntry.overridenUrls[override.url] = true;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function actionById(traceModel: TraceModel, actionId: string): { context: ContextEntry, page: PageEntry, action: ActionEntry } {
 | 
					export function actionById(traceModel: TraceModel, actionId: string): { context: ContextEntry, page: PageEntry, action: ActionEntry } {
 | 
				
			||||||
 | 
				
			|||||||
@ -54,8 +54,10 @@ const emptyModel: TraceModel = {
 | 
				
			|||||||
      name: '<empty>',
 | 
					      name: '<empty>',
 | 
				
			||||||
      filePath: '',
 | 
					      filePath: '',
 | 
				
			||||||
      pages: [],
 | 
					      pages: [],
 | 
				
			||||||
 | 
					      resourcesByUrl: {},
 | 
				
			||||||
 | 
					      overridenUrls: {}
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  ]
 | 
					  ],
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class TraceViewer {
 | 
					class TraceViewer {
 | 
				
			||||||
 | 
				
			|||||||
@ -26,7 +26,7 @@ import { FrameSnapshot } from './traceTypes';
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export type SnapshotterResource = {
 | 
					export type SnapshotterResource = {
 | 
				
			||||||
  pageId: string,
 | 
					  pageId: string,
 | 
				
			||||||
  frameId: string,
 | 
					  frameId: string,  // Empty means main frame
 | 
				
			||||||
  url: string,
 | 
					  url: string,
 | 
				
			||||||
  contentType: string,
 | 
					  contentType: string,
 | 
				
			||||||
  responseHeaders: { name: string, value: string }[],
 | 
					  responseHeaders: { name: string, value: string }[],
 | 
				
			||||||
@ -47,6 +47,7 @@ export interface SnapshotterDelegate {
 | 
				
			|||||||
  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;
 | 
					  pageId(page: Page): string;
 | 
				
			||||||
 | 
					  frameId(frame: Frame): string;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class Snapshotter {
 | 
					export class Snapshotter {
 | 
				
			||||||
@ -115,7 +116,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: frame._id });
 | 
					        }, { kSnapshotStreamer, frameElement, frameId: this._delegate.frameId(frame) });
 | 
				
			||||||
        frameElement.dispose();
 | 
					        frameElement.dispose();
 | 
				
			||||||
      } catch (e) {
 | 
					      } catch (e) {
 | 
				
			||||||
        // Ignore
 | 
					        // Ignore
 | 
				
			||||||
@ -149,7 +150,7 @@ export class Snapshotter {
 | 
				
			|||||||
    const responseSha1 = body ? calculateSha1(body) : 'none';
 | 
					    const responseSha1 = body ? calculateSha1(body) : 'none';
 | 
				
			||||||
    const resource: SnapshotterResource = {
 | 
					    const resource: SnapshotterResource = {
 | 
				
			||||||
      pageId: this._delegate.pageId(page),
 | 
					      pageId: this._delegate.pageId(page),
 | 
				
			||||||
      frameId: response.frame()._id,
 | 
					      frameId: this._delegate.frameId(response.frame()),
 | 
				
			||||||
      url,
 | 
					      url,
 | 
				
			||||||
      contentType,
 | 
					      contentType,
 | 
				
			||||||
      responseHeaders: response.headers(),
 | 
					      responseHeaders: response.headers(),
 | 
				
			||||||
 | 
				
			|||||||
@ -129,7 +129,7 @@ export type FrameSnapshotTraceEvent = {
 | 
				
			|||||||
  type: 'snapshot',
 | 
					  type: 'snapshot',
 | 
				
			||||||
  contextId: string,
 | 
					  contextId: string,
 | 
				
			||||||
  pageId: string,
 | 
					  pageId: string,
 | 
				
			||||||
  frameId: string,  // Empty means main frame.
 | 
					  frameId: string,
 | 
				
			||||||
  snapshot: FrameSnapshot,
 | 
					  snapshot: FrameSnapshot,
 | 
				
			||||||
  frameUrl: string,
 | 
					  frameUrl: string,
 | 
				
			||||||
  snapshotId?: string,
 | 
					  snapshotId?: string,
 | 
				
			||||||
@ -149,9 +149,15 @@ export type TraceEvent =
 | 
				
			|||||||
    LoadEvent |
 | 
					    LoadEvent |
 | 
				
			||||||
    FrameSnapshotTraceEvent;
 | 
					    FrameSnapshotTraceEvent;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type ResourceOverride = {
 | 
				
			||||||
 | 
					  url: string,
 | 
				
			||||||
 | 
					  sha1?: string,
 | 
				
			||||||
 | 
					  ref?: number
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type FrameSnapshot = {
 | 
					export type FrameSnapshot = {
 | 
				
			||||||
  doctype?: string,
 | 
					  doctype?: string,
 | 
				
			||||||
  html: NodeSnapshot,
 | 
					  html: NodeSnapshot,
 | 
				
			||||||
  resourceOverrides: { url: string, sha1?: string, ref?: number }[],
 | 
					  resourceOverrides: ResourceOverride[],
 | 
				
			||||||
  viewport: { width: number, height: number },
 | 
					  viewport: { width: number, height: number },
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
@ -144,7 +144,7 @@ class ContextTracer implements SnapshotterDelegate {
 | 
				
			|||||||
      type: 'snapshot',
 | 
					      type: 'snapshot',
 | 
				
			||||||
      contextId: this._contextId,
 | 
					      contextId: this._contextId,
 | 
				
			||||||
      pageId: this.pageId(frame._page),
 | 
					      pageId: this.pageId(frame._page),
 | 
				
			||||||
      frameId: frame._page.mainFrame() === frame ? '' : frame._id,
 | 
					      frameId: this.frameId(frame),
 | 
				
			||||||
      snapshot: snapshot,
 | 
					      snapshot: snapshot,
 | 
				
			||||||
      frameUrl,
 | 
					      frameUrl,
 | 
				
			||||||
      snapshotId,
 | 
					      snapshotId,
 | 
				
			||||||
@ -156,6 +156,10 @@ class ContextTracer implements SnapshotterDelegate {
 | 
				
			|||||||
    return (page as any)[pageIdSymbol];
 | 
					    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;
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user