Skip to content
Merged
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
8 changes: 4 additions & 4 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ jobs:
- name: Use Node.js
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
with:
node-version: '10.x'
- name: Install dependencies
node-version: '20.x'
- name: Install dependencies
run: npm ci
- name: Verify lint
run: npm run lint
- name: Verify build
run: npm run build

28 changes: 19 additions & 9 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
<script src="/js/compare/generate.js" defer></script>
<script src="/js/compare/generateVisualProgress.js" defer></script>
<script src="/js/compare/templates.js" defer></script>
<script src="/js/compare/share.js" defer></script>
<script src="/js/compare/compare.js" defer></script>


Expand Down Expand Up @@ -91,11 +92,16 @@
}
getAndSet("har1", "harurl");
getAndSet("har2", "harurl2");
// Carry stripVersion through share links — `?stripVersion=1`
// mirrors the start-page checkbox so a shared URL renders the
// same comparison the sender saw.
const stripVersionFromUrl = getParam("stripVersion") === "1";
if (getParam("har1") != undefined && getParam("har2") != undefined) {
showLoading();
loadHARsFromConfig({
har1: { url: getParam("har1") },
har2: { url: getParam("har2") }
har2: { url: getParam("har2") },
stripVersion: stripVersionFromUrl
});
} else if (
(getParam("har1") != undefined || getParam("har2") != undefined) &&
Expand All @@ -104,7 +110,8 @@
showLoading();
loadHARsFromConfig({
har1: { url: getParam("har1") || getParam("har2") },
har2: {}
har2: {},
stripVersion: stripVersionFromUrl
});
} else if (getParam("gist") !== undefined) {
showLoading();
Expand All @@ -116,25 +123,27 @@

document.addEventListener("paste", function (e) {
const paste = e.clipboardData.getData("Text");
// Is it a HAR or a gist?
// Is it a HAR, a bundle, or a gist?
try {
const har = JSON.parse(paste);
const parsed = JSON.parse(paste);
showLoading();
if (har.log) {
if (isBundle(parsed)) {
loadFromBundle(parsed);
} else if (parsed.log) {
generate({
har1: {
har: har,
har: parsed,
run: 0,
label: "HAR1"
},
har2: {
har: har,
run: har.log.pages.length > 1 ? 1 : 0,
har: parsed,
run: parsed.log.pages.length > 1 ? 1 : 0,
label: "HAR2"
}
});
} else {
readConfig(har);
readConfig(parsed);
}
} catch (e) {
if (paste.startsWith("https://gist.github.com/")) {
Expand Down Expand Up @@ -229,6 +238,7 @@ <h1 class="title">Compare</h1>
</a>
</div>
<nav id="resultHeaderContent" aria-label="Page sections"></nav>
<div id="shareControls" class="share-controls" aria-live="polite"></div>
</div>
</div>
</header>
Expand Down
30 changes: 24 additions & 6 deletions public/js/compare/generate.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/* global getLastTiming, removeAndHide, createUpload, getAllDomains, hideUpload, objectPropertiesToArray, registerTemplateHelpers, parseTemplate, getTotalDiff, generateVisualProgress, formatDate, getUniqueRequests, getFilmstrip, compareWaterfall */
/* global getLastTiming, removeAndHide, createUpload, getAllDomains, hideUpload, objectPropertiesToArray, registerTemplateHelpers, parseTemplate, getTotalDiff, generateVisualProgress, formatDate, getUniqueRequests, getFilmstrip, compareWaterfall, renderShareControls */
/* exported showUpload, formatDate, generate, toggleRow, regenerate, formatTime, showLoading*/

/**
Expand All @@ -11,17 +11,28 @@ function regenerate(switchHar) {
const e2 = document.getElementById('run2Option');
const runIndex = e ? e.options[e.selectedIndex].value : 0;
const runIndex2 = e2 ? e2.options[e2.selectedIndex].value : 0;
const prev = window.har || {};
// Carry per-HAR metadata (url) and top-level config (title,
// firstParty, stripVersion, comments) across a Switch / run change.
// Without this, the share UI would flip from "Copy share link" to
// "Download bundle" the moment the user toggled anything.
generate({
har1: {
har: switchHar ? window.har.har2.har : window.har.har1.har,
har: switchHar ? prev.har2.har : prev.har1.har,
run: switchHar ? runIndex2 : runIndex,
label: switchHar ? window.har.har2.label : window.har.har1.label
label: switchHar ? prev.har2.label : prev.har1.label,
url: switchHar ? prev.har2.url : prev.har1.url
},
har2: {
har: switchHar ? window.har.har1.har : window.har.har2.har,
har: switchHar ? prev.har1.har : prev.har2.har,
run: switchHar ? runIndex : runIndex2,
label: switchHar ? window.har.har1.label : window.har.har2.label
}
label: switchHar ? prev.har1.label : prev.har2.label,
url: switchHar ? prev.har1.url : prev.har2.url
},
title: prev.title,
firstParty: prev.firstParty,
stripVersion: prev.stripVersion,
comments: prev.comments
});
}

Expand Down Expand Up @@ -293,4 +304,11 @@ function generate(config) {

createUpload('har1upload');
createUpload('har2upload');

// Render/refresh the share affordance now that window.har is set.
// Guarded so a missing share.js (e.g. embedded usage) is a no-op
// rather than a hard error.
if (typeof renderShareControls === 'function') {
renderShareControls();
}
}
47 changes: 35 additions & 12 deletions public/js/compare/har.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,21 +107,21 @@ function getUniqueRequests(har1, run1, har2, run2, options) {
const urls2 = getURLs(har2, run2, options.stripVersion);
const all = [];
const minDiffInBytes = 1000;
// Use `in` for presence checks because a URL whose transfer size is
// unknown is stored as 0 — truthy checks would mis-classify those as
// removed/added.
for (let url of Object.keys(urls1)) {
if (
(urls2[url] && urls2[url] === urls1[url]) ||
(urls2[url] &&
urls2[url] - urls1[url] < minDiffInBytes &&
urls2[url] - urls1[url] > -minDiffInBytes)
) {
// TODO no diff, do nada
} else if (urls2[url]) {
// There's a diff in size
if (url in urls2) {
const delta = urls2[url] - urls1[url];
if (delta < minDiffInBytes && delta > -minDiffInBytes) {
// no meaningful diff, skip
continue;
}
all.push({
url: url,
har1: urls1[url],
har2: urls2[url],
diff: urls2[url] - urls1[url]
diff: delta
});
} else {
all.push({
Expand All @@ -132,7 +132,7 @@ function getUniqueRequests(har1, run1, har2, run2, options) {
}
}
for (let url of Object.keys(urls2)) {
if (urls2[url] && !urls1[url]) {
if (!(url in urls1)) {
all.push({
url: url,
diff: urls2[url],
Expand Down Expand Up @@ -162,7 +162,30 @@ function getURLs(har, run, stripVersion) {
if (stripVersion) {
url = url.replace(/version=[A-Za-z0-9]+/i, '');
}
urls[url] = entry.response.bodySize;
urls[url] = bestTransferSize(entry.response);
}
return urls;
}

// Best-effort "bytes over the wire" for a HAR response. The request
// diff cares about network cost, not decoded payload size, so prefer
// _transferSize (added by Chrome devtools / sitespeed.io / WPT) which
// is exactly that. HAR-spec bodySize is the encoded body length when
// known and -1 when not — usable as a fallback. content.size is the
// decoded body and a last resort. Anything missing or <= 0 (spec uses
// -1 for "unknown") becomes 0 so totals don't go negative.
function bestTransferSize(response) {
if (!response) return 0;
if (typeof response._transferSize === 'number' && response._transferSize > 0) {
return response._transferSize;
}
if (typeof response.bodySize === 'number' && response.bodySize > 0) {
return response.bodySize;
}
if (response.content &&
typeof response.content.size === 'number' &&
response.content.size > 0) {
return response.content.size;
}
return 0;
}
50 changes: 47 additions & 3 deletions public/js/compare/load.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* global isFileGzipped, isFileZipped, gzipArrayBufferToJSON, readGZipFile, errorMessage, generate, showUpload */
/* exported readHar, fetchHar, getHarURL, loadFilesFromURL, loadFilesFromGist, loadFilesFromConfig*/
/* exported readHar, fetchHar, getHarURL, loadFilesFromURL, loadFilesFromGist, loadFilesFromConfig, loadFromBundle, isBundle*/

/**
* Help functions to read HAR/JSON files from file
Expand Down Expand Up @@ -166,6 +166,43 @@ function loadFilesFromGist(id) {
});
}

// Compare bundle: a single JSON file with both HARs embedded, produced
// by the "Download bundle" share action. We detect it by the explicit
// `compareBundle` flag so a future format bump doesn't get mistaken
// for the current shape.
function isBundle(obj) {
return !!(obj && obj.compareBundle === true && obj.har1 && obj.har2);
}

function loadFromBundle(bundle) {
if (!bundle.har1 || !bundle.har1.har || !bundle.har1.har.log) {
errorMessage('Bundle is missing har1.');
showUpload();
return;
}
if (!bundle.har2 || !bundle.har2.har || !bundle.har2.har.log) {
errorMessage('Bundle is missing har2.');
showUpload();
return;
}
generate({
har1: {
har: bundle.har1.har,
run: bundle.har1.run || 0,
label: bundle.har1.label || 'HAR1'
},
har2: {
har: bundle.har2.har,
run: bundle.har2.run || 0,
label: bundle.har2.label || 'HAR2'
},
title: bundle.title || 'Compare HAR files',
firstParty: bundle.firstParty || undefined,
stripVersion: !!bundle.stripVersion,
comments: bundle.comments || undefined
});
}

function loadHARsFromConfig(config) {
// The runs/pages are zero based since it's an array but
// in configuration we wanna use 1 based since it makes more sense
Expand Down Expand Up @@ -201,16 +238,23 @@ function loadHARsFromConfig(config) {
const har2Run = sameHar
? (har1.log.pages.length > 1 ? 1 : 0)
: (reworkedConfig2.run || config.har2.run || 0);
// Preserve the *user-supplied* URLs (not the rewritten sitespeed
// .har.gz paths) so a share link sends the recipient to the same
// landing context the original viewer used. The single-HAR case
// (`?compare=1&har1=…`) intentionally leaves har2.url unset — the
// share UI then offers a bundle download instead.
return generate({
har1: {
har: har1,
run: har1Run,
label: config.har1.label || 'HAR1'
label: config.har1.label || 'HAR1',
url: config.har1.url
},
har2: {
har: har2,
run: har2Run,
label: config.har2.label || 'HAR2'
label: config.har2.label || 'HAR2',
url: sameHar ? undefined : config.har2.url
},
comments: config.comments || undefined,
title: config.title || 'Compare HAR files',
Expand Down
Loading