Skip to content

Commit 92e554b

Browse files
committed
feat(har): include WebSocket in .har
add an entry for each `WebSocket` when generating a `.har` also include data for each sent/received frame in a custom property `_webSocketMessages` (for more info see <https://developer.chrome.com/blog/new-in-devtools-76#websocket>) both Chrome and WebKit already capture the `wallTime` for the initial request and a `timestamp` for each subsequent message (i.e. diff the `timestamp` relative to the initial `timestamp` and add the `wallTime` in order to determine the current `wallTime`) unfortunately Firefox does not have this so fall back to `Date.now() / 1000`
1 parent 08a37f2 commit 92e554b

8 files changed

Lines changed: 283 additions & 31 deletions

File tree

packages/playwright-core/src/server/chromium/crNetworkManager.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,10 +76,10 @@ export class CRNetworkManager {
7676
if (this._page) {
7777
sessionInfo.eventListeners.push(...[
7878
eventsHelper.addEventListener(session, 'Network.webSocketCreated', e => this._page!.frameManager.onWebSocketCreated(e.requestId, e.url)),
79-
eventsHelper.addEventListener(session, 'Network.webSocketWillSendHandshakeRequest', e => this._page!.frameManager.onWebSocketRequest(e.requestId)),
80-
eventsHelper.addEventListener(session, 'Network.webSocketHandshakeResponseReceived', e => this._page!.frameManager.onWebSocketResponse(e.requestId, e.response.status, e.response.statusText)),
81-
eventsHelper.addEventListener(session, 'Network.webSocketFrameSent', e => e.response.payloadData && this._page!.frameManager.onWebSocketFrameSent(e.requestId, e.response.opcode, e.response.payloadData)),
82-
eventsHelper.addEventListener(session, 'Network.webSocketFrameReceived', e => e.response.payloadData && this._page!.frameManager.webSocketFrameReceived(e.requestId, e.response.opcode, e.response.payloadData)),
79+
eventsHelper.addEventListener(session, 'Network.webSocketWillSendHandshakeRequest', e => this._page!.frameManager.onWebSocketRequest(e.requestId, e.wallTime, e.timestamp, headersObjectToArray(e.request.headers, '\n'))),
80+
eventsHelper.addEventListener(session, 'Network.webSocketHandshakeResponseReceived', e => this._page!.frameManager.onWebSocketResponse(e.requestId, e.response.status, e.response.statusText, headersObjectToArray(e.response.headers, '\n'))),
81+
eventsHelper.addEventListener(session, 'Network.webSocketFrameSent', e => e.response.payloadData && this._page!.frameManager.onWebSocketFrameSent(e.requestId, e.response.opcode, e.response.payloadData, e.timestamp)),
82+
eventsHelper.addEventListener(session, 'Network.webSocketFrameReceived', e => e.response.payloadData && this._page!.frameManager.webSocketFrameReceived(e.requestId, e.response.opcode, e.response.payloadData, e.timestamp)),
8383
eventsHelper.addEventListener(session, 'Network.webSocketClosed', e => this._page!.frameManager.webSocketClosed(e.requestId)),
8484
eventsHelper.addEventListener(session, 'Network.webSocketFrameError', e => this._page!.frameManager.webSocketError(e.requestId, e.errorMessage)),
8585
]);

packages/playwright-core/src/server/frames.ts

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -403,30 +403,44 @@ export class FrameManager {
403403
this._webSockets.set(requestId, ws);
404404
}
405405

406-
onWebSocketRequest(requestId: string) {
406+
onWebSocketRequest(requestId: string, wallTime?: number, timestamp?: number, headers?: types.HeadersArray) {
407407
const ws = this._webSockets.get(requestId);
408-
if (ws && ws.markAsNotified())
408+
if (!ws)
409+
return;
410+
411+
// Firefox does not provide any timing info for WebSocket activity.
412+
// Remove this check if/when support is added.
413+
if (wallTime !== undefined && timestamp !== undefined)
414+
ws.setTimestampBaseline(wallTime, timestamp);
415+
416+
if (ws.markAsNotified())
409417
this._page.emit(Page.Events.WebSocket, ws);
418+
419+
// Firefox does not provide any headers for WebSocket activity.
420+
// Remove this check if/when support is added.
421+
if (headers)
422+
ws.requestSent(headers);
410423
}
411424

412-
onWebSocketResponse(requestId: string, status: number, statusText: string) {
425+
onWebSocketResponse(requestId: string, status: number, statusText: string, headers: types.HeadersArray) {
413426
const ws = this._webSockets.get(requestId);
414-
if (status < 400)
427+
if (!ws)
415428
return;
416-
if (ws)
429+
ws.responseReceived(status, statusText, headers);
430+
if (status >= 400)
417431
ws.error(`${statusText}: ${status}`);
418432
}
419433

420-
onWebSocketFrameSent(requestId: string, opcode: number, data: string) {
434+
onWebSocketFrameSent(requestId: string, opcode: number, data: string, timestamp?: number) {
421435
const ws = this._webSockets.get(requestId);
422436
if (ws)
423-
ws.frameSent(opcode, data);
437+
ws.frameSent(opcode, data, timestamp);
424438
}
425439

426-
webSocketFrameReceived(requestId: string, opcode: number, data: string) {
440+
webSocketFrameReceived(requestId: string, opcode: number, data: string, timestamp?: number) {
427441
const ws = this._webSockets.get(requestId);
428442
if (ws)
429-
ws.frameReceived(opcode, data);
443+
ws.frameReceived(opcode, data, timestamp);
430444
}
431445

432446
webSocketClosed(requestId: string) {

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

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,10 @@ import { helper } from '../helper';
3030
import * as network from '../network';
3131
import { nullProgress } from '../progress';
3232

33+
import { Page } from '../page';
34+
3335
import type { RegisteredListener } from '@utils/eventsHelper';
3436
import type { APIRequestEvent, APIRequestFinishedEvent } from '../fetch';
35-
import type { Page } from '../page';
3637
import type { Worker } from '../page';
3738
import type { HeadersArray, LifecycleEvent } from '../types';
3839
import type * as har from '@trace/har';
@@ -102,7 +103,10 @@ export class HarTracer {
102103
];
103104
if (this._context instanceof BrowserContext) {
104105
this._eventListeners.push(
105-
eventsHelper.addEventListener(this._context, BrowserContext.Events.Page, (page: Page) => this._createPageEntryIfNeeded(page)),
106+
eventsHelper.addEventListener(this._context, BrowserContext.Events.Page, (page: Page) => {
107+
this._addPageEventListeners(page);
108+
this._createPageEntryIfNeeded(page);
109+
}),
106110
eventsHelper.addEventListener(this._context, BrowserContext.Events.Request, (request: network.Request) => this._onRequest(request)),
107111
eventsHelper.addEventListener(this._context, BrowserContext.Events.RequestFinished, ({ request, response }) => this._onRequestFinished(request, response).catch(() => {})),
108112
eventsHelper.addEventListener(this._context, BrowserContext.Events.RequestFailed, request => this._onRequestFailed(request)),
@@ -111,11 +115,21 @@ export class HarTracer {
111115
eventsHelper.addEventListener(this._context, BrowserContext.Events.RequestFulfilled, request => this._onRequestFulfilled(request)),
112116
eventsHelper.addEventListener(this._context, BrowserContext.Events.RequestContinued, request => this._onRequestContinued(request)),
113117
);
114-
for (const page of this._context.pages())
118+
for (const page of this._context.pages()) {
119+
this._addPageEventListeners(page);
115120
this._createPageEntryIfNeeded(page);
121+
}
116122
}
117123
}
118124

125+
private _addPageEventListeners(page: Page) {
126+
if (this._page && page !== this._page)
127+
return;
128+
this._eventListeners.push(
129+
eventsHelper.addEventListener(page, Page.Events.WebSocket, (webSocket: network.WebSocket) => this._onWebSocket(page, webSocket)),
130+
);
131+
}
132+
119133
private _shouldIncludeEntryWithUrl(urlString: string) {
120134
return !this._options.urlFilter || urlMatches(this._baseURL, urlString, this._options.urlFilter);
121135
}
@@ -418,6 +432,48 @@ export class HarTracer {
418432
harEntry._wasContinued = true;
419433
}
420434

435+
private _onWebSocket(page: Page, webSocket: network.WebSocket) {
436+
if (!this._shouldIncludeEntryWithUrl(webSocket.url()))
437+
return;
438+
const url = network.parseURL(webSocket.url());
439+
if (!url)
440+
return;
441+
442+
const pageEntry = this._createPageEntryIfNeeded(page);
443+
const harEntry = createHarEntry(pageEntry?.id, 'GET', url, page.mainFrame().guid, this._options);
444+
harEntry._resourceType = 'websocket';
445+
harEntry._webSocketMessages = [];
446+
447+
// Fallbacks in case we never receive a response.
448+
harEntry.response.status = 101;
449+
harEntry.response.statusText = 'Switching Protocols';
450+
451+
if (this._started)
452+
this._delegate.onEntryStarted(harEntry);
453+
454+
eventsHelper.addEventListener(webSocket, network.WebSocket.Events.Request, ({ headers }: { headers: HeadersArray }) => {
455+
this._recordRequestHeadersAndCookies(harEntry, headers);
456+
});
457+
eventsHelper.addEventListener(webSocket, network.WebSocket.Events.Response, ({ status, statusText, headers }: { status: number, statusText: string, headers: HeadersArray }) => {
458+
harEntry.response.status = status;
459+
harEntry.response.statusText = statusText;
460+
this._recordResponseHeaders(harEntry, headers);
461+
});
462+
eventsHelper.addEventListener(webSocket, network.WebSocket.Events.FrameSent, ({ opcode, data, timestamp }: { opcode: number, data: string, timestamp: number }) => {
463+
harEntry._webSocketMessages!.push({ type: 'send', time: timestamp, opcode, data });
464+
});
465+
eventsHelper.addEventListener(webSocket, network.WebSocket.Events.FrameReceived, ({ opcode, data, timestamp }: { opcode: number, data: string, timestamp: number }) => {
466+
harEntry._webSocketMessages!.push({ type: 'receive', time: timestamp, opcode, data });
467+
});
468+
eventsHelper.addEventListener(webSocket, network.WebSocket.Events.SocketError, (errorMessage: string) => {
469+
harEntry.response._failureText = errorMessage;
470+
});
471+
eventsHelper.addEventListener(webSocket, network.WebSocket.Events.Close, () => {
472+
if (this._started)
473+
this._delegate.onEntryFinished(harEntry);
474+
});
475+
}
476+
421477
private _storeResponseContent(buffer: Buffer | undefined, content: har.Content, resourceType: string) {
422478
if (!buffer) {
423479
content.size = 0;

packages/playwright-core/src/server/network.ts

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -725,12 +725,20 @@ export class Response extends SdkObject {
725725
export class WebSocket extends SdkObject {
726726
private _url: string;
727727
private _notified = false;
728+
private _requestWallTime: number | undefined;
729+
private _requestTimestamp: number | undefined;
730+
private _status: number | undefined;
731+
private _statusText: string | undefined;
732+
private _requestHeaders: HeadersArray | undefined;
733+
private _responseHeaders: HeadersArray | undefined;
728734

729735
static Events = {
730736
Close: 'close',
731737
SocketError: 'socketerror',
732738
FrameReceived: 'framereceived',
733739
FrameSent: 'framesent',
740+
Request: 'request',
741+
Response: 'response',
734742
};
735743

736744
constructor(parent: SdkObject, url: string) {
@@ -748,16 +756,43 @@ export class WebSocket extends SdkObject {
748756
return true;
749757
}
750758

759+
setTimestampBaseline(wallTime: number, timestamp: number) {
760+
this._requestWallTime = wallTime;
761+
this._requestTimestamp = timestamp;
762+
}
763+
764+
private _toWallTime(timestamp: number | undefined): number {
765+
if (timestamp !== undefined && this._requestWallTime !== undefined && this._requestTimestamp !== undefined)
766+
return this._requestWallTime + (timestamp - this._requestTimestamp);
767+
return Date.now() / 1000;
768+
}
769+
751770
url(): string {
752771
return this._url;
753772
}
754773

755-
frameSent(opcode: number, data: string) {
756-
this.emit(WebSocket.Events.FrameSent, { opcode, data });
774+
requestSent(headers: HeadersArray) {
775+
this.emit(WebSocket.Events.Request, { headers });
757776
}
758777

759-
frameReceived(opcode: number, data: string) {
760-
this.emit(WebSocket.Events.FrameReceived, { opcode, data });
778+
responseReceived(status: number, statusText: string, headers: HeadersArray) {
779+
this.emit(WebSocket.Events.Response, { status, statusText, headers });
780+
}
781+
782+
frameSent(opcode: number, data: string, timestamp?: number) {
783+
this.emit(WebSocket.Events.FrameSent, {
784+
opcode,
785+
data,
786+
timestamp: this._toWallTime(timestamp),
787+
});
788+
}
789+
790+
frameReceived(opcode: number, data: string, timestamp?: number) {
791+
this.emit(WebSocket.Events.FrameReceived, {
792+
opcode,
793+
data,
794+
timestamp: this._toWallTime(timestamp),
795+
});
761796
}
762797

763798
error(errorMessage: string) {

packages/playwright-core/src/server/webkit/webview/wvPage.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
import { PNG } from 'pngjs';
1919
import jpegjs from 'jpeg-js';
2020
import { assert } from '@isomorphic/assert';
21-
import { headersArrayToObject } from '@isomorphic/headers';
21+
import { headersArrayToObject, headersObjectToArray } from '@isomorphic/headers';
2222
import { splitErrorMessage } from '@isomorphic/stackTrace';
2323
import { debugLogger } from '@utils/debugLogger';
2424
import { eventsHelper } from '@utils/eventsHelper';
@@ -300,6 +300,8 @@ export class WVPage implements PageDelegate {
300300
}
301301

302302
private _buildSessionListeners(session: WVSession): RegisteredListener[] {
303+
const setCookieSeparator = process.platform === 'darwin' ? ',' : 'playwright-set-cookie-separator';
304+
303305
return [
304306
eventsHelper.addEventListener(session, 'Page.frameNavigated', event => this._onFrameNavigated(event.frame, false)),
305307
eventsHelper.addEventListener(session, 'Page.navigatedWithinDocument', event => this._onFrameNavigatedWithinDocument(event.frameId, event.url)),
@@ -316,10 +318,10 @@ export class WVPage implements PageDelegate {
316318
eventsHelper.addEventListener(session, 'Network.loadingFinished', e => this._onLoadingFinished(e)),
317319
eventsHelper.addEventListener(session, 'Network.loadingFailed', e => this._onLoadingFailed(session, e)),
318320
eventsHelper.addEventListener(session, 'Network.webSocketCreated', e => this._page.frameManager.onWebSocketCreated(e.requestId, e.url)),
319-
eventsHelper.addEventListener(session, 'Network.webSocketWillSendHandshakeRequest', e => this._page.frameManager.onWebSocketRequest(e.requestId)),
320-
eventsHelper.addEventListener(session, 'Network.webSocketHandshakeResponseReceived', e => this._page.frameManager.onWebSocketResponse(e.requestId, e.response.status, e.response.statusText)),
321-
eventsHelper.addEventListener(session, 'Network.webSocketFrameSent', e => e.response.payloadData && this._page.frameManager.onWebSocketFrameSent(e.requestId, e.response.opcode, e.response.payloadData)),
322-
eventsHelper.addEventListener(session, 'Network.webSocketFrameReceived', e => e.response.payloadData && this._page.frameManager.webSocketFrameReceived(e.requestId, e.response.opcode, e.response.payloadData)),
321+
eventsHelper.addEventListener(session, 'Network.webSocketWillSendHandshakeRequest', e => this._page.frameManager.onWebSocketRequest(e.requestId, e.walltime, e.timestamp, headersObjectToArray(e.request.headers))),
322+
eventsHelper.addEventListener(session, 'Network.webSocketHandshakeResponseReceived', e => this._page.frameManager.onWebSocketResponse(e.requestId, e.response.status, e.response.statusText, headersObjectToArray(e.response.headers, ',', setCookieSeparator))),
323+
eventsHelper.addEventListener(session, 'Network.webSocketFrameSent', e => e.response.payloadData && this._page.frameManager.onWebSocketFrameSent(e.requestId, e.response.opcode, e.response.payloadData, e.timestamp)),
324+
eventsHelper.addEventListener(session, 'Network.webSocketFrameReceived', e => e.response.payloadData && this._page.frameManager.webSocketFrameReceived(e.requestId, e.response.opcode, e.response.payloadData, e.timestamp)),
323325
eventsHelper.addEventListener(session, 'Network.webSocketClosed', e => this._page.frameManager.webSocketClosed(e.requestId)),
324326
eventsHelper.addEventListener(session, 'Network.webSocketFrameError', e => this._page.frameManager.webSocketError(e.requestId, e.errorMessage)),
325327
];

packages/playwright-core/src/server/webkit/wkPage.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
import { PNG } from 'pngjs';
1919
import jpegjs from 'jpeg-js';
20-
import { headersArrayToObject } from '@isomorphic/headers';
20+
import { headersArrayToObject, headersObjectToArray } from '@isomorphic/headers';
2121
import { splitErrorMessage } from '@isomorphic/stackTrace';
2222
import { eventsHelper } from '@utils/eventsHelper';
2323
import { hostPlatform } from '@utils/hostPlatform';
@@ -376,6 +376,8 @@ export class WKPage implements PageDelegate {
376376
}
377377

378378
private _addSessionListeners() {
379+
const setCookieSeparator = process.platform === 'darwin' ? ',' : 'playwright-set-cookie-separator';
380+
379381
this._sessionListeners = [
380382
eventsHelper.addEventListener(this._session, 'Page.frameNavigated', event => this._onFrameNavigated(event.frame, false)),
381383
eventsHelper.addEventListener(this._session, 'Page.navigatedWithinDocument', event => this._onFrameNavigatedWithinDocument(event.frameId, event.url)),
@@ -397,10 +399,10 @@ export class WKPage implements PageDelegate {
397399
eventsHelper.addEventListener(this._session, 'Network.loadingFinished', e => this._onLoadingFinished(e)),
398400
eventsHelper.addEventListener(this._session, 'Network.loadingFailed', e => this._onLoadingFailed(this._session, e)),
399401
eventsHelper.addEventListener(this._session, 'Network.webSocketCreated', e => this._page.frameManager.onWebSocketCreated(e.requestId, e.url)),
400-
eventsHelper.addEventListener(this._session, 'Network.webSocketWillSendHandshakeRequest', e => this._page.frameManager.onWebSocketRequest(e.requestId)),
401-
eventsHelper.addEventListener(this._session, 'Network.webSocketHandshakeResponseReceived', e => this._page.frameManager.onWebSocketResponse(e.requestId, e.response.status, e.response.statusText)),
402-
eventsHelper.addEventListener(this._session, 'Network.webSocketFrameSent', e => e.response.payloadData && this._page.frameManager.onWebSocketFrameSent(e.requestId, e.response.opcode, e.response.payloadData)),
403-
eventsHelper.addEventListener(this._session, 'Network.webSocketFrameReceived', e => e.response.payloadData && this._page.frameManager.webSocketFrameReceived(e.requestId, e.response.opcode, e.response.payloadData)),
402+
eventsHelper.addEventListener(this._session, 'Network.webSocketWillSendHandshakeRequest', e => this._page.frameManager.onWebSocketRequest(e.requestId, e.walltime, e.timestamp, headersObjectToArray(e.request.headers))),
403+
eventsHelper.addEventListener(this._session, 'Network.webSocketHandshakeResponseReceived', e => this._page.frameManager.onWebSocketResponse(e.requestId, e.response.status, e.response.statusText, headersObjectToArray(e.response.headers, ',', setCookieSeparator))),
404+
eventsHelper.addEventListener(this._session, 'Network.webSocketFrameSent', e => e.response.payloadData && this._page.frameManager.onWebSocketFrameSent(e.requestId, e.response.opcode, e.response.payloadData, e.timestamp)),
405+
eventsHelper.addEventListener(this._session, 'Network.webSocketFrameReceived', e => e.response.payloadData && this._page.frameManager.webSocketFrameReceived(e.requestId, e.response.opcode, e.response.payloadData, e.timestamp)),
404406
eventsHelper.addEventListener(this._session, 'Network.webSocketClosed', e => this._page.frameManager.webSocketClosed(e.requestId)),
405407
eventsHelper.addEventListener(this._session, 'Network.webSocketFrameError', e => this._page.frameManager.webSocketError(e.requestId, e.errorMessage)),
406408
];

packages/trace/src/har.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,15 @@ export type Entry = {
7272
_wasFulfilled?: boolean;
7373
_wasContinued?: boolean;
7474
_apiRequest?: boolean;
75+
_resourceType?: string;
76+
_webSocketMessages?: WebSocketMessage[];
77+
};
78+
79+
export type WebSocketMessage = {
80+
type: 'send' | 'receive';
81+
time: number;
82+
opcode: number;
83+
data: string;
7584
};
7685

7786
export type Request = {

0 commit comments

Comments
 (0)