From f2dde2c0d6ba63c453013d5eac050da70f32c223 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcus=20=C3=96sterberg?= Date: Wed, 15 Jul 2026 16:24:32 +0200 Subject: [PATCH] Only lint styles belonging to the tested page A browsertime HAR can contain more than one page, for example when a concurrent browsertime run on the same host races for the same Chrome DevTools port and its crossed CDP session records another website's page load into this run's recording (see Webperf-se/webperf_core#1557). This analyzer linted every HTML and CSS response body in the whole HAR (and refetched missing CSS files from any domain in it), so a crossed-in recording could attribute another website's CSS issues to the tested website. Filter entries to the ones belonging to the first page in the HAR's pages array, and verify that the recording's first request matches the tested URL's hostname (the URL API normalizes IDN hostnames to punycode on both sides). On mismatch nothing is analyzed. HARs without a pages array and entries without pageref behave as before. --- lib/harAnalyzer.js | 47 +++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 42 insertions(+), 5 deletions(-) diff --git a/lib/harAnalyzer.js b/lib/harAnalyzer.js index 6bf1784..c9a77cb 100644 --- a/lib/harAnalyzer.js +++ b/lib/harAnalyzer.js @@ -20,6 +20,47 @@ export class HarAnalyzer { this.version = this.package.version; } + getFirstPageEntries(url, harData) { + if ('log' in harData) { + harData = harData['log']; + } + + const entries = harData.entries; + if (!Array.isArray(entries)) { + return []; + } + + // A HAR can contain more than one page, for example when a concurrent + // browsertime run ends up in the same browser session (crossed DevTools + // port) and navigates to another website mid-recording. Requests made + // by other pages must not be attributed to the tested website, and if + // the recording doesn't even start with the tested website nothing in + // it can be trusted. + if (url && entries.length > 0) { + const firstUrl = entries[0].request && entries[0].request.url; + if (firstUrl) { + try { + if (new URL(firstUrl).hostname !== new URL(url).hostname) { + return []; + } + } catch { + // Unparsable URLs are handled by the entry loops as before + } + } + } + + const pages = harData.pages; + if (!Array.isArray(pages) || pages.length === 0) { + return entries; + } + const firstPageId = pages[0].id; + if (firstPageId === undefined) { + return entries; + } + return entries.filter(entry => + entry.pageref === undefined || entry.pageref === firstPageId); + } + async transform2SimplifiedData(harData, url) { const data = { 'url': url, @@ -31,10 +72,6 @@ export class HarAnalyzer { 'style-files': [] }; - if ('log' in harData) { - harData = harData['log']; - } - let reqIndex = 1; // CSS resources whose response body is missing from the HAR (served from @@ -44,7 +81,7 @@ export class HarAnalyzer { // falsely reported as "Unknown custom property" (no-unknown-custom-properties). const missingCssUrls = new Set(); - for (const entry of harData.entries) { + for (const entry of this.getFirstPageEntries(url, harData)) { const req = entry.request; const res = entry.response; const reqUrl = req.url;