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
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ Concerns that have historically diverged across plugins and should be checked to
- **Notification backends** (`slack`, `matrix`) — both read the budget-result message. When adding a new notification destination, check both for what shape they read.
- **Time-series backends** (`graphite`, the standalone InfluxDB plugin since [#4451](https://github.com/sitespeedio/sitespeed.io/pull/4451)) — share the metric-name flattening logic. Renaming a metric in one without the other diverges historical dashboards.
- **Test data destinations** — the `axe`, `coach`, `sustainable`, `crux` plugins all attach blocks to the `pages[]` JSON. Keep nesting depth and key naming consistent with existing blocks; the HTML report assumes that shape.
- **Latest metadata waits for Compare.** `lateststorer` collects first-iteration metadata on `browsertime.run` but writes the JSON on `sitespeedio.prepareToRender`: Compare emits its per-URL baseline timestamp during `sitespeedio.summarize`. Upload plugins read the latest files on `html.finished`, after that write. Read the timestamp from `compare.pageSummary`, since `--compare.saveBaseline` may already have replaced the baseline file.

## High-risk areas (request verification before modifying unless explicitly instructed)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,8 @@ In the Grafana panel the path to the screenshot is generated by:
And the video:
`$resulturl/$testname/$group.$page.$browser.$connectivity.mp4`

The latest test metadata is stored alongside them as `$group.$page.$browser.$connectivity.json`. Its `friendlyHTML` field describes the test setup for Grafana. When the [compare plugin](/documentation/sitespeed.io/compare/) is enabled, the JSON includes `compare.enabled: true` and the description mentions the plugin. If a comparison was completed, `compare.baselineTimestamp` contains the baseline date in `YYYY-MM-DD HH:mm:ss` format, and that date is also included in `friendlyHTML`. Runs without an existing baseline have no `baselineTimestamp`. The metadata is written after comparisons have finished, before the results are uploaded.

If you can't see the screenshot or the video you can debug it by either inspecting the HTML in Grafana, checking the network log in DevTools (to see if the full URL is correct), or adding `<div>$resulturl/$testname/$group.$page.$browser.$connectivity.$screenshottype</div>` to the panel so you can see the generated URL.

## Set up your own user journey dashboard
Expand Down
50 changes: 44 additions & 6 deletions lib/plugins/lateststorer/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,48 @@ export default class LatestStorerPlugin extends SitespeedioPlugin {
open(context, options) {
this.storageManager = context.storageManager;
this.alias = {};
this.latest = [];
this.baselineTimestamps = new Map();
this.useCompare = false;
this.options = options;
this.context = context;
}
async processMessage(message) {
switch (message.type) {
case 'compare.setup': {
this.useCompare = true;
break;
}

case 'compare.pageSummary': {
this.baselineTimestamps.set(
message.url,
message.data.meta.baseline.timestamp
);
break;
}

case 'sitespeedio.prepareToRender': {
for (const { url, json, filename, directory } of this.latest) {
if (this.useCompare) {
json.compare = { enabled: true };
json.friendlyHTML += ' using the compare plugin';
const baselineTimestamp = this.baselineTimestamps.get(url);
if (baselineTimestamp) {
json.compare.baselineTimestamp = baselineTimestamp;
json.friendlyHTML += ` against baseline from ${baselineTimestamp}`;
}
}

await this.storageManager.writeDataToDir(
JSON.stringify(json),
filename,
directory
);
}
break;
}

// Collect alias so we can use it
case 'browsertime.alias': {
this.alias[message.url] = message.data;
Expand Down Expand Up @@ -211,12 +248,13 @@ export default class LatestStorerPlugin extends SitespeedioPlugin {
json.result = resultURL;
}

const data = JSON.stringify(json, undefined, 0);
return this.storageManager.writeDataToDir(
data,
name + '.' + options.browser + '.' + connectivity + '.json',
newPath
);
this.latest.push({
url: message.url,
json,
filename:
name + '.' + options.browser + '.' + connectivity + '.json',
directory: newPath
});
}
}
}
Expand Down
168 changes: 168 additions & 0 deletions test/lateststorerTests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import path from 'node:path';
import { tmpdir } from 'node:os';

import test from 'ava';
import { getLogger } from '@sitespeed.io/log';

import LatestStorerPlugin from '../lib/plugins/lateststorer/index.js';
import { messageMaker } from '../lib/support/messageMaker.js';
import timestamp from '../lib/support/time.js';

const url = 'https://www.sitespeed.io/';
const baselineTimestamp = '2023-12-19 14:18:42';

function setup(overrides = {}) {
const writes = [];
const copies = [];
const options = {
copyLatestFilesToBase: true,
browser: 'chrome',
viewPort: '1280x720',
browsertime: { iterations: 2, connectivity: { profile: 'native' } },
graphite: {},
...overrides
};
const directory = path.join(tmpdir(), 'sitespeed-lateststorer');
const context = {
messageMaker,
getLogger,
timestamp: timestamp('2023-12-21T13:10:58Z'),
resultUrls: { hasBaseUrl: () => false },
storageManager: {
getBaseDir: () => path.join(directory, '2023-12-21-13-10-58'),
async writeDataToDir(data, filename, directory) {
writes.push({ json: JSON.parse(data), filename, directory });
},
async copyFileToDir(source, destination) {
copies.push({ source, destination });
}
}
};
const plugin = new LatestStorerPlugin(options, context);
plugin.open(context, options);
return { plugin, writes, copies, directory };
}

function runMessage(pageUrl = url, iteration = 1) {
return {
type: 'browsertime.run',
url: pageUrl,
group: 'www.sitespeed.io',
iteration,
data: { title: 'Sitespeed.io' }
};
}

function compareMessage(pageUrl = url, date = baselineTimestamp) {
return {
type: 'compare.pageSummary',
url: pageUrl,
data: { meta: { baseline: { timestamp: date } } }
};
}

test('latest metadata includes the comparison baseline after summarizing', async t => {
const { plugin, writes, directory } = setup();
await plugin.processMessage({ type: 'compare.setup' });
await plugin.processMessage(runMessage());
t.is(writes.length, 0);

await plugin.processMessage(compareMessage());
await plugin.processMessage({ type: 'sitespeedio.prepareToRender' });

t.is(writes.length, 1);
t.is(writes[0].directory, directory);
t.true(writes[0].filename.endsWith('.chrome.native.json'));
t.deepEqual(writes[0].json.compare, { enabled: true, baselineTimestamp });
t.true(
writes[0].json.friendlyHTML.includes(
`using the compare plugin against baseline from ${baselineTimestamp}`
)
);
});

test('an enabled compare plugin is reported without an existing baseline', async t => {
const { plugin, writes } = setup();
await plugin.processMessage({ type: 'compare.setup' });
await plugin.processMessage(runMessage());
await plugin.processMessage({ type: 'sitespeedio.prepareToRender' });

t.deepEqual(writes[0].json.compare, { enabled: true });
t.true(writes[0].json.friendlyHTML.includes('using the compare plugin'));
t.false(writes[0].json.friendlyHTML.includes('baseline'));
});

test('compare options alone do not report an excluded plugin as enabled', async t => {
const { plugin, writes } = setup({ compare: { saveBaseline: true } });
await plugin.processMessage(runMessage());
await plugin.processMessage({ type: 'sitespeedio.prepareToRender' });

const json = writes[0].json;
t.false(Object.hasOwn(json, 'compare'));
t.false(json.friendlyHTML.includes('compare'));
t.is(json.url, url);
t.is(json.title, 'Sitespeed.io');
t.is(json.iterations, 2);
t.deepEqual(json.browser, { name: 'Unknown', version: 'unknown' });
});

test('baseline timestamps stay associated with their page URL', async t => {
const { plugin, writes } = setup();
const otherUrl = 'https://www.sitespeed.io/documentation/';
const thirdUrl = 'https://www.sitespeed.io/about/';
const otherTimestamp = '2023-12-20 10:00:00';
await plugin.processMessage({ type: 'compare.setup' });
for (const pageUrl of [url, otherUrl, thirdUrl]) {
await plugin.processMessage(runMessage(pageUrl));
}
await plugin.processMessage(compareMessage(otherUrl, otherTimestamp));
await plugin.processMessage(compareMessage());
await plugin.processMessage({ type: 'sitespeedio.prepareToRender' });

t.is(writes.length, 3);
t.is(writes[0].json.compare.baselineTimestamp, baselineTimestamp);
t.is(writes[1].json.compare.baselineTimestamp, otherTimestamp);
t.deepEqual(writes[2].json.compare, { enabled: true });
});

test('only the first iteration produces latest metadata', async t => {
const { plugin, writes } = setup();
await plugin.processMessage(runMessage());
await plugin.processMessage(runMessage(url, 2));
await plugin.processMessage({ type: 'sitespeedio.prepareToRender' });

t.is(writes.length, 1);
});

test('disabled latest file copying does not write comparison metadata', async t => {
const { plugin, writes } = setup({ copyLatestFilesToBase: false });
await plugin.processMessage({ type: 'compare.setup' });
await plugin.processMessage(runMessage());
await plugin.processMessage(compareMessage());
await plugin.processMessage({ type: 'sitespeedio.prepareToRender' });

t.is(writes.length, 0);
});

test('screenshots and video are still copied during the first run', async t => {
const { plugin, writes, copies } = setup({
browsertime: {
iterations: 2,
connectivity: { profile: 'native' },
video: true
}
});
await plugin.processMessage({
type: 'browsertime.config',
data: { screenshot: true, screenshotType: 'png' }
});
const message = runMessage();
message.data.screenshots = ['screenshots/afterPageCompleteCheck.png'];
message.data.video = 'video/1.mp4';
await plugin.processMessage(message);

t.is(copies.length, 2);
t.true(copies[0].destination.endsWith('.chrome.native.png'));
t.true(copies[1].destination.endsWith('.chrome.native.mp4'));
t.is(writes.length, 0);
});
Loading