github robintra/perf-sentinel v0.9.26

latest release: chart-v0.9.26
3 hours ago

What's new in v0.9.26

v0.9.26 is entirely about the HTML report, and about one question it kept answering badly: which spans is this finding actually talking about. Two finding types highlighted the container instead of the calls, the list led with whichever type sorted first rather than with the worst rows, and the help icon that explains a detector had become unreachable to anyone not using a mouse. Nothing else in the pipeline moves.

The trace tree names the calls, not the container

A serialized_calls or excessive_fanout finding now highlights the spans it reports. Both name their parent span in pattern.template, so the template rule the tree uses lit the container above the calls, and for a serialized chain nothing better was available: an embedded span carries a duration but no timestamp, so a browser cannot work out which siblings overlap and which run one after the other. The chain was therefore shown with no highlight at all, and the fanout with its parent lit and its children dark.

The detectors know the answer, so the sink asks them again over the traces it embeds and ships the span ids alongside the report. That costs nothing on any other finding type, whose culprits are already derivable from the template or the event type.

Three properties make those ids trustworthy rather than decorative. Each rerun result is matched back to a finding the report actually publishes and dropped when it has none, so an acknowledged or trimmed finding never ships spans no view can key on. The key carries the finding's time bounds beside its signature, because serialized_calls takes both its service and its endpoint from the parent span, which means two chains under two parents on the same route hash identically, and a key two findings still claim is dropped rather than published as the union of both, since lighting one chain's spans under the other's heading is worse than lighting nothing. And the ids are withheld entirely when the report carries no detection_config, because re-running a detector under thresholds other than the ones that produced the findings would name the wrong spans with full confidence.

The sibling rows the tree folds together follow the same rule: culprit membership joins the fold key, so a group can no longer mix reported and unreported siblings behind a single row that speaks for both.

The list leads with the worst rows

The findings list is sorted by descending severity. It used to arrive in the deterministic order the detectors emit, which groups by finding type first, so a critical N+1 HTTP could sit on the second page under eight warnings while the first page opened on an info. The sort is stable, so that detector order survives inside each severity band, and it happens on the filtered data rather than on the rendered rows, so the paginated list, the counter, the sidebar badge and the CSV export all agree on one order. The diff columns and the correlation list already worked this way.

The payload itself is untouched: report.findings still ships in detector order, which every other sink preserves.

The help icon is a control

The ? beside a finding type, a data tab or a carbon card used to sit inside its heading. It carries its help text as an aria-label, so a screen reader folded that whole paragraph into the heading's accessible name, and the heading's text no longer matched the label shown on the finding row.

Moving it out of the heading fixed both, and would have made it unreachable instead: an aria-label on a plain span is not exposed, and hover was the only way in. It is now a real control, reachable by tab, showing its summary on focus and opening its method sheet on Enter or Space, which it never did with a mouse either.

Two tooltip defects go with it. A tooltip now widens to fit a code snippet rather than letting it spill past its border, since the snippet is deliberately unbreakable so a threshold key is never cut across two lines, and it is clamped at the left viewport edge as well as the right, where a wide box near the right of a narrow window used to push its first characters off screen. Switching tabs no longer strands one either: the topbar icon is rebuilt on every route change, so a hovered one was removed without ever seeing its mouseleave and a keyboard route change left the box floating over the dashboard.

Upgrade impact

Nothing changes outside the HTML report. No detector, threshold, score, exit code, metric or output format moves.

One detail can move a serialized_calls finding's text, and it is a fix rather than a side effect. The scan sorts sibling spans by end time, and two siblings ending in the same microsecond used to be ordered arbitrarily, which decided which of two equally long chains was reported. That tie is now broken on the span index, so the choice is stable across runs and machines instead of depending on the sort's internals. On a trace holding such a tie, the reported chain may differ from the one 0.9.25 happened to pick, with the same length and the same verdict.

Two things change for a reader of the dashboard. Findings are ordered by severity rather than by type, which also reorders the CSV that the Findings tab exports. And the report's embedded payload gains a culprit_spans object, additive, absent when there is nothing to highlight.

For downstream crates, nothing on the public API moves. The two detectors keep their signatures and their results.

On Windows, the release build no longer emits a dead-code warning for a constant that only the Unix termination path reads.

Why this is a patch and not a minor

The pre-1.0 scheme bumps the third number for features too. Nothing is removed, renamed or given a different meaning, no public type or function changes shape, and the one payload addition is additive. A pipeline that consumes any format other than the HTML report cannot tell 0.9.25 from 0.9.26.

Validation

The full Rust suite passes (2924 tests, 5 of them new and covering the culprit-span rules: the exact chain among overlapping siblings, two chains sharing a signature keeping their own spans, every child of a fanout parent, and the two cases where the ids must be withheld). Clippy is clean at -D warnings in the default configuration and with the daemon feature, cargo fmt --check passes, and the default and --no-default-features builds both pass. The 27-test Playwright suite passes against a freshly built binary, and the highlight, the keyboard path and the tooltip clamp were each measured in a browser on the rendered report.

The simulation-lab gate was not run for this version. It exercises the daemon and the batch pipeline, neither of which this release touches: every change is in the report sink and its template, downstream of detection and scoring.

Verifying this release

# Binary integrity via SLSA Build L3 attestation
gh attestation verify perf-sentinel-linux-amd64 \
  --repo robintra/perf-sentinel

# A periodic disclosure produced by this binary
perf-sentinel verify-hash --report perf-sentinel-report.json \
  --expected-identity "https://github.com/robintra/perf-sentinel/.github/workflows/release.yml@refs/tags/v0.9.26" \
  --expected-issuer "https://token.actions.githubusercontent.com" \
  --verify-binary ./perf-sentinel-linux-amd64

gh CLI 2.49 or newer required for gh attestation verify.

Full Changelog: v0.9.25...v0.9.26

Don't miss a new perf-sentinel release

NewReleases is sending notifications on new releases.