What's new in v0.5.1
Consolidation release focused on the HTML dashboard introduced in v0.5.0. The report subcommand gains six polish items and a new flag, the daemon correlator emits a representative trace id per pair so Correlations rows become jumpable, the dashboard ships a full WAI-ARIA upgrade and an auto theme mode following prefers-color-scheme, and the Correlations / pg_stat / Diff tabs all pick up end-user ergonomics. A Playwright smoke suite now exercises the dashboard in a real Chromium browser (clipboard, CSV blob, hashchange, deep-link state, ARIA) so DOM-level regressions surface in CI, and two dashboard_{dark,light}.gif tours plus twelve per-tab still frames land under docs/img/report/ for the README. Security hardening around the report sink (CSP, input_label sanitization, cross-trace id cap), a rustls-webpki bump against RUSTSEC-2026-0104, and clearing all eleven open SonarCloud findings close the release.
Added
--pg-stat-top Nflag onreport. Override the default 10-row ranking cut. Clap-level range guard (1..=10_000) rejects zero, negatives, and implausible values before any filesystem or network I/O. When the pg_stat source is Prometheus (--pg-stat-prometheus), the scrape size widens tomax(top_n, 200)so the secondary rankings (by_calls / by_mean_time / by_io_blocks) still see the full hot-spot distribution instead of the top-N byseconds_totalonly. Documented indocs/INTEGRATION.md+ FR anddocs/design/07-CLI-CONFIG-RELEASE.md+ FR.- Dashboard: Copy link button next to Export CSV on every listable tab (Findings, pg_stat, Diff, Correlations). Writes
location.hrefto the clipboard vianavigator.clipboard.writeText, with adocument.execCommand('copy')fallback for non-HTTPS origins. The button flashes "Copied" for 1500 ms, double-click-safe via a per-button timer id. Paired with the existing deep-link hash so a shared URL restores the exact filtered view. - Dashboard: filename in the browser tab title.
<title>now readsperf-sentinel: <filename>when--inputpoints at a file, falling back toperf-sentinel reportfor stdin or wheninput_labelis missing. Makes multi-report tabs distinguishable in the browser tab bar and history. - Dashboard: clickable resolved Diff findings. Clicking a resolved row in the Diff tab jumps to Explain with an empty-state banner explaining the trace lives in the baseline Report (not the current run), so the user knows to open the baseline dashboard separately instead of blaming the renderer.
- Dashboard: clickable Correlations rows when a sample trace is available. The daemon's correlator now stores the most recent target-side
trace_idobserved per pair (PairState.last_trace_id, capped at 128 bytes, truncated on a UTF-8 boundary). The dashboard exposes this viasample_trace_idon eachCrossTraceCorrelation. Rows whosesample_trace_idresolves to an embedded trace become clickable and open Explain on that trace; rows without a sample or with a non-embedded trace stay non-interactive (same behavior as before) instead of producing broken-link UX. - Dashboard: auto theme mode following
prefers-color-scheme. The theme toggle cyclesauto->dark->light->auto.auto(the new default) reads the OS preference viawindow.matchMedia('(prefers-color-scheme: dark)')and reapplies on system change in real time. Forceddark/lightfrom prior v0.5.0 sessions keep their meaning via the existingsessionStoragepersistence, so no user-visible regression on upgrade. - Dashboard: Powered by perf-sentinel credit centered under the footer, linking to the GitHub repo.
target="_blank"+rel="noopener noreferrer"to block reverse-tabnabbing. CSP-compatible (top-level navigation is unaffected bydefault-src 'none').
Changed
- Dashboard tabs and chips carry WAI-ARIA-compliant markup.
<div role="tablist" aria-label="Dashboard sections">wraps the tab strip, each tab button carriesrole="tab"+aria-selected+aria-controls+ rovingtabindex(0 active, -1 otherwise). Left / Right arrow keys cycle focus through the visible tabs (skipping conditional tabs that are not registered), Home / End jump to first / last, Space / Enter activates. Severity filter chips form anaria-label="Finding severity"radiogroup witharia-checked, service chips become plain toggle buttons witharia-pressed. pg_stat ranking chips are aaria-label="pg_stat ranking"radiogroup. The existingg f / g e / ...shortcut state machine is untouched and co-exists on a separate input path. - Daemon correlator:
sample_trace_idonCrossTraceCorrelation.PairStategrows alast_trace_id: Option<String>, updated on every co-occurrence byPairState::update_sample_trace_id(extracted helper, short-circuits on empty strings and on values equal to the current record to avoid needless allocations on batched ingest). Serialized with#[serde(default, skip_serializing_if = "Option::is_none")]so older Report consumers keep parsing without schema changes. Ignored for the memory budget: adds ~320 KB at the 10k-pair cap, well under 2%.
Fixed
- Dashboard: URL hashchange handler. An in-page
location.hash = "#pgstat&ranking=calls"now applies state, not just a full-page navigation. Internal writes viawriteHashare tracked via a module-scoped_lastWrittenHashso the listener ignores self-triggered events and only responds to user paste / anchor clicks. - Dashboard sink: input_label sanitization + Content-Security-Policy + placeholder ordering. The
<title>{{PAGE_TITLE}}</title>and the embeddedinput_labelboth now pass throughsanitize_input_label, which strips UnicodeCc(control) andCf(format: BiDi overrides, LSEP, BOM) characters before the HTML-escape pass. A<meta http-equiv="Content-Security-Policy">tag locks the dashboard todefault-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data:; base-uri 'none'; form-action 'none', closing the class of defects where a future sink change might leak an external reference. The placeholder injection order inhtml::injectnow goes title first, JSON second, so a malicious title cannot shift the JSON payload boundary. - CLI:
--pg-stat-topsanity. Capped at 10_000 at the clap level and decoupled from the scrape size on the Prometheus path (see Added). Without the cap, a user passing--pg-stat-top 2_000_000_000would OOM the process on ingest. - Daemon correlator:
sample_trace_idlength and identical-write short-circuit. Truncated at 128 bytes on a UTF-8 character boundary so a pathological emitter cannot blow up thePairStatememory budget. The update path short-circuits when the incomingtrace_idalready matches the recorded value, avoiding ato_string()allocation on the common case of a single trace emitting multiple findings in one ingest batch. - Browser test suite: http-server binds
127.0.0.1explicitly. Without-a 127.0.0.1,http-serverdefaults to0.0.0.0and would expose the dashboard fixture to every interface on the CI runner for the duration of the suite. The browser README now documents the constraint in both EN and FR. - Dashboard demo pipeline hardening (cycleTo + themeFor + build-gif.sh). The demo's
cycleTothrows when the theme cycle never reaches the requested target,themeForthrows on a project name that does not end with-darkor-light(instead of silently writing a light still under a dark name), and thecaseinbuild-gif.shmatches the exact Playwright output directory (not a loose glob that could catch the stills projects). - All eleven SonarCloud findings cleared. Cognitive complexity on
Correlator::record_co_occurrencesdrops below the 15-point cap by extractingupdate_sample_trace_idandtruncate_to_utf8_boundary. Sixtabindex="0"attributes drop fromrole="tabpanel"containers (WAI-ARIA APG says a tabpanel with focusable children should NOT be focusable itself).Array.indexOf >= 0->Array.includes,parentNode.removeChild->childNode.remove,window.addEventListener->globalThis.addEventListener, and an explicitreturn 0in theconvert_webmbash helper. - Dependency:
rustls-webpkibumped to 0.103.13 against RUSTSEC-2026-0104 (reachable panic in certificate revocation list parsing).rustls-webpkilands transitively throughrustls 0.23 -> tokio-rustls + hyper-rustlsused by the daemon's TLS stack. Patch-level bump, semver-compatible,cargo auditgreen.
Tests
- Playwright browser smoke suite under
crates/sentinel-cli/tests/browser/. Ten specs cover page-title filename,g pkeyboard tab switch, Findings -> Explain cross-nav, Explain -> pg_stat drill-in,/search filter, Export CSV blob content (with full RFC 4180 quoting assertion on embedded commas), hash deep-link on fresh load,hashchangeon in-page paste, Copy link clipboard write, and ARIA tablist shape. Runs in a separatebrowser-testsGitHub Actions job with Node 24 + Chromium + Playwright, parallel with the Rustcheckjob so the Rust-only path is not slowed. The hand-written Rust twin of the JScsvEscapefunction retires now that Playwright covers the same contract in a real browser.
Docs
docs/img/report/grows a dashboard tour vianpm run demoin the browser-tests dir:dashboard_dark.gifanddashboard_light.gif(one Playwright tour per primary theme, ~28 s each, ffmpeg palette-optimised at 1000 px / 15 fps) plus per-tab still frames (findings,explain,pg-stat,diff,correlations,cheatsheetin both light and dark). The root README + README-FR serve the correct variant via a<picture>tag withprefers-color-scheme, matching the pattern already used for the logo and architecture diagram. The browser-tests README documents the regen cadence so casual doc tweaks do not balloon the repo with fresh ~5 MB blobs.tsconfig.json+@types/nodeland undercrates/sentinel-cli/tests/browser/so IDE TypeScript language servers no longer flag TS2705 on the async spec functions.targetpinned toES2024(the newest stable target shipped by TypeScript 5.9, Node 24 supports it natively). Playwright's own CLI transpiler is unaffected.
Install
Prebuilt binaries (Linux amd64 / arm64, macOS arm64, Windows amd64):
curl -LO https://github.com/robintra/perf-sentinel/releases/download/v0.5.1/perf-sentinel-linux-amd64
chmod +x perf-sentinel-linux-amd64
sudo mv perf-sentinel-linux-amd64 /usr/local/bin/perf-sentinelLinux binaries are statically linked against musl and run on any distribution (Alpine, Debian, RHEL, Ubuntu any version) regardless of glibc version, and inside FROM scratch images.
From crates.io:
cargo install perf-sentinelDocker:
docker run --rm -p 4317:4317 -p 4318:4318 \
ghcr.io/robintra/perf-sentinel:0.5.1 watch --listen-address 0.0.0.0Also available on Docker Hub: robintrassard/perf-sentinel:0.5.1.
Verify the binary against SHA256SUMS.txt:
curl -LO https://github.com/robintra/perf-sentinel/releases/download/v0.5.1/SHA256SUMS.txt
sha256sum -c SHA256SUMS.txt --ignore-missingFull diff: v0.5.0...v0.5.1