Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions browser_patches/firefox/juggler/content/FrameTree.js
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,7 @@ class Frame {
wsid: webSocketSerialID + '',
opcode: frame.opCode,
data: frame.opCode !== 1 ? btoa(frame.payload) : frame.payload,
timestamp: frame.timeStamp / 1_000_000,
});
this._webSocketListener = {
QueryInterface: ChromeUtils.generateQI([Ci.nsIWebSocketEventListener, ]),
Expand Down Expand Up @@ -492,6 +493,7 @@ class Frame {
wsid: webSocketSerialID + '',
opcode: frame.opCode,
data: frame.opCode !== 1 ? btoa(frame.payload) : frame.payload,
timestamp: frame.timeStamp / 1_000_000,
});
},
};
Expand Down
2 changes: 2 additions & 0 deletions browser_patches/firefox/juggler/protocol/Protocol.js
Original file line number Diff line number Diff line change
Expand Up @@ -716,12 +716,14 @@ const Page = {
wsid: t.String,
opcode: t.Number,
data: t.String,
timestamp: t.Number,
},
'webSocketFrameReceived': {
frameId: t.String,
wsid: t.String,
opcode: t.Number,
data: t.String,
timestamp: t.Number,
},
'screencastFrame': {
data: t.String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ class BidiRequest {
redirectedFrom._redirectedTo = this;
// TODO: missing in the spec?
const postDataBuffer = null;
this.request = new network.Request(frame._page.browserContext, frame, null, redirectedFrom ? redirectedFrom.request : null, payload.navigation ?? undefined,
this.request = new network.Request(frame._page.browserContext, frame, null, redirectedFrom ? redirectedFrom.request : null, this._id, payload.navigation ?? undefined,
payload.request.url, resourceTypeFromBidi(payload.request.destination, payload.request.initiatorType, payload.initiator?.type), payload.request.method,
postDataBuffer, headersOverride || fromBidiHeaders(payload.request.headers));
// "raw" headers are the same as "provisional" headers in Bidi.
Expand Down
10 changes: 5 additions & 5 deletions packages/playwright-core/src/server/chromium/crNetworkManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,10 @@ export class CRNetworkManager {
if (this._page) {
sessionInfo.eventListeners.push(...[
eventsHelper.addEventListener(session, 'Network.webSocketCreated', e => this._page!.frameManager.onWebSocketCreated(e.requestId, e.url)),
eventsHelper.addEventListener(session, 'Network.webSocketWillSendHandshakeRequest', e => this._page!.frameManager.onWebSocketRequest(e.requestId)),
eventsHelper.addEventListener(session, 'Network.webSocketHandshakeResponseReceived', e => this._page!.frameManager.onWebSocketResponse(e.requestId, e.response.status, e.response.statusText)),
eventsHelper.addEventListener(session, 'Network.webSocketFrameSent', e => e.response.payloadData && this._page!.frameManager.onWebSocketFrameSent(e.requestId, e.response.opcode, e.response.payloadData)),
eventsHelper.addEventListener(session, 'Network.webSocketFrameReceived', e => e.response.payloadData && this._page!.frameManager.webSocketFrameReceived(e.requestId, e.response.opcode, e.response.payloadData)),
eventsHelper.addEventListener(session, 'Network.webSocketWillSendHandshakeRequest', e => this._page!.frameManager.onWebSocketRequest(e.requestId, e.wallTime, e.timestamp, headersObjectToArray(e.request.headers, '\n'))),
eventsHelper.addEventListener(session, 'Network.webSocketHandshakeResponseReceived', e => this._page!.frameManager.onWebSocketResponse(e.requestId, e.response.status, e.response.statusText, headersObjectToArray(e.response.headers, '\n'))),
eventsHelper.addEventListener(session, 'Network.webSocketFrameSent', e => e.response.payloadData && this._page!.frameManager.onWebSocketFrameSent(e.requestId, e.response.opcode, e.response.payloadData, e.timestamp)),
eventsHelper.addEventListener(session, 'Network.webSocketFrameReceived', e => e.response.payloadData && this._page!.frameManager.webSocketFrameReceived(e.requestId, e.response.opcode, e.response.payloadData, e.timestamp)),
eventsHelper.addEventListener(session, 'Network.webSocketClosed', e => this._page!.frameManager.webSocketClosed(e.requestId)),
eventsHelper.addEventListener(session, 'Network.webSocketFrameError', e => this._page!.frameManager.webSocketError(e.requestId, e.errorMessage)),
]);
Expand Down Expand Up @@ -605,7 +605,7 @@ class InterceptableRequest {
if (entries && entries.length)
postDataBuffer = Buffer.concat(entries.map(entry => Buffer.from(entry.bytes!, 'base64')));

this.request = new network.Request(context, frame, serviceWorker, redirectedFrom?.request || null, documentId, url, toResourceType(requestWillBeSentEvent.type || 'Other'), method, postDataBuffer, headersOverride || headersObjectToArray(headers));
this.request = new network.Request(context, frame, serviceWorker, redirectedFrom?.request || null, this._requestId, documentId, url, toResourceType(requestWillBeSentEvent.type || 'Other'), method, postDataBuffer, headersOverride || headersObjectToArray(headers));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ class InterceptableRequest {
let postDataBuffer = null;
if (payload.postData)
postDataBuffer = Buffer.from(payload.postData, 'base64');
this.request = new network.Request(frame._page.browserContext, frame, null, redirectedFrom ? redirectedFrom.request : null, payload.navigationId,
this.request = new network.Request(frame._page.browserContext, frame, null, redirectedFrom ? redirectedFrom.request : null, this._id, payload.navigationId,
payload.url, internalCauseToResourceType[payload.internalCause] || causeToResourceType[payload.cause] || 'other', payload.method, postDataBuffer, payload.headers);
// "raw" headers are the same as "provisional" headers in Firefox.
this.request.setRawRequestHeaders(null);
Expand Down
10 changes: 7 additions & 3 deletions packages/playwright-core/src/server/firefox/ffPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ export class FFPage implements PageDelegate {
eventsHelper.addEventListener(this._session, 'Page.crashed', this._onCrashed.bind(this)),

eventsHelper.addEventListener(this._session, 'Page.webSocketCreated', this._onWebSocketCreated.bind(this)),
eventsHelper.addEventListener(this._session, 'Page.webSocketOpened', this._onWebSocketOpened.bind(this)),
eventsHelper.addEventListener(this._session, 'Page.webSocketClosed', this._onWebSocketClosed.bind(this)),
eventsHelper.addEventListener(this._session, 'Page.webSocketFrameReceived', this._onWebSocketFrameReceived.bind(this)),
eventsHelper.addEventListener(this._session, 'Page.webSocketFrameSent', this._onWebSocketFrameSent.bind(this)),
Expand Down Expand Up @@ -119,7 +120,10 @@ export class FFPage implements PageDelegate {

_onWebSocketCreated(event: Protocol.Page.webSocketCreatedPayload) {
this._page.frameManager.onWebSocketCreated(webSocketId(event.frameId, event.wsid), event.requestURL);
this._page.frameManager.onWebSocketRequest(webSocketId(event.frameId, event.wsid));
}

_onWebSocketOpened(event: Protocol.Page.webSocketOpenedPayload) {
this._page.frameManager.onWebSocketOpened(webSocketId(event.frameId, event.wsid), event.requestId);
}

_onWebSocketClosed(event: Protocol.Page.webSocketClosedPayload) {
Expand All @@ -129,11 +133,11 @@ export class FFPage implements PageDelegate {
}

_onWebSocketFrameReceived(event: Protocol.Page.webSocketFrameReceivedPayload) {
this._page.frameManager.webSocketFrameReceived(webSocketId(event.frameId, event.wsid), event.opcode, event.data);
this._page.frameManager.webSocketFrameReceived(webSocketId(event.frameId, event.wsid), event.opcode, event.data, event.timestamp);
}

_onWebSocketFrameSent(event: Protocol.Page.webSocketFrameSentPayload) {
this._page.frameManager.onWebSocketFrameSent(webSocketId(event.frameId, event.wsid), event.opcode, event.data);
this._page.frameManager.onWebSocketFrameSent(webSocketId(event.frameId, event.wsid), event.opcode, event.data, event.timestamp);
}

_onExecutionContextCreated(payload: Protocol.Runtime.executionContextCreatedPayload) {
Expand Down
2 changes: 2 additions & 0 deletions packages/playwright-core/src/server/firefox/protocol.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -486,12 +486,14 @@ export namespace Protocol {
wsid: string;
opcode: number;
data: string;
timestamp: number;
}
export type webSocketFrameReceivedPayload = {
frameId: string;
wsid: string;
opcode: number;
data: string;
timestamp: number;
}
export type screencastFramePayload = {
data: string;
Expand Down
58 changes: 37 additions & 21 deletions packages/playwright-core/src/server/frames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -398,46 +398,63 @@ export class FrameManager {
this._webSockets.clear();
}

onWebSocketCreated(requestId: string, url: string) {
onWebSocketCreated(webSocketId: string, url: string) {
const ws = new network.WebSocket(this._page, url);
this._webSockets.set(requestId, ws);
this._webSockets.set(webSocketId, ws);
}

onWebSocketRequest(requestId: string) {
const ws = this._webSockets.get(requestId);
if (ws && ws.markAsNotified())
onWebSocketOpened(webSocketId: string, requestId: string) {
const ws = this._webSockets.get(webSocketId);
if (!ws)
return;

if (ws.markAsNotified())
this._page.emit(Page.Events.WebSocket, ws);

ws.opened(requestId);
}

onWebSocketResponse(requestId: string, status: number, statusText: string) {
const ws = this._webSockets.get(requestId);
if (status < 400)
onWebSocketRequest(webSocketId: string, wallTime: number, timestamp: number, headers: types.HeadersArray) {
const ws = this._webSockets.get(webSocketId);
if (!ws)
return;
if (ws)

if (ws.markAsNotified())
this._page.emit(Page.Events.WebSocket, ws);

ws.requestSent(wallTime, timestamp, headers);
}

onWebSocketResponse(webSocketId: string, status: number, statusText: string, headers: types.HeadersArray) {
const ws = this._webSockets.get(webSocketId);
if (!ws)
return;
ws.responseReceived(status, statusText, headers);
if (status >= 400)
ws.error(`${statusText}: ${status}`);
}

onWebSocketFrameSent(requestId: string, opcode: number, data: string) {
const ws = this._webSockets.get(requestId);
onWebSocketFrameSent(webSocketId: string, opcode: number, data: string, timestamp: number) {
const ws = this._webSockets.get(webSocketId);
if (ws)
ws.frameSent(opcode, data);
ws.frameSent(opcode, data, timestamp);
}

webSocketFrameReceived(requestId: string, opcode: number, data: string) {
const ws = this._webSockets.get(requestId);
webSocketFrameReceived(webSocketId: string, opcode: number, data: string, timestamp: number) {
const ws = this._webSockets.get(webSocketId);
if (ws)
ws.frameReceived(opcode, data);
ws.frameReceived(opcode, data, timestamp);
}

webSocketClosed(requestId: string) {
const ws = this._webSockets.get(requestId);
webSocketClosed(webSocketId: string) {
const ws = this._webSockets.get(webSocketId);
if (ws)
ws.closed();
this._webSockets.delete(requestId);
this._webSockets.delete(webSocketId);
}

webSocketError(requestId: string, errorMessage: string): void {
const ws = this._webSockets.get(requestId);
webSocketError(webSocketId: string, errorMessage: string): void {
const ws = this._webSockets.get(webSocketId);
if (ws)
ws.error(errorMessage);
}
Expand Down Expand Up @@ -936,7 +953,6 @@ export class Frame extends SdkObject<FrameEventMap> {
const lifecyclePromise = progress.race(tagPromise).then(() => this.waitForLoadState(progress, waitUntil));
const contentPromise = progress.race(context.evaluate(({ html, tag }) => {
document.open();
console.debug(tag); // eslint-disable-line no-console
document.write(html);
document.close();
}, { html, tag }));
Expand Down
96 changes: 93 additions & 3 deletions packages/playwright-core/src/server/har/harTracer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,10 @@ import { helper } from '../helper';
import * as network from '../network';
import { nullProgress } from '../progress';

import { Page } from '../page';

import type { RegisteredListener } from '@utils/eventsHelper';
import type { APIRequestEvent, APIRequestFinishedEvent } from '../fetch';
import type { Page } from '../page';
import type { Worker } from '../page';
import type { HeadersArray, LifecycleEvent } from '../types';
import type * as har from '@trace/har';
Expand Down Expand Up @@ -70,6 +71,7 @@ export class HarTracer {
private _eventListeners: RegisteredListener[] = [];
private _started = false;
private _entrySymbol: symbol;
private _webSocketEntries = new Map</* requestId */ string, har.Entry>();
private _baseURL: string | undefined;
private _page: Page | null;

Expand Down Expand Up @@ -102,7 +104,10 @@ export class HarTracer {
];
if (this._context instanceof BrowserContext) {
this._eventListeners.push(
eventsHelper.addEventListener(this._context, BrowserContext.Events.Page, (page: Page) => this._createPageEntryIfNeeded(page)),
eventsHelper.addEventListener(this._context, BrowserContext.Events.Page, (page: Page) => {
this._addPageEventListeners(page);
this._createPageEntryIfNeeded(page);
}),
eventsHelper.addEventListener(this._context, BrowserContext.Events.Request, (request: network.Request) => this._onRequest(request)),
eventsHelper.addEventListener(this._context, BrowserContext.Events.RequestFinished, ({ request, response }) => this._onRequestFinished(request, response).catch(() => {})),
eventsHelper.addEventListener(this._context, BrowserContext.Events.RequestFailed, request => this._onRequestFailed(request)),
Expand All @@ -111,11 +116,21 @@ export class HarTracer {
eventsHelper.addEventListener(this._context, BrowserContext.Events.RequestFulfilled, request => this._onRequestFulfilled(request)),
eventsHelper.addEventListener(this._context, BrowserContext.Events.RequestContinued, request => this._onRequestContinued(request)),
);
for (const page of this._context.pages())
for (const page of this._context.pages()) {
this._addPageEventListeners(page);
this._createPageEntryIfNeeded(page);
}
}
}

private _addPageEventListeners(page: Page) {
if (this._page && page !== this._page)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't we want to listen to all pages?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it seems like it's possible to create a HarTracer that only looks at stuff for a particular Page

for example, you can see this check also in _createPageEntryIfNeeded and _onRequest

return;
this._eventListeners.push(
eventsHelper.addEventListener(page, Page.Events.WebSocket, (webSocket: network.WebSocket) => this._onWebSocket(page, webSocket)),
);
}

private _shouldIncludeEntryWithUrl(urlString: string) {
return !this._options.urlFilter || urlMatches(this._baseURL, urlString, this._options.urlFilter);
}
Expand Down Expand Up @@ -281,6 +296,10 @@ export class HarTracer {
fromEntry.response.redirectURL = request.url();
}
(request as any)[this._entrySymbol] = harEntry;
// In Firefox, WebSockets have additional events once opened.
// In Chromium and WebKit, WebSockets have an entirely different lifecycle and won't reach this.
if (request.resourceType() === 'websocket')
this._webSocketEntries.set(request.requestId(), harEntry);
assert(this._started);
this._delegate.onEntryStarted(harEntry);
}
Expand Down Expand Up @@ -362,6 +381,11 @@ export class HarTracer {
}).catch(() => {
compressionCalculationBarrier?.setDecodedBodySize(0);
}).then(() => {
// In Firefox, WebSockets have additional events once opened.
// In Chromium and WebKit, WebSockets have an entirely different lifecycle and won't reach this.
if (request.resourceType() === 'websocket')
return;

if (this._started)
this._delegate.onEntryFinished(harEntry);
});
Expand Down Expand Up @@ -418,6 +442,72 @@ export class HarTracer {
harEntry._wasContinued = true;
}

private _onWebSocket(page: Page, webSocket: network.WebSocket) {
if (!this._shouldIncludeEntryWithUrl(webSocket.url()))
return;
const url = network.parseURL(webSocket.url());
if (!url)
return;

let harEntry: har.Entry | undefined = undefined;
let matchingRequestId: string | undefined = undefined;

// Only listen for the rest of the WebSocket lifecycle once it's been opened.
const addRemainingListeners = () => {
this._eventListeners.push(
eventsHelper.addEventListener(webSocket, network.WebSocket.Events.Response, ({ status, statusText, headers }: { status: number, statusText: string, headers: HeadersArray }) => {
harEntry!.response.status = status;
harEntry!.response.statusText = statusText;
this._recordResponseHeaders(harEntry!, headers);
}),
eventsHelper.addEventListener(webSocket, network.WebSocket.Events.FrameSent, ({ opcode, data, timestamp }: { opcode: number, data: string, timestamp: number }) => {
harEntry!._webSocketMessages!.push({ type: 'send', time: timestamp, opcode, data });
}),
eventsHelper.addEventListener(webSocket, network.WebSocket.Events.FrameReceived, ({ opcode, data, timestamp }: { opcode: number, data: string, timestamp: number }) => {
harEntry!._webSocketMessages!.push({ type: 'receive', time: timestamp, opcode, data });
}),
eventsHelper.addEventListener(webSocket, network.WebSocket.Events.SocketError, (errorMessage: string) => {
harEntry!.response._failureText = errorMessage;
}),
eventsHelper.addEventListener(webSocket, network.WebSocket.Events.Close, () => {
if (matchingRequestId !== undefined)
this._webSocketEntries.delete(matchingRequestId);

if (this._started)
this._delegate.onEntryFinished(harEntry!);
}),
);
};

this._eventListeners.push(
eventsHelper.addEventListener(webSocket, network.WebSocket.Events.Open, ({ requestId }: { requestId: string }) => {
matchingRequestId = requestId;

harEntry = this._webSocketEntries.get(matchingRequestId);
if (!harEntry)
return;

harEntry.request.url = webSocket.url();
harEntry._resourceType = 'websocket';
harEntry._webSocketMessages = [];

addRemainingListeners();
}),
eventsHelper.addEventListener(webSocket, network.WebSocket.Events.Request, ({ headers }: { headers: HeadersArray }) => {
const pageEntry = this._createPageEntryIfNeeded(page);
harEntry = createHarEntry(pageEntry?.id, 'GET', url, page.mainFrame().guid, this._options);
harEntry._resourceType = 'websocket';
harEntry._webSocketMessages = [];
this._recordRequestHeadersAndCookies(harEntry, headers);

if (this._started)
this._delegate.onEntryStarted(harEntry);

addRemainingListeners();
}),
);
}

private _storeResponseContent(buffer: Buffer | undefined, content: har.Content, resourceType: string) {
if (!buffer) {
content.size = 0;
Expand Down
Loading
Loading