Skip to content

rage-restart followup#92591

Merged
wbinnssmith merged 9 commits intocanaryfrom
wbinnssmith/rage-restart-git-branch
Apr 21, 2026
Merged

rage-restart followup#92591
wbinnssmith merged 9 commits intocanaryfrom
wbinnssmith/rage-restart-git-branch

Conversation

@wbinnssmith
Copy link
Copy Markdown
Member

@wbinnssmith wbinnssmith commented Apr 9, 2026

What?

Two behaviour changes to the rage-restart detection in next dev, plus a refactor of the git helper code:

  1. Lower the rage-restart threshold from 120s to 90s. The previous window was too generous; 90 s is a better match for the workflow of stopping and immediately restarting the dev server.

  2. Don't count a restart as a rage-restart if the git branch changed. A common workflow is: stop the dev server, git checkout <other-branch>, restart. That sequence has nothing to do with frustration — it's intentional. The dev state file now records the current git branch on shutdown, and on startup the recorded branch is compared to the current one. If they differ the restart is not flagged. If either branch is unknown (detached HEAD, not a git repo, etc.) the comparison is skipped and we fall back to time-only detection, preserving the existing behaviour.

Why?

The rage-restart signal is meant to detect genuine frustration with the dev server (e.g. it's hanging or erroring on startup). Switching branches is an intentional, deliberate action that happens to look the same from a pure timing perspective. Capturing it as rage-restart pollutes telemetry and could trigger unwanted recovery behaviour.

The git helper consolidation removes duplicated spawnSync / execSync calls spread across unrelated files, centralises the VERCEL_GIT_* env-var fallback logic, and makes it easy to add further git queries in one place.

How?

  • writeDevState() in next-dev.ts now calls getGitBranch(dir) and stores the result under a gitBranch key in the dev state file (omitted when the branch is unknown).
  • On startup, after reading the dev state file, the stored gitBranch is compared to the freshly-read getGitBranch(dir). isRageRestart is only set when the branch either hasn't changed or couldn't be determined.
  • packages/next/src/lib/helpers/git.ts provides gitExec() (private), getGitBranch(), and getGitCommit(). It replaces the deleted get-git-branch.ts and the open-coded child_process.spawnSync calls in trace-uploader.ts.

wbinnssmith and others added 4 commits April 9, 2026 20:14
…nged

- Reduce RAGE_RESTART_THRESHOLD_MS from 120s to 90s.
- Store the current git branch in dev-state.json on shutdown. On startup,
  if the stored branch and current branch are both known but differ, don't
  flag the restart as a rage restart — the user switched branches.
- Add a test case that verifies branch changes suppress the rage-restart flag.

Co-Authored-By: Claude <noreply@anthropic.com>
Move the inline getGitBranch() from next-dev.ts into
packages/next/src/lib/helpers/get-git-branch.ts and import it from both
next-dev.ts and trace-uploader.ts (which had its own open-coded version).

Co-Authored-By: Claude <noreply@anthropic.com>
Replace the single-function get-git-branch.ts with a general git.ts
module. Add getGitCommit() alongside getGitBranch() and use it in
trace-uploader.ts to replace its remaining open-coded spawnSync call,
removing the child_process import from that file entirely.

Co-Authored-By: Claude <noreply@anthropic.com>
Callers no longer need to know about these env vars; the preference
logic lives alongside the git queries that it overrides.

Co-Authored-By: Claude <noreply@anthropic.com>
@nextjs-bot
Copy link
Copy Markdown
Contributor

nextjs-bot commented Apr 9, 2026

Tests Passed

@wbinnssmith wbinnssmith changed the title dev: lower rage-restart threshold and skip on git branch change rage-restart followup Apr 9, 2026
@wbinnssmith wbinnssmith requested a review from lukesandberg April 9, 2026 22:05
@nextjs-bot
Copy link
Copy Markdown
Contributor

nextjs-bot commented Apr 9, 2026

Stats from current PR

✅ No significant changes detected

📊 All Metrics
📖 Metrics Glossary

Dev Server Metrics:

  • Listen = TCP port starts accepting connections
  • First Request = HTTP server returns successful response
  • Cold = Fresh build (no cache)
  • Warm = With cached build artifacts

Build Metrics:

  • Fresh = Clean build (no .next directory)
  • Cached = With existing .next directory

Change Thresholds:

  • Time: Changes < 50ms AND < 10%, OR < 2% are insignificant
  • Size: Changes < 1KB AND < 1% are insignificant
  • All other changes are flagged to catch regressions

⚡ Dev Server

Metric Canary PR Change Trend
Cold (Listen) 455ms 456ms ▅▅█▅▅
Cold (Ready in log) 446ms 445ms ▅▃▇▇█
Cold (First Request) 1.114s 1.139s ▂▂█▄█
Warm (Listen) 456ms 456ms █▁███
Warm (Ready in log) 448ms 444ms ▆▁▆▇█
Warm (First Request) 350ms 350ms █▄▅▆█
📦 Dev Server (Webpack) (Legacy)

📦 Dev Server (Webpack)

Metric Canary PR Change Trend
Cold (Listen) 456ms 455ms ▁▅▅█▁
Cold (Ready in log) 439ms 440ms ▁▁▅█▅
Cold (First Request) 1.930s 1.932s ▅▁▇█▆
Warm (Listen) 456ms 456ms ▁▁▁▁▁
Warm (Ready in log) 438ms 438ms ▁▂▅█▆
Warm (First Request) 1.931s 1.938s ▆▁▇█▆

⚡ Production Builds

Metric Canary PR Change Trend
Fresh Build 3.860s 3.940s ▆▁▅▆█
Cached Build 3.957s 3.923s ▆▄██▇
📦 Production Builds (Webpack) (Legacy)

📦 Production Builds (Webpack)

Metric Canary PR Change Trend
Fresh Build 14.526s 14.556s ▂▂▄▆▃
Cached Build 14.632s 14.637s ▃▃▄█▄
node_modules Size 492 MB 492 MB ▁▁███
📦 Bundle Sizes

Bundle Sizes

⚡ Turbopack

Client

Main Bundles
Canary PR Change
0-xemccipzit0.js gzip 13 kB N/A -
00-sfwc7hh0nr.js gzip 12.9 kB N/A -
0cz1d0mv5g_q7.js gzip 39.4 kB 39.4 kB
0f46ogy4oqhbg.js gzip 155 B N/A -
0ksxp7o1hx8pf.js gzip 167 B N/A -
0z6zj7qe8s-s2.js gzip 157 B N/A -
10pt4w_1muouh.js gzip 155 B N/A -
13_2qj6sfhcpr.js gzip 8.51 kB N/A -
13q15tdry7-jw.js gzip 9.81 kB N/A -
16jdy7mb2hpzo.js gzip 2.28 kB N/A -
16lhqjoqbznyg.js gzip 220 B 220 B
17c6iioxxyq_7.js gzip 8.51 kB N/A -
1c5i9886ljzvw.js gzip 152 B N/A -
1dckul_xzwsx_.js gzip 155 B N/A -
1e9hak60wi8_q.js gzip 10.1 kB N/A -
1elt1qium-r2m.css gzip 115 B 115 B
1f8lc2681z647.js gzip 154 B N/A -
1lti_nqcijcmi.js gzip 225 B N/A -
1m9l9vnf18-38.js gzip 49 kB N/A -
1zq04q8id1dsq.js gzip 8.59 kB N/A -
246le60fytek6.js gzip 1.46 kB N/A -
25c1ukc-mir52.js gzip 70.8 kB N/A -
2ipc3se0d9mja.js gzip 7.61 kB N/A -
2rm1ibbmjhlgi.js gzip 8.57 kB N/A -
2yj-ir7tizolc.js gzip 157 B N/A -
2zm7mepei8xku.js gzip 158 B N/A -
30ls8fwpkrnuy.js gzip 155 B N/A -
30z650ayitjz3.js gzip 5.67 kB N/A -
33ur7c3w36-m-.js gzip 8.56 kB N/A -
35rz-np9njhmv.js gzip 158 B N/A -
36acwz6drq2cy.js gzip 154 B N/A -
377ai6ro_mien.js gzip 65.5 kB N/A -
396buwq-nlhir.js gzip 8.59 kB N/A -
39mk1fjm58e-4.js gzip 8.62 kB N/A -
3cq10epinkxrc.js gzip 450 B N/A -
3t2qn5usd1f-e.js gzip 156 B N/A -
3wc1tgfurjhi-.js gzip 9.23 kB N/A -
3ze9s70gat6n_.js gzip 8.56 kB N/A -
41obdnb4lqdgs.js gzip 13.3 kB N/A -
454bom347xpxj.js gzip 13.8 kB N/A -
457x5n-k0jr1x.js gzip 10.4 kB N/A -
turbopack-00..186v.js gzip 4.17 kB N/A -
turbopack-0e..0dyk.js gzip 4.19 kB N/A -
turbopack-0l..avon.js gzip 4.17 kB N/A -
turbopack-0n..lai4.js gzip 4.17 kB N/A -
turbopack-16..zovp.js gzip 4.17 kB N/A -
turbopack-1a..polh.js gzip 4.17 kB N/A -
turbopack-1s..l6v_.js gzip 4.17 kB N/A -
turbopack-1v..cs5b.js gzip 4.17 kB N/A -
turbopack-2-..z_z1.js gzip 4.17 kB N/A -
turbopack-25..h1ve.js gzip 4.17 kB N/A -
turbopack-28..pcbs.js gzip 4.17 kB N/A -
turbopack-2z..hyi3.js gzip 4.17 kB N/A -
turbopack-3t..arfg.js gzip 4.16 kB N/A -
turbopack-44..b1wm.js gzip 4.18 kB N/A -
0_1u_xrpzaeaj.js gzip N/A 8.52 kB -
0-ua_-urjvdtw.js gzip N/A 8.56 kB -
001tqi6o0z1t9.js gzip N/A 156 B -
05_r_-_rf4w-n.js gzip N/A 7.61 kB -
0eihfygkvyao-.js gzip N/A 1.46 kB -
0g_88ua4o_jp-.js gzip N/A 9.24 kB -
0kvc43dagz7f6.js gzip N/A 157 B -
0pk3a0imvjxyg.js gzip N/A 156 B -
0ua91j3aes80c.js gzip N/A 8.58 kB -
0zwsxw6xkvw9p.js gzip N/A 8.62 kB -
1_fyx0hi94qc-.js gzip N/A 49 kB -
10mvvt3xn1_3j.js gzip N/A 8.59 kB -
13v7vkq3viabh.js gzip N/A 70.8 kB -
13wh3pltnc-77.js gzip N/A 156 B -
1904znfimgi15.js gzip N/A 156 B -
1b75ishu64v5s.js gzip N/A 13 kB -
1b83ah3nflxjf.js gzip N/A 8.52 kB -
1brt7q153-qnw.js gzip N/A 65.5 kB -
1f2et7pys_lyt.js gzip N/A 157 B -
1fd23spooi5r7.js gzip N/A 225 B -
1nw99o32asytf.js gzip N/A 450 B -
1owct1x5874t1.js gzip N/A 157 B -
1uboyp1_w1n83.js gzip N/A 161 B -
1x-xn_y5xlhug.js gzip N/A 151 B -
1y6qa6xp0i1nz.js gzip N/A 13.3 kB -
25hjq5q0t8w_3.js gzip N/A 168 B -
2kxdvc3gr7nt9.js gzip N/A 8.59 kB -
2qn63kv8i9bty.js gzip N/A 155 B -
2sgg_sxyixu_p.js gzip N/A 13.8 kB -
2sk4gp5rmalb0.js gzip N/A 10.1 kB -
2u87ln5_zfir_.js gzip N/A 5.67 kB -
2w3dswowq0syc.js gzip N/A 162 B -
33602db0fe2xi.js gzip N/A 9.81 kB -
3j3snr-ce7e0q.js gzip N/A 10.4 kB -
3ousyyvaw2mdb.js gzip N/A 156 B -
3yby446qbgls0.js gzip N/A 8.56 kB -
3yypm2pwzx0mq.js gzip N/A 12.9 kB -
42lado0_6oegq.js gzip N/A 2.28 kB -
turbopack-08..x59c.js gzip N/A 4.18 kB -
turbopack-0d..8f4o.js gzip N/A 4.18 kB -
turbopack-0f..c2p2.js gzip N/A 4.16 kB -
turbopack-0g..u8-b.js gzip N/A 4.19 kB -
turbopack-0q.._3ql.js gzip N/A 4.18 kB -
turbopack-1n..l6b0.js gzip N/A 4.18 kB -
turbopack-1s..rst2.js gzip N/A 4.18 kB -
turbopack-2n..9un6.js gzip N/A 4.18 kB -
turbopack-2r..9e9u.js gzip N/A 4.18 kB -
turbopack-2w.._-oe.js gzip N/A 4.18 kB -
turbopack-3-..hz1i.js gzip N/A 4.18 kB -
turbopack-34..c6vu.js gzip N/A 4.18 kB -
turbopack-37..pbkw.js gzip N/A 4.18 kB -
turbopack-40..gi5p.js gzip N/A 4.18 kB -
Total 464 kB 465 kB ⚠️ +135 B

Server

Middleware
Canary PR Change
middleware-b..fest.js gzip 714 B 719 B
Total 714 B 719 B ⚠️ +5 B
Build Details
Build Manifests
Canary PR Change
_buildManifest.js gzip 435 B 434 B
Total 435 B 434 B ✅ -1 B

📦 Webpack

Client

Main Bundles
Canary PR Change
1011-HASH.js gzip 5.58 kB N/A -
2168.HASH.js gzip 169 B N/A -
2225-HASH.js gzip 4.64 kB N/A -
61a8f394-HASH.js gzip 62.8 kB N/A -
850-HASH.js gzip 60.6 kB N/A -
framework-HASH.js gzip 59.7 kB 59.7 kB
main-app-HASH.js gzip 257 B 251 B 🟢 6 B (-2%)
main-HASH.js gzip 39.3 kB 39.6 kB
webpack-HASH.js gzip 1.68 kB 1.68 kB
36c7d9a6-HASH.js gzip N/A 62.8 kB -
3967-HASH.js gzip N/A 4.63 kB -
5025-HASH.js gzip N/A 5.58 kB -
634-HASH.js gzip N/A 60.9 kB -
7586.HASH.js gzip N/A 170 B -
Total 235 kB 235 kB ⚠️ +607 B
Polyfills
Canary PR Change
polyfills-HASH.js gzip 39.4 kB 39.4 kB
Total 39.4 kB 39.4 kB
Pages
Canary PR Change
_app-HASH.js gzip 194 B 194 B
_error-HASH.js gzip 182 B 181 B
css-HASH.js gzip 334 B 333 B
dynamic-HASH.js gzip 1.8 kB 1.81 kB
edge-ssr-HASH.js gzip 255 B 254 B
head-HASH.js gzip 352 B 352 B
hooks-HASH.js gzip 384 B 384 B
image-HASH.js gzip 580 B 581 B
index-HASH.js gzip 259 B 259 B
link-HASH.js gzip 2.52 kB 2.52 kB
routerDirect..HASH.js gzip 320 B 317 B
script-HASH.js gzip 386 B 386 B
withRouter-HASH.js gzip 315 B 315 B
1afbb74e6ecf..834.css gzip 106 B 106 B
Total 7.98 kB 7.99 kB ⚠️ +4 B

Server

Edge SSR
Canary PR Change
edge-ssr.js gzip 125 kB 126 kB
page.js gzip 272 kB 273 kB
Total 398 kB 399 kB ⚠️ +1.32 kB
Middleware
Canary PR Change
middleware-b..fest.js gzip 613 B 623 B 🔴 +10 B (+2%)
middleware-r..fest.js gzip 156 B 156 B
middleware.js gzip 44.4 kB 44 kB
edge-runtime..pack.js gzip 842 B 842 B
Total 46 kB 45.6 kB ✅ -368 B
Build Details
Build Manifests
Canary PR Change
_buildManifest.js gzip 719 B 718 B
Total 719 B 718 B ✅ -1 B
Build Cache
Canary PR Change
0.pack gzip 4.38 MB 4.37 MB
index.pack gzip 114 kB 115 kB 🔴 +1.17 kB (+1%)
index.pack.old gzip 112 kB 112 kB
Total 4.6 MB 4.6 MB ✅ -2.66 kB

🔄 Shared (bundler-independent)

Runtimes
Canary PR Change
app-page-exp...dev.js gzip 345 kB 345 kB
app-page-exp..prod.js gzip 191 kB 191 kB
app-page-tur...dev.js gzip 345 kB 345 kB
app-page-tur..prod.js gzip 191 kB 191 kB
app-page-tur...dev.js gzip 341 kB 341 kB
app-page-tur..prod.js gzip 189 kB 189 kB
app-page.run...dev.js gzip 342 kB 342 kB
app-page.run..prod.js gzip 189 kB 189 kB
app-route-ex...dev.js gzip 76.9 kB 76.9 kB
app-route-ex..prod.js gzip 52.5 kB 52.5 kB
app-route-tu...dev.js gzip 77 kB 77 kB
app-route-tu..prod.js gzip 52.5 kB 52.5 kB
app-route-tu...dev.js gzip 76.6 kB 76.6 kB
app-route-tu..prod.js gzip 52.2 kB 52.2 kB
app-route.ru...dev.js gzip 76.5 kB 76.5 kB
app-route.ru..prod.js gzip 52.2 kB 52.2 kB
dist_client_...dev.js gzip 324 B 324 B
dist_client_...dev.js gzip 326 B 326 B
dist_client_...dev.js gzip 318 B 318 B
dist_client_...dev.js gzip 317 B 317 B
pages-api-tu...dev.js gzip 43.9 kB 43.9 kB
pages-api-tu..prod.js gzip 33.5 kB 33.5 kB
pages-api.ru...dev.js gzip 43.8 kB 43.8 kB
pages-api.ru..prod.js gzip 33.4 kB 33.4 kB
pages-turbo....dev.js gzip 53.3 kB 53.3 kB
pages-turbo...prod.js gzip 39.1 kB 39.1 kB
pages.runtim...dev.js gzip 53.2 kB 53.2 kB
pages.runtim..prod.js gzip 39 kB 39 kB
server.runti..prod.js gzip 62.8 kB 62.8 kB
Total 3.05 MB 3.05 MB ✅ -1 B
📎 Tarball URL
https://vercel-packages.vercel.app/next/commits/3b07445eb210dc9e0bb830a89e794a80618131e3/next

git rev-parse --abbrev-ref HEAD outputs "HEAD" (exit 0) on a fresh repo
with no commits, which the helper converted to an empty string. This made
the branch-change check always fall back to time-only detection in test
fixtures that initialise a bare git repo. Switch to
git symbolic-ref --short HEAD, which returns the branch name correctly on
empty repos and exits non-zero in detached HEAD state (caught and
returned as "").

Co-Authored-By: Claude <noreply@anthropic.com>
Comment thread packages/next/src/cli/next-dev.ts Outdated
Extract a named DevStateEntry type in next-dev.ts to replace the two
separate inline type casts for reading and writing the dev-state file.
This avoids duplicating the shape and makes it clear all fields are
optional for backward compatibility with older state entries.

Also update the next-server-nft inline snapshot to include git.js, which
now appears in the standalone trace because trace-uploader.ts imports
from the new helpers/git module.

Co-Authored-By: Claude <noreply@anthropic.com>
Comment thread packages/next/src/lib/helpers/git.ts Outdated
Comment thread packages/next/src/lib/helpers/git.ts Outdated
wbinnssmith and others added 3 commits April 9, 2026 23:00
getGitBranch and getGitCommit now return string | undefined on failure
rather than ''. Empty string was semantically ambiguous; undefined makes
the "unknown" case explicit and lets callers use ?? / optional chaining
naturally. Update TraceMetadata.branch/commit to optional accordingly.

Co-Authored-By: Claude <noreply@anthropic.com>
The API server expects branch and commit to always be string fields.
After getGitBranch/getGitCommit were changed to return string | undefined,
JSON.stringify would silently drop undefined values, changing the wire
format. Normalize at the call site with ?? '' so the payload stays
backward compatible while keeping the cleaner internal return type.

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
@wbinnssmith wbinnssmith marked this pull request as ready for review April 9, 2026 23:29
@wbinnssmith wbinnssmith requested a review from bgw April 9, 2026 23:49
@wbinnssmith wbinnssmith merged commit 162016b into canary Apr 21, 2026
171 checks passed
@wbinnssmith wbinnssmith deleted the wbinnssmith/rage-restart-git-branch branch April 21, 2026 22:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants