Skip to content

ci: use trend images in bundle-size PR comments#7012

Draft
Sheraff wants to merge 2 commits intomainfrom
opencode/curious-garden
Draft

ci: use trend images in bundle-size PR comments#7012
Sheraff wants to merge 2 commits intomainfrom
opencode/curious-garden

Conversation

@Sheraff
Copy link
Contributor

@Sheraff Sheraff commented Mar 22, 2026

Summary

  • replace the bundle-size PR comment sparkline with an inline tiny-graph image built from historical measurement timestamps and gzip byte values
  • keep the existing baseline and size reporting intact while appending the current PR measurement as the final trend point
  • update the trend note text to describe the chart-based output

Example output

Bundle Size Benchmarks

  • Commit: 616481641456
  • Measured at: 2026-03-22T20:25:12+01:00
  • Baseline source: history:b9f9ea15b696
  • Dashboard: bundle-size history
Scenario Current (gzip) Delta vs baseline Raw Brotli Trend
react-router.minimal 88.15 KiB 0 B (0.00%) 278.36 KiB 76.56 KiB Historical gzip bytes trend
react-router.full 91.38 KiB +77 B (+0.08%) 289.35 KiB 79.21 KiB Historical gzip bytes trend
solid-router.minimal 35.80 KiB 0 B (0.00%) 108.26 KiB 32.08 KiB Historical gzip bytes trend
solid-router.full 40.22 KiB +70 B (+0.17%) 121.66 KiB 36.01 KiB Historical gzip bytes trend
vue-router.minimal 53.78 KiB 0 B (0.00%) 154.49 KiB 48.23 KiB Historical gzip bytes trend
vue-router.full 58.64 KiB +96 B (+0.16%) 169.97 KiB 52.52 KiB Historical gzip bytes trend
react-start.minimal 102.56 KiB +121 B (+0.12%) 326.38 KiB 88.64 KiB Historical gzip bytes trend
react-start.full 105.95 KiB +97 B (+0.09%) 336.69 KiB 91.49 KiB Historical gzip bytes trend
solid-start.minimal 49.87 KiB 0 B (0.00%) 154.45 KiB 43.93 KiB Historical gzip bytes trend
solid-start.full 55.35 KiB +99 B (+0.17%) 170.54 KiB 48.62 KiB Historical gzip bytes trend

Trend chart uses historical gzip bytes plotted by measurement date, ending with this PR measurement; lower is better.

Summary by CodeRabbit

  • Chores
    • Refactored PR bundle size trend visualization from inline sparklines to remote chart-based graphs, improving trend clarity.
    • Modified trend data structure to track measurements as timestamped coordinate pairs (measurement date and gzip byte size).
    • Added timestamp validation to ensure only measurements with valid timestamps and sizes are included in trend reports.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 22, 2026

📝 Walkthrough

Walkthrough

The PR replaces inline ASCII sparklines with a buildTrendGraph function that constructs external chart image URLs, adds timestamp parsing/resolution helpers, converts history points to {x, y} Unix-timestamped coordinates, skips entries with non-finite timestamps/values, and deduplicates the current point before embedding a "Trend chart" image in the report.

Changes

Cohort / File(s) Summary
Trend Graph Refactoring
scripts/benchmarks/bundle-size/pr-report.mjs
Replaced inline sparkline(values) rendering with buildTrendGraph(points) that returns a markdown image or n/a. Added timestamp helpers: toUnixTimestamp, resolveHistoryEntryTimestamp, resolveCurrentTimestamp. Reworked history series to store per-bench { x: timestamp, y: Number(bench.value) }, skipping entries with non-finite timestamps or values. Appends current point only when timestamp is finite and differs from last history point by both x and y. Updated description text from "Trend sparkline …" to "Trend chart …".

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Script as Reporter Script
  participant Service as Trend Graph Service
  participant PR as PR Platform

  Script->>Script: collect history -> build points [{x,y}...]
  Script->>Service: request chart image URL with points
  Service-->>Script: returns image URL (markdown)
  Script->>PR: post report including "Trend chart" markdown image
  PR-->>Script: stores/displays report
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

🐰 I swapped sparkles for charts on the hill,
Timestamps hopping to points with a thrill,
Remote pixels bloom from gzip and time,
A rabbit drew lines and called them sublime,
✨📈

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'ci: use trend images in bundle-size PR comments' directly and clearly describes the main change: replacing sparklines with trend images in bundle-size PR comments.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch opencode/curious-garden

Comment @coderabbitai help to get the list of available commands and usage tips.

@nx-cloud
Copy link

nx-cloud bot commented Mar 22, 2026

View your CI Pipeline Execution ↗ for commit 17f5e10

Command Status Duration Result
nx affected --targets=test:eslint,test:unit,tes... ✅ Succeeded <1s View ↗
nx run-many --target=build --exclude=examples/*... ✅ Succeeded 2s View ↗

☁️ Nx Cloud last updated this comment at 2026-03-22 22:20:54 UTC

@github-actions
Copy link
Contributor

github-actions bot commented Mar 22, 2026

🚀 Changeset Version Preview

No changeset entries found. Merging this PR will not cause a version bump for any packages.

@pkg-pr-new
Copy link

pkg-pr-new bot commented Mar 22, 2026

More templates

@tanstack/arktype-adapter

npm i https://pkg.pr.new/@tanstack/arktype-adapter@7012

@tanstack/eslint-plugin-router

npm i https://pkg.pr.new/@tanstack/eslint-plugin-router@7012

@tanstack/history

npm i https://pkg.pr.new/@tanstack/history@7012

@tanstack/nitro-v2-vite-plugin

npm i https://pkg.pr.new/@tanstack/nitro-v2-vite-plugin@7012

@tanstack/react-router

npm i https://pkg.pr.new/@tanstack/react-router@7012

@tanstack/react-router-devtools

npm i https://pkg.pr.new/@tanstack/react-router-devtools@7012

@tanstack/react-router-ssr-query

npm i https://pkg.pr.new/@tanstack/react-router-ssr-query@7012

@tanstack/react-start

npm i https://pkg.pr.new/@tanstack/react-start@7012

@tanstack/react-start-client

npm i https://pkg.pr.new/@tanstack/react-start-client@7012

@tanstack/react-start-server

npm i https://pkg.pr.new/@tanstack/react-start-server@7012

@tanstack/router-cli

npm i https://pkg.pr.new/@tanstack/router-cli@7012

@tanstack/router-core

npm i https://pkg.pr.new/@tanstack/router-core@7012

@tanstack/router-devtools

npm i https://pkg.pr.new/@tanstack/router-devtools@7012

@tanstack/router-devtools-core

npm i https://pkg.pr.new/@tanstack/router-devtools-core@7012

@tanstack/router-generator

npm i https://pkg.pr.new/@tanstack/router-generator@7012

@tanstack/router-plugin

npm i https://pkg.pr.new/@tanstack/router-plugin@7012

@tanstack/router-ssr-query-core

npm i https://pkg.pr.new/@tanstack/router-ssr-query-core@7012

@tanstack/router-utils

npm i https://pkg.pr.new/@tanstack/router-utils@7012

@tanstack/router-vite-plugin

npm i https://pkg.pr.new/@tanstack/router-vite-plugin@7012

@tanstack/solid-router

npm i https://pkg.pr.new/@tanstack/solid-router@7012

@tanstack/solid-router-devtools

npm i https://pkg.pr.new/@tanstack/solid-router-devtools@7012

@tanstack/solid-router-ssr-query

npm i https://pkg.pr.new/@tanstack/solid-router-ssr-query@7012

@tanstack/solid-start

npm i https://pkg.pr.new/@tanstack/solid-start@7012

@tanstack/solid-start-client

npm i https://pkg.pr.new/@tanstack/solid-start-client@7012

@tanstack/solid-start-server

npm i https://pkg.pr.new/@tanstack/solid-start-server@7012

@tanstack/start-client-core

npm i https://pkg.pr.new/@tanstack/start-client-core@7012

@tanstack/start-fn-stubs

npm i https://pkg.pr.new/@tanstack/start-fn-stubs@7012

@tanstack/start-plugin-core

npm i https://pkg.pr.new/@tanstack/start-plugin-core@7012

@tanstack/start-server-core

npm i https://pkg.pr.new/@tanstack/start-server-core@7012

@tanstack/start-static-server-functions

npm i https://pkg.pr.new/@tanstack/start-static-server-functions@7012

@tanstack/start-storage-context

npm i https://pkg.pr.new/@tanstack/start-storage-context@7012

@tanstack/valibot-adapter

npm i https://pkg.pr.new/@tanstack/valibot-adapter@7012

@tanstack/virtual-file-routes

npm i https://pkg.pr.new/@tanstack/virtual-file-routes@7012

@tanstack/vue-router

npm i https://pkg.pr.new/@tanstack/vue-router@7012

@tanstack/vue-router-devtools

npm i https://pkg.pr.new/@tanstack/vue-router-devtools@7012

@tanstack/vue-router-ssr-query

npm i https://pkg.pr.new/@tanstack/vue-router-ssr-query@7012

@tanstack/vue-start

npm i https://pkg.pr.new/@tanstack/vue-start@7012

@tanstack/vue-start-client

npm i https://pkg.pr.new/@tanstack/vue-start-client@7012

@tanstack/vue-start-server

npm i https://pkg.pr.new/@tanstack/vue-start-server@7012

@tanstack/zod-adapter

npm i https://pkg.pr.new/@tanstack/zod-adapter@7012

commit: 17f5e10

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
scripts/benchmarks/bundle-size/pr-report.mjs (1)

304-331: ⚠️ Potential issue | 🟠 Major

Don't silently drop the current PR point when its timestamp is missing.

resolveCurrentTimestamp() can return undefined, but Line 326 requires a finite x, so the current measurement is omitted from the trend instead of being shown as the final sample.

Possible fix
-    const currentPoint = {
-      x: currentTimestamp,
-      y: metric.gzipBytes,
-    }
     const lastPoint = historySeries[historySeries.length - 1]
+    const currentPoint = Number.isFinite(metric.gzipBytes)
+      ? {
+          x:
+            Number.isFinite(currentTimestamp)
+              ? currentTimestamp
+              : Number.isFinite(lastPoint?.x)
+                ? lastPoint.x + 1
+                : 1,
+          y: Number(metric.gzipBytes),
+        }
+      : undefined
 
     if (
-      Number.isFinite(currentPoint.x) &&
+      currentPoint &&
       (!lastPoint ||
         lastPoint.x !== currentPoint.x ||
         lastPoint.y !== currentPoint.y)
     ) {
       historySeries.push(currentPoint)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/benchmarks/bundle-size/pr-report.mjs` around lines 304 - 331, The
current check drops the currentPoint when resolveCurrentTimestamp(current)
returns undefined because it requires Number.isFinite(currentPoint.x); update
the conditional that guards pushing currentPoint so it allows a missing
timestamp: replace the strict Number.isFinite(currentPoint.x) check with one
that permits null/undefined timestamps (e.g., (currentPoint.x == null ||
Number.isFinite(currentPoint.x))) while keeping the duplicate-point avoidance
logic that compares lastPoint.x/lastPoint.y; this ensures currentPoint
(constructed in currentPoint) is appended to historySeries even when
currentTimestamp is undefined.
🧹 Nitpick comments (1)
scripts/benchmarks/bundle-size/pr-report.mjs (1)

205-231: Sort each scenario by timestamp before trimming it.

This function keeps whatever order the history feed arrives in, but Line 315 later slices from the end as if it were already chronological. With explicit x values, a newest-first or partially reordered feed will draw backward in time and can trim the wrong points.

Possible fix
 function buildSeriesByScenario(historyEntries) {
   const map = new Map()
@@
   }
 
+  for (const points of map.values()) {
+    points.sort((a, b) => a.x - b.x)
+  }
+
   return map
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/benchmarks/bundle-size/pr-report.mjs` around lines 205 - 231, The
buildSeriesByScenario function accumulates points per scenario but doesn't
guarantee chronological order; after collecting points for each bench name (map
entries), sort each series by the point timestamp (the x property) in ascending
order so later slicing/trimming (which assumes chronological order) operates on
correct data; locate buildSeriesByScenario and after the loop that pushes {x:
timestamp, y: Number(bench.value)} into map.get(bench.name), iterate the map
entries and sort each array by (a.x - b.x) (use resolveHistoryEntryTimestamp
where relevant to ensure numeric timestamps) before returning the map.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@scripts/benchmarks/bundle-size/pr-report.mjs`:
- Around line 304-331: The current check drops the currentPoint when
resolveCurrentTimestamp(current) returns undefined because it requires
Number.isFinite(currentPoint.x); update the conditional that guards pushing
currentPoint so it allows a missing timestamp: replace the strict
Number.isFinite(currentPoint.x) check with one that permits null/undefined
timestamps (e.g., (currentPoint.x == null || Number.isFinite(currentPoint.x)))
while keeping the duplicate-point avoidance logic that compares
lastPoint.x/lastPoint.y; this ensures currentPoint (constructed in currentPoint)
is appended to historySeries even when currentTimestamp is undefined.

---

Nitpick comments:
In `@scripts/benchmarks/bundle-size/pr-report.mjs`:
- Around line 205-231: The buildSeriesByScenario function accumulates points per
scenario but doesn't guarantee chronological order; after collecting points for
each bench name (map entries), sort each series by the point timestamp (the x
property) in ascending order so later slicing/trimming (which assumes
chronological order) operates on correct data; locate buildSeriesByScenario and
after the loop that pushes {x: timestamp, y: Number(bench.value)} into
map.get(bench.name), iterate the map entries and sort each array by (a.x - b.x)
(use resolveHistoryEntryTimestamp where relevant to ensure numeric timestamps)
before returning the map.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f0284042-31be-4ef0-a4ea-8e80e5a4fa61

📥 Commits

Reviewing files that changed from the base of the PR and between 6164816 and ce6f4e4.

📒 Files selected for processing (1)
  • scripts/benchmarks/bundle-size/pr-report.mjs

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
scripts/benchmarks/bundle-size/pr-report.mjs (2)

375-375: The footer text is slightly stronger than the resolution logic.

Line 375 says the series is plotted by “measurement date”, but the code can fall back to commit.timestamp and generatedAt. “Timestamp” would match the implemented sources better.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/benchmarks/bundle-size/pr-report.mjs` at line 375, Update the footer
string literal that currently reads "_Trend chart uses historical gzip bytes
plotted by measurement date, ending with this PR measurement; lower is better._"
to use "timestamp" (or explicitly mention the possible sources) so it matches
the implemented fallback behavior (measurement.timestamp, commit.timestamp or
generatedAt); e.g. replace "measurement date" with "timestamp
(measurement.timestamp, commit.timestamp, or generatedAt)".

210-236: Sort each scenario series before trimming/appending.

The chart path now compares against lastPoint and appends the PR measurement to the tail, so it implicitly assumes historyEntries is already chronological. Sorting each series by x here would make the graph and duplicate check deterministic even if the history feed arrives out of order.

♻️ Suggested hardening
 function buildSeriesByScenario(historyEntries) {
   const map = new Map()
@@
       map.get(bench.name).push({
         x: timestamp,
         y: Number(bench.value),
       })
     }
   }
+
+  for (const points of map.values()) {
+    points.sort((a, b) => a.x - b.x)
+  }
 
   return map
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/benchmarks/bundle-size/pr-report.mjs` around lines 210 - 236, In
buildSeriesByScenario: the per-scenario arrays are not guaranteed to be
chronological, which breaks the later duplicate check and appending logic; after
populating each Map entry (i.e., after map.get(bench.name).push(...)) sort each
series by the x timestamp (use a numeric comparator like (a,b) => a.x - b.x) so
each series is deterministic and ordered before any trimming or comparison
operations; do the sort once after the loop (iterate map.values()) to keep
behavior stable even if historyEntries is out-of-order.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@scripts/benchmarks/bundle-size/pr-report.mjs`:
- Around line 320-337: The code trims history before checking whether to append
the current point, which causes the series to shrink when the current point is a
duplicate; fix by first getting the full series (const fullSeries =
seriesByScenario.get(metric.id) || []), compute currentPoint (using
currentTimestamp and metric.gzipBytes) and determine duplication against
fullSeries's last point (check Number.isFinite(currentPoint.x) and compare x/y);
then set historySeries = fullSeries.slice(willAppend ? -args.trendPoints + 1 :
-args.trendPoints) and only push currentPoint when willAppend is true (push to
historySeries when not duplicate). Ensure you reference historySeries,
seriesByScenario, currentPoint, currentTimestamp, metric.gzipBytes, and
args.trendPoints.

---

Nitpick comments:
In `@scripts/benchmarks/bundle-size/pr-report.mjs`:
- Line 375: Update the footer string literal that currently reads "_Trend chart
uses historical gzip bytes plotted by measurement date, ending with this PR
measurement; lower is better._" to use "timestamp" (or explicitly mention the
possible sources) so it matches the implemented fallback behavior
(measurement.timestamp, commit.timestamp or generatedAt); e.g. replace
"measurement date" with "timestamp (measurement.timestamp, commit.timestamp, or
generatedAt)".
- Around line 210-236: In buildSeriesByScenario: the per-scenario arrays are not
guaranteed to be chronological, which breaks the later duplicate check and
appending logic; after populating each Map entry (i.e., after
map.get(bench.name).push(...)) sort each series by the x timestamp (use a
numeric comparator like (a,b) => a.x - b.x) so each series is deterministic and
ordered before any trimming or comparison operations; do the sort once after the
loop (iterate map.values()) to keep behavior stable even if historyEntries is
out-of-order.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c82a8c19-6b8f-4dc4-b534-aff172b83e65

📥 Commits

Reviewing files that changed from the base of the PR and between ce6f4e4 and 17f5e10.

📒 Files selected for processing (1)
  • scripts/benchmarks/bundle-size/pr-report.mjs

Comment on lines 320 to 337
const historySeries = (seriesByScenario.get(metric.id) || []).slice(
// Reserve one slot for the current metric so the sparkline stays at trendPoints.
// Reserve one slot for the current metric so the trend stays at trendPoints.
-args.trendPoints + 1,
)
const currentPoint = {
x: currentTimestamp,
y: metric.gzipBytes,
}
const lastPoint = historySeries[historySeries.length - 1]

if (
!historySeries.length ||
historySeries[historySeries.length - 1] !== metric.gzipBytes
Number.isFinite(currentPoint.x) &&
(!lastPoint ||
lastPoint.x !== currentPoint.x ||
lastPoint.y !== currentPoint.y)
) {
historySeries.push(metric.gzipBytes)
historySeries.push(currentPoint)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Only reserve a slot when the current point is actually appended.

Lines 320-337 always pre-trim to trendPoints - 1, but the current point is skipped when it exactly matches the latest historical point. In that case the chart drops to 11 points instead of 12. Check duplication against the full scenario series first, then reserve a slot only when currentPoint will be added.

🐛 Suggested fix
-    const historySeries = (seriesByScenario.get(metric.id) || []).slice(
-      // Reserve one slot for the current metric so the trend stays at trendPoints.
-      -args.trendPoints + 1,
-    )
+    const scenarioSeries = seriesByScenario.get(metric.id) || []
     const currentPoint = {
       x: currentTimestamp,
       y: metric.gzipBytes,
     }
-    const lastPoint = historySeries[historySeries.length - 1]
+    const lastPoint = scenarioSeries[scenarioSeries.length - 1]
+    const shouldAppendCurrent =
+      Number.isFinite(currentPoint.x) &&
+      Number.isFinite(currentPoint.y) &&
+      (!lastPoint ||
+        lastPoint.x !== currentPoint.x ||
+        lastPoint.y !== currentPoint.y)
+    const historySeries = scenarioSeries.slice(
+      -(args.trendPoints - (shouldAppendCurrent ? 1 : 0)),
+    )
 
-    if (
-      Number.isFinite(currentPoint.x) &&
-      (!lastPoint ||
-        lastPoint.x !== currentPoint.x ||
-        lastPoint.y !== currentPoint.y)
-    ) {
+    if (shouldAppendCurrent) {
       historySeries.push(currentPoint)
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/benchmarks/bundle-size/pr-report.mjs` around lines 320 - 337, The
code trims history before checking whether to append the current point, which
causes the series to shrink when the current point is a duplicate; fix by first
getting the full series (const fullSeries = seriesByScenario.get(metric.id) ||
[]), compute currentPoint (using currentTimestamp and metric.gzipBytes) and
determine duplication against fullSeries's last point (check
Number.isFinite(currentPoint.x) and compare x/y); then set historySeries =
fullSeries.slice(willAppend ? -args.trendPoints + 1 : -args.trendPoints) and
only push currentPoint when willAppend is true (push to historySeries when not
duplicate). Ensure you reference historySeries, seriesByScenario, currentPoint,
currentTimestamp, metric.gzipBytes, and args.trendPoints.

@Sheraff Sheraff marked this pull request as draft March 23, 2026 06:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant