Skip to content

Commit 2c29a94

Browse files
committed
fix(tracing): stop recording websocket frames outside of chunks (#41398)
1 parent 4324b19 commit 2c29a94

4 files changed

Lines changed: 66 additions & 5 deletions

File tree

‎packages/playwright-core/src/server/har/harRecorder.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,8 @@ export class HarRecorder implements HarTracerDelegate {
5757
recordRequestOverrides: true,
5858
waitForContentOnStop: true,
5959
urlFilter: urlFilterRe ?? options.urlGlob,
60-
omitWebSocketFrames: !!process.env.PLAYWRIGHT_HAR_NO_WEBSOCKET_FRAMES,
6160
});
61+
this._tracer.setOmitWebSocketFrames(!!process.env.PLAYWRIGHT_HAR_NO_WEBSOCKET_FRAMES);
6262
this._tracer.start({ omitScripts: false });
6363
}
6464

‎packages/playwright-core/src/server/har/harTracer.ts‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,6 @@ type HarTracerOptions = {
6262
omitPages?: boolean;
6363
omitSizes?: boolean;
6464
omitScripts?: boolean;
65-
omitWebSocketFrames?: boolean;
6665
};
6766

6867
export class HarTracer {
@@ -77,6 +76,7 @@ export class HarTracer {
7776
private _pageEntrySymbol: symbol;
7877
private _baseURL: string | undefined;
7978
private _page: Page | null;
79+
private _omitWebSocketFrames = true;
8080

8181
constructor(context: BrowserContext | APIRequestContext, page: Page | null, delegate: HarTracerDelegate, options: HarTracerOptions) {
8282
this._context = context;
@@ -96,6 +96,10 @@ export class HarTracer {
9696
this._baseURL = context instanceof APIRequestContext ? context._defaultOptions().baseURL : context._options.baseURL;
9797
}
9898

99+
setOmitWebSocketFrames(omitWebSocketFrames: boolean) {
100+
this._omitWebSocketFrames = omitWebSocketFrames;
101+
}
102+
99103
start(options: { omitScripts: boolean }) {
100104
if (this._started)
101105
return;
@@ -443,7 +447,7 @@ export class HarTracer {
443447

444448
let sha1: string | undefined = undefined;
445449
const recordMessage = (type: 'send' | 'receive', opcode: number, data: string, wallTimeMs: number) => {
446-
if (this._options.omitWebSocketFrames)
450+
if (this._omitWebSocketFrames)
447451
return;
448452
const message = { type, time: this._options.omitTiming ? -1 : wallTimeMs, opcode, data };
449453
if (this._options.content === 'embed') {

‎packages/playwright-core/src/server/trace/recorder/tracing.ts‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,6 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
108108
includeTraceInfo: true,
109109
recordRequestOverrides: false,
110110
waitForContentOnStop: false,
111-
omitWebSocketFrames: !!process.env.PLAYWRIGHT_TRACING_NO_WEBSOCKET_FRAMES,
112111
});
113112
const testIdAttributeName = ('selectors' in context) ? context.selectors().testIdAttributeName() : undefined;
114113
this._contextCreatedEvent = {
@@ -223,6 +222,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
223222
);
224223
if (this._state.options.screenshots)
225224
this._startScreencast();
225+
this._harTracer.setOmitWebSocketFrames(!!process.env.PLAYWRIGHT_TRACING_NO_WEBSOCKET_FRAMES);
226226
if (this._state.options.snapshots)
227227
await this._snapshotter?.start(progress);
228228
return { traceName: this._state.traceName };
@@ -394,7 +394,9 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
394394
eventsHelper.removeEventListeners(this._eventListeners);
395395
if (this._state.options.screenshots)
396396
this._stopScreencast();
397-
397+
// We don't need websocket frames outside of the recording window. This also
398+
// stops updating websocket content blobs, which we want to stay unchanged for zipping.
399+
this._harTracer.setOmitWebSocketFrames(true);
398400
if (this._state.options.snapshots)
399401
this._snapshotter?.stop();
400402

‎tests/library/tracing.spec.ts‎

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -857,6 +857,61 @@ test('should not emit after w/o before', async ({ browserType, mode }, testInfo)
857857
expect(call2after).toBe(call2before);
858858
});
859859

860+
test('should save trace while a WebSocket keeps streaming frames', {
861+
annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/41351' }
862+
}, async ({ context, page, server }, testInfo) => {
863+
let streaming = true;
864+
server.onceWebSocketConnection(ws => {
865+
const timer = setInterval(() => {
866+
if (streaming && ws.readyState === ws.OPEN)
867+
ws.send('x'.repeat(16 * 1024), () => {});
868+
}, 1);
869+
const stop = () => clearInterval(timer);
870+
ws.on('close', stop);
871+
ws.on('error', stop);
872+
});
873+
874+
await context.tracing.start({ snapshots: true });
875+
876+
await context.tracing.startChunk();
877+
await page.goto(server.EMPTY_PAGE);
878+
await page.evaluate(url => {
879+
(window as any).ws = new WebSocket(url);
880+
return new Promise<void>(resolve => (window as any).ws.addEventListener('open', () => resolve()));
881+
}, `ws://${server.HOST}/ws`);
882+
await page.waitForTimeout(100);
883+
const tracePath1 = testInfo.outputPath('trace1.zip');
884+
await context.tracing.stopChunk({ path: tracePath1 });
885+
886+
streaming = false;
887+
await context.tracing.startChunk();
888+
await page.waitForTimeout(100);
889+
const tracePath2 = testInfo.outputPath('trace2.zip');
890+
await context.tracing.stopChunk({ path: tracePath2 });
891+
892+
await page.evaluate(() => new Promise<void>(resolve => {
893+
const ws = (window as any).ws as WebSocket;
894+
if (ws.readyState === WebSocket.CLOSED) {
895+
resolve();
896+
return;
897+
}
898+
ws.addEventListener('close', () => resolve(), { once: true });
899+
ws.close();
900+
}));
901+
902+
const webSocketLines = await Promise.all([tracePath1, tracePath2].map(async path => {
903+
const { resources } = await parseTraceRaw(path);
904+
const websocketResource = Array.from(resources).find(([name, buffer]) => name.endsWith('.jsonl'))!;
905+
const lines = websocketResource[1].toString().split('\n').filter(Boolean);
906+
expect(lines.length).toBeGreaterThan(0);
907+
for (const line of lines)
908+
expect(() => JSON.parse(line)).not.toThrow();
909+
return { path, lines };
910+
}));
911+
expect(webSocketLines[0].path).not.toEqual(webSocketLines[1].path);
912+
expect(webSocketLines[1].lines).toEqual(webSocketLines[1].lines);
913+
});
914+
860915
function expectRed(pixels: Buffer, offset: number) {
861916
const r = pixels.readUInt8(offset);
862917
const g = pixels.readUInt8(offset + 1);

0 commit comments

Comments
 (0)