fix(tracing): error handling (#6888)

- Reject when ZipFile signals an error.
- Make sure snapshotter does not save trace events after stop().
- Await pending blob writes on stop().
This commit is contained in:
Dmitry Gozman 2021-06-04 14:52:16 -07:00 committed by GitHub
parent b5ac393284
commit a83646684a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 26 additions and 19 deletions

View file

@ -116,7 +116,7 @@ export class Snapshotter {
const snapshots = page.frames().map(async frame => { const snapshots = page.frames().map(async frame => {
const data = await frame.nonStallingRawEvaluateInExistingMainContext(expression).catch(e => debugLogger.log('error', e)) as SnapshotData; const data = await frame.nonStallingRawEvaluateInExistingMainContext(expression).catch(e => debugLogger.log('error', e)) as SnapshotData;
// Something went wrong -> bail out, our snapshots are best-efforty. // Something went wrong -> bail out, our snapshots are best-efforty.
if (!data) if (!data || !this._started)
return; return;
const snapshot: FrameSnapshot = { const snapshot: FrameSnapshot = {

View file

@ -48,6 +48,7 @@ export class TraceSnapshotter extends EventEmitter implements SnapshotterDelegat
async stop(): Promise<void> { async stop(): Promise<void> {
await this._snapshotter.stop(); await this._snapshotter.stop();
await this._writeArtifactChain;
} }
async dispose() { async dispose() {

View file

@ -17,6 +17,7 @@
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import yazl from 'yazl'; import yazl from 'yazl';
import { EventEmitter } from 'events';
import { calculateSha1, createGuid, mkdirIfNeeded, monotonicTime } from '../../../utils/utils'; import { calculateSha1, createGuid, mkdirIfNeeded, monotonicTime } from '../../../utils/utils';
import { Artifact } from '../../artifact'; import { Artifact } from '../../artifact';
import { BrowserContext } from '../../browserContext'; import { BrowserContext } from '../../browserContext';
@ -43,7 +44,7 @@ export class Tracing implements InstrumentationListener {
private _resourcesDir: string; private _resourcesDir: string;
private _sha1s: string[] = []; private _sha1s: string[] = [];
private _started = false; private _started = false;
private _tracesDir: string | undefined; private _tracesDir: string;
constructor(context: BrowserContext) { constructor(context: BrowserContext) {
this._context = context; this._context = context;
@ -54,8 +55,6 @@ export class Tracing implements InstrumentationListener {
async start(options: TracerOptions): Promise<void> { async start(options: TracerOptions): Promise<void> {
// context + page must be the first events added, this method can't have awaits before them. // context + page must be the first events added, this method can't have awaits before them.
if (!this._tracesDir)
throw new Error('Tracing directory is not specified when launching the browser');
if (this._started) if (this._started)
throw new Error('Tracing has already been started'); throw new Error('Tracing has already been started');
this._started = true; this._started = true;
@ -86,13 +85,13 @@ export class Tracing implements InstrumentationListener {
if (!this._started) if (!this._started)
return; return;
this._started = false; this._started = false;
await this._snapshotter.stop();
this._context.instrumentation.removeListener(this); this._context.instrumentation.removeListener(this);
helper.removeEventListeners(this._eventListeners); helper.removeEventListeners(this._eventListeners);
for (const { sdkObject, metadata } of this._pendingCalls.values()) for (const { sdkObject, metadata } of this._pendingCalls.values())
await this.onAfterCall(sdkObject, metadata); await this.onAfterCall(sdkObject, metadata);
for (const page of this._context.pages()) for (const page of this._context.pages())
page.setScreencastOptions(null); page.setScreencastOptions(null);
await this._snapshotter.stop();
// Ensure all writes are finished. // Ensure all writes are finished.
await this._appendEventChain; await this._appendEventChain;
@ -103,21 +102,25 @@ export class Tracing implements InstrumentationListener {
} }
async export(): Promise<Artifact> { async export(): Promise<Artifact> {
if (!this._traceFile) if (!this._traceFile || this._started)
throw new Error('Tracing directory is not specified when launching the browser'); throw new Error('Must start and stop tracing before exporting');
const zipFile = new yazl.ZipFile(); const zipFile = new yazl.ZipFile();
zipFile.addFile(this._traceFile, 'trace.trace'); const failedPromise = new Promise<Artifact>((_, reject) => (zipFile as any as EventEmitter).on('error', reject));
const zipFileName = this._traceFile + '.zip';
this._traceFile = undefined; const succeededPromise = new Promise<Artifact>(async fulfill => {
for (const sha1 of this._sha1s) zipFile.addFile(this._traceFile!, 'trace.trace');
zipFile.addFile(path.join(this._resourcesDir!, sha1), path.join('resources', sha1)); const zipFileName = this._traceFile! + '.zip';
zipFile.end(); for (const sha1 of this._sha1s)
await new Promise(f => { zipFile.addFile(path.join(this._resourcesDir!, sha1), path.join('resources', sha1));
zipFile.outputStream.pipe(fs.createWriteStream(zipFileName)).on('close', f); zipFile.end();
await new Promise(f => {
zipFile.outputStream.pipe(fs.createWriteStream(zipFileName)).on('close', f);
});
const artifact = new Artifact(this._context, zipFileName);
artifact.reportFinished();
fulfill(artifact);
}); });
const artifact = new Artifact(this._context, zipFileName); return Promise.race([failedPromise, succeededPromise]);
artifact.reportFinished();
return artifact;
} }
async _captureSnapshot(name: 'before' | 'after' | 'action' | 'event', sdkObject: SdkObject, metadata: CallMetadata, element?: ElementHandle) { async _captureSnapshot(name: 'before' | 'after' | 'action' | 'event', sdkObject: SdkObject, metadata: CallMetadata, element?: ElementHandle) {
@ -181,6 +184,9 @@ export class Tracing implements InstrumentationListener {
} }
private _appendTraceEvent(event: any) { private _appendTraceEvent(event: any) {
if (!this._started)
return;
const visit = (object: any) => { const visit = (object: any) => {
if (Array.isArray(object)) { if (Array.isArray(object)) {
object.forEach(visit); object.forEach(visit);

View file

@ -38,7 +38,7 @@ test('should collect trace', async ({ context, page, server, browserName }, test
expect(events.some(e => e.type === 'screencast-frame')).toBeTruthy(); expect(events.some(e => e.type === 'screencast-frame')).toBeTruthy();
}); });
test('should collect trace', async ({ context, page, server }, testInfo) => { test('should not collect snapshots by default', async ({ context, page, server }, testInfo) => {
await context.tracing.start(); await context.tracing.start();
await page.goto(server.EMPTY_PAGE); await page.goto(server.EMPTY_PAGE);
await page.setContent('<button>Click</button>'); await page.setContent('<button>Click</button>');