Let the app admit when something goes wrong - #45
Merged
Merged
Conversation
Three findings from PRODUCTION-TODO, all the same shape: the app failed quietly and the student was left to guess. Thirteen catches swallowed errors, but they are not one problem. Seven are markIntroSeen — if that fails a tour replays, and telling a student their tour-seen flag did not save is noise. One is a hover prefetch whose real fetch happens on click. One is moduleProgress's write chain, which deliberately keeps the chain alive and hands the error to its caller. Those nine stay silent. Four were real. useModuleChecklist swallowed both its load and its write: a failed load rendered the checklist empty, which reads as "none of my work saved" and invites redoing it, and a failed tick rolled back with no reason given, so the box simply refused to stay ticked. Financial Aid swallowed three loads — tracked scholarships, the college list, NPC runs — each of which leaves a different part of the module quietly wrong. They share one message, since three toasts for one dropped connection is noise. The profile editor reopened on a failed save and said nothing, which reads as the app rejecting what was typed rather than failing to store it. Error tracking goes through PostHog, and deliberately sends as little as it can: person_profiles 'never', no identify, no autocapture, no pageviews, no session recording. This app holds a minor's zip code, school, GPA, household income, race, religion and immigration status, and the cheapest way to keep that out of a third-party tool is to never send anything joinable to a person. An error says a crash happened and where, not who. Without VITE_POSTHOG_KEY nothing leaves the browser and errors go to the console exactly as before. It also listens for window errors and unhandled rejections, which nothing listened for at all — those were invisible even in the console's eyes, since no boundary saw them. One thing surfaced while testing: putting `toast` in the load effects' dependencies ties a data fetch to a context object's identity. It is stable only because ToastProvider memoises its value, which is not a promise these hooks should rely on — if that memo ever went, every module would refetch on every render and clobber its own optimistic writes. The toast is held in a ref and the effects depend on what they actually care about.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Deploying timeline-prototype with
|
| Latest commit: |
f885e11
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://37ba2c50.timeline-prototype.pages.dev |
| Branch Preview URL: | https://fix-surface-failures.timeline-prototype.pages.dev |
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Items 9, 11 and 13 from
PRODUCTION-TODO.md, which are all the same shape: the app failed quietly and the student was left to guess what happened.#9 — nine of the thirteen swallows should stay
The audit counted thirteen
catch(() => {})sites. They are not one problem, and wrapping them all in toasts would make the app noisier without making it clearer:markIntroSeen. If that write fails a tour replays. Telling a student their tour-seen flag did not save is noise about nothing they can act on.CollegeDiscoverTab— the real fetch happens on click.moduleProgress's write chain, which deliberately keeps the chain alive after a rejection and hands the error to its caller.Those stay silent. Four were real:
useModuleChecklistswallowed both of its failure paths. A failed load rendered the checklist empty, which reads as "none of my work saved" and invites redoing it. A failed write rolled the tick back with no reason given, so the box simply refused to stay ticked.Financial Aid swallowed three loads — tracked scholarships, the college list, NPC runs. Each failing alone leaves a different part of the module quietly wrong: a tracked scholarship showing "+ Track", a saved college missing from the aid comparison, a cost estimate gone. They share one message, because three toasts for one dropped connection is noise again.
Not changed:
useModuleDataanduseModuleValuealready expose aloadFailedflag, andsaveDataalready returnsfalseon failure. Those are contracts, not swallows.#11 — the profile editor
It reopened on a failed save and said nothing, which reads as the app rejecting what was typed rather than failing to store it. It now says which.
#13 — error tracking, through PostHog
Deliberately minimal about what leaves the browser:
This app holds a minor's zip code, school, GPA, household income, race, religion and immigration status. The cheapest way to guarantee none of that reaches a third-party error tool is to never send anything that could be joined back to a person. An error needs to say a crash happened and where — not who.
It also registers
window.onerrorandunhandledrejectionhandlers. Nothing was listening for either, so those failures were invisible even to the console-only module boundary, which only sees React render errors.Without
VITE_POSTHOG_KEYthe module is inert:initdoes nothing andreportErrorfalls back to the console, so development and any unconfigured deploy behave exactly as today.A bug this turned up
Putting
toastin the load effects' dependency arrays ties a data fetch to a context object's identity. It is stable today only becauseToastProvidermemoises its value — which is not a promise these hooks should be relying on. Had that memo ever gone, every module would refetch on every render and clobber its own optimistic writes.The toast is held in a ref and the effects depend on what they actually care about: whether the module is open, and which module it is.
A failing test is what exposed it, and the same failure showed that my first rollback test had been passing vacuously —
availablewas both the starting value and the rolled-back one. The success path is asserted separately now, so the rollback assertion means something.Testing
377 tests, 27 files. New: the checklist's load and write failure paths including the quiet cases, and the error tracker — that it stays inert without a key, that it never builds a person profile, that it catches unhandled rejections, that a non-Error rejection reason is wrapped rather than dropped, and that the tracker failing does not become a second error on the way out.
tsc -b,eslint .,vitest runandnpm run buildclean locally.Needs from you
A PostHog project key as
VITE_POSTHOG_KEYin the host's environment, andVITE_POSTHOG_HOSTif you are not on US cloud..env.exampledocuments both. Until the key is set this ships dark and nothing is reported.Adds one dependency:
posthog-js.