fix: error-overview sweep — TDZ import cycle, expired-session UX, Sentry double-reports, miner deadlock retry#803
Conversation
…try double-reports, miner deadlock retry
Fixes for every live error across Vercel runtime, admin_events, and Sentry (7d window):
- CoachHelm cold-start TDZ ("Cannot access 'a' before initialization" in
generateAlerts): trigger-insights-bridge lazily import()ed insights.ts,
the dynamic back-edge of a value-level import cycle (insights.ts
statically imports the bridge's register fn). Two concurrent first-touches
on a cold instance — a background after() round trigger racing a dashboard
action — could observe insights.ts mid-evaluation. The bridge no longer
imports insights.ts; both real callers (post-round-trigger, roster-sweep
cron) now carry a synchronous side-effect import so registration completes
at module init with no async gap.
- Golf dashboard "Not authenticated" digests: a session can pass the
top-of-page check yet expire before the data fetch re-validates it. That
narrow case now redirects to /golf/login (both coach and player branches)
instead of crashing to the route error boundary; real outages still surface.
- Sentry duplicate captures of expected baseball control-flow throws
(observed as BaseballUnauthorizedError from a logged-out tab's 60s
NotificationBell poll): withBaseballAction already classifies these as
handled/expected and re-raises; the re-raise escapes the server-action
boundary where onRequestError/console capture re-reports it as an
unhandled Error. The six expected error classes are now in
sharedIgnoreErrors — they remain fully visible in admin_events/Bridge.
- pattern-miner supersede deadlock (40P01, roster-sweep cron racing a
round-submit mine over the same team): the supersede UPDATE is idempotent,
so retry up to 2x with backoff instead of surfacing a transient as an error.
Verified: tsc clean, eslint --max-warnings 0 clean, 494 test files /
4,917 tests green, production build green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J2E46URHkhCnEQSYDtZLJX
|
This pull request has been ignored for the connected project Preview Branches by Supabase. |
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
Summary by CodeRabbit
WalkthroughThe PR adds targeted dashboard redirects for expired sessions, eager insight-trigger registration, deadlock retries for stale-pattern updates, and Sentry filtering for expected application errors. ChangesDashboard authentication handling
Insight trigger registration
Runtime error resilience
Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related PRs
Suggested labels: 🚥 Pre-merge checks | ✅ 11 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (11 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ast-grep (0.44.1)ast-grep could not parse rule config: /ast-grep-rules/../git/.coderabbit/ast-grep/no-explicit-any.yml 🔧 ESLint
src/app/golf/actions/dashboard-data.tsESLint skipped: missing config or dependency (missing-dependency). The ESLint configuration references a package that is not available in the sandbox. src/lib/coachhelm/v2/mining/pattern-miner.tsESLint skipped: the ESLint configuration for this file references a package that is not available in the sandbox. Comment |
PR Summary by QodoFix cold-start TDZ crash, expired-session redirects, Sentry noise, and miner deadlock retries
AI Description
Diagram
High-Level Assessment
Files changed (7)
|
Greptile SummaryThis PR fixes several observed production error paths across GolfHelm, CoachHelm, and BaseballHelm. The main changes are:
Confidence Score: 5/5This looks safe to merge after considering the miner retry cleanup.
src/lib/coachhelm/v2/mining/pattern-miner.ts Important Files Changed
Prompt To Fix All With AIFix the following 2 code review issues. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 2
src/lib/coachhelm/v2/mining/pattern-miner.ts:1004-1012
**Supersede Failure Leaves Stale Patterns**
When the new retry loop exhausts or sees any non-`40P01` error, `savePatterns` only logs and returns after the earlier upserts have already committed. That leaves both the newly upserted active patterns and the old patterns that this update was supposed to retire visible until a later mine happens to clean them up.
### Issue 2 of 2
src/lib/coachhelm/v2/mining/pattern-miner.ts:1013
**Deadlock Retry Depends On Code Shape**
This only retries when the returned error has `code === '40P01'`. If the Supabase/PostgREST layer surfaces the observed deadlock as a thrown exception or a wrapped error without that exact field, the loop breaks after the first attempt and the live deadlock path still logs a failed supersede without retrying.
Reviews (1): Last reviewed commit: "fix: error-overview sweep — TDZ import c..." | Re-trigger Greptile |
| const { error } = await fromUntyped(supabase, 'golf_patterns_v2') | ||
| .update({ is_active: false, updated_at: new Date().toISOString() }) | ||
| .in('player_id', playerIds) | ||
| .eq('is_active', true) | ||
| .not('id', 'in', idList) | ||
| // Keep coach-curated patterns visible; only retire auto-detected ones | ||
| // (NULL or non-preserved lifecycle_state). | ||
| .or('lifecycle_state.is.null,lifecycle_state.not.in.(confirmed,addressed,resolved,dismissed)'); | ||
| supersedeError = error; |
There was a problem hiding this comment.
Supersede Failure Leaves Stale Patterns
When the new retry loop exhausts or sees any non-40P01 error, savePatterns only logs and returns after the earlier upserts have already committed. That leaves both the newly upserted active patterns and the old patterns that this update was supposed to retire visible until a later mine happens to clean them up.
Rule Used: No DELETE-then-INSERT in any save/submit/sync path... (source)
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/coachhelm/v2/mining/pattern-miner.ts
Line: 1004-1012
Comment:
**Supersede Failure Leaves Stale Patterns**
When the new retry loop exhausts or sees any non-`40P01` error, `savePatterns` only logs and returns after the earlier upserts have already committed. That leaves both the newly upserted active patterns and the old patterns that this update was supposed to retire visible until a later mine happens to clean them up.
**Rule Used:** No DELETE-then-INSERT in any save/submit/sync path... ([source](.greptile))
How can I resolve this? If you propose a fix, please make it concise.| // (NULL or non-preserved lifecycle_state). | ||
| .or('lifecycle_state.is.null,lifecycle_state.not.in.(confirmed,addressed,resolved,dismissed)'); | ||
| supersedeError = error; | ||
| if (!error || error.code !== '40P01') break; |
There was a problem hiding this comment.
Deadlock Retry Depends On Code Shape
This only retries when the returned error has code === '40P01'. If the Supabase/PostgREST layer surfaces the observed deadlock as a thrown exception or a wrapped error without that exact field, the loop breaks after the first attempt and the live deadlock path still logs a failed supersede without retrying.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/coachhelm/v2/mining/pattern-miner.ts
Line: 1013
Comment:
**Deadlock Retry Depends On Code Shape**
This only retries when the returned error has `code === '40P01'`. If the Supabase/PostgREST layer surfaces the observed deadlock as a thrown exception or a wrapped error without that exact field, the loop breaks after the first attempt and the live deadlock path still logs a failed supersede without retrying.
How can I resolve this? If you propose a fix, please make it concise.
Code Review by Qodo
Context used✅ Compliance rules (platform):
98 rules 1.
|
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/lib/coachhelm/v2/mining/pattern-miner.ts`:
- Around line 1001-1014: Add randomized jitter to the retry delay in the
supersede update loop around `supersedeError`, while preserving the existing
attempt-based backoff and deadlock retry behavior. Compute the delay as the
current `200 * attempt` base plus a small random offset before calling
`setTimeout`, so concurrent retries are less likely to run in lock-step.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 4b69d5b2-16d3-44b0-a8c4-e7c07a8de63e
📒 Files selected for processing (7)
src/app/api/cron/coachhelm-roster-sweep/route.tssrc/app/golf/(dashboard)/dashboard/page.tsxsrc/instrumentation.tssrc/lib/coachhelm/v2/mining/pattern-miner.tssrc/lib/coachhelm/v2/post-round-trigger.tssrc/lib/coachhelm/v2/trigger-insights-bridge.tssrc/test/coachhelm/v2/post-round-trigger.test.ts
…ck backoff Two follow-ups from PR #803 review: - dashboard-data.ts: getUser() failures that supabase-js marks retryable (network / GoTrue 5xx) now surface to the route error boundary instead of being misread as 'Not authenticated' and redirected to login. A missing/expired session (AuthSessionMissingError, user null) still maps to the login redirect — applying the reviewer's literal 'throw any authError' suggestion would have broken the expiry redirect, since expiry returns an error object alongside the null user. - pattern-miner.ts: add random jitter (0-150ms) to the supersede deadlock backoff so victims retried in lock-step with the concurrent miner don't re-collide on the same window. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019WJ1Gzjj9MT3UsxhMoMzSV
The dashboard's expired-session redirect (PR #803) is user-visible behavior — note it in memory/features/auth-onboarding-join.md per the feature-doc rule, including the outage-vs-expiry split. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019WJ1Gzjj9MT3UsxhMoMzSV
Error overview (7-day window, all three channels)
Surveyed Vercel runtime errors, in-app
admin_events, and Sentry. Every distinct error root-caused; four code fixes here, the rest verified as already resolved by the deploy earlier today.Fixes in this PR
1. CoachHelm cold-start TDZ crash (
generateTeamInsight.generateAlerts failed: Cannot access 'a' before initialization— Jun 23 + Jul 10)Forensic trace found a genuine value-level import cycle:
insights.tsstatically imports__registerTriggerPlayerInsightsAfterRoundfromtrigger-insights-bridge.ts, and the bridge lazilyawait import()edinsights.tsback. On a cold Fluid Compute instance, a backgroundafter()round trigger racing a dashboard server action could observeinsights.tsmid-evaluation → TDZ. The bridge's dynamic import is deleted; both real callers (post-round-trigger.ts, roster-sweep cron) now carry a synchronous side-effectimport '@/app/golf/actions/insights'so registration completes at module-init with no async gap. Security property unchanged: the impl is still never exported from a'use server'file. (Madge missed this cycle previously because it was run without--ts-config, dropping all@/alias edges.)2. Golf dashboard
Not authenticateddigests (×2, Jul 6)Session passes the top-of-page check, expires before the data fetch re-validates. That narrow case now redirects to
/golf/login?returnTo=…in both coach and player branches; anything else still surfaces to the route error boundary (P002/P426 — real outages must never be swallowed).3. Sentry double-reports of expected baseball control-flow throws (
BaseballUnauthorizedError: You must be signed in— a logged-out tab's 60s NotificationBell poll)withBaseballActionalready classifies these as handled/expected (admin_events + skipSentry) and re-raises for caller branching — but the re-raise escapes the server-action boundary whereonRequestError/console capture re-report it as an unhandled Sentry Error. The six expected classes joinsharedIgnoreErrors. They stay fully visible in admin_events / Helm Bridge; only the duplicate Sentry Error goes away. The client poll already degrades gracefully — no UX change needed.4. pattern-miner supersede deadlock (
40P01, Jul 8 cron)Roster-sweep batches racing a round-submit mine over the same team deadlocked on the multi-row supersede UPDATE. The operation is idempotent (deterministic ids, converges on re-run), so it now retries up to 2× with backoff; only a persistent failure logs.
Verified already-resolved (no code needed)
triggerPlayerInsightsAfterRound: No completed rounds in 90 days×6 — theengine_no_recent_roundsclassifier shipped in Error sweep: APNs token pruning, honest severities, unauth redirects, baseball RLS-denial parity #798 maps these to severityinfo; the 03:45 UTC cron ran pre-deploy (prod went live 20:41 UTC). Self-resolves tonight.runOperationalSignalDetectionsave failure — one-off on Jul 4, pre-deploy, no recurrence.Gates
tsc clean · eslint --max-warnings 0 clean · 494 test files / 4,917 tests green · production build green. Test blast radius of the side-effect import covered:
post-round-trigger.test.tsstubsinsights.ts(bridge is fully mocked there); the other three affected suites already mockpost-round-triggerwholesale.🤖 Generated with Claude Code
https://claude.ai/code/session_01J2E46URHkhCnEQSYDtZLJX