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:
parent
b5ac393284
commit
a83646684a
|
|
@ -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 = {
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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>');
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue