SYS://FIELD-REPORT-08

The Sentry That Never Fired

The app had Sentry. It was in package.json, it was in the config files, the wizard had clearly run. On any checklist that stops at “is error tracking installed?” this app passes. It was also, in the six weeks it had been live, completely silent — not one event, not one alert, not one issue in the dashboard. The owner believed production was watched. Production was not watched. That is a worse place to stand than knowing you have nothing.

This is a representative audit — the exact pattern I hit over and over on apps built with Claude Code, Cursor, Bolt, and v0. Names and paths are changed, but every finding is real-shaped and drawn straight from the kind of report the launchworthy skill produces. Call the app shipmate: a Next.js app on Vercel, built with Claude Code, wired to Sentry, about to onboard its first paying team.

It got a scorecard. Operations & Recovery did not pass.

1/5
domains passing
0
critical findings
4
high findings

Note the zero. There is no CRITICAL here — no leaked secret, no open table, nothing that ends the business in an afternoon. The worst finding on shipmate is a [HIGH], and I am going to call it a [HIGH], because inflating it to CRITICAL for drama would be exactly the kind of pattern-matching this whole exercise exists to replace. A [HIGH] that hides behind a green checkmark is interesting because it looks handled. That is the entire story.

The finding that ends the app: the Sentry that never fired

The dangerous finding is never the missing thing. Missing error tracking is honest — you know you’re flying blind, and you either fix it or you accept it. The dangerous finding is the one that looks handled and isn’t. shipmate’s Sentry was installed and inert, and here is the exact line that did it.

sentry.client.config.ts:6:

Sentry.init({
  dsn: process.env.SENTRY_DSN,
  tracesSampleRate: 1.0,
  enabled: process.env.NODE_ENV === "production",
});

Two problems stacked on top of each other, and both of them are invisible until you go looking. First, enabled only turns Sentry on in production — which is defensible in principle, except it means you can never see it work in development. You test locally, init never runs, nothing breaks, and you conclude it’s fine. Second, and fatally: SENTRY_DSN was nowhere. Not in .env.example, not in the Vercel project’s environment variables, not referenced in any deploy config. In production, process.env.SENTRY_DSN resolved to undefined, and Sentry silently no-ops on a falsy DSN. It does not throw. It does not warn. It just quietly captures nothing, forever.

So the SDK loaded, init ran in production exactly as intended, and every error since launch went into the void — while the config file, sitting there in the repo, read as proof the problem was solved.

CRITICAL · CAUGHT

Error tracking installed but inert — [HIGH]

Sentry is present and configured, and has never sent a single event. The DSN reads from SENTRY_DSN, which appears in no .env.example and no deploy config — so in production it is unset and every error is dropped. An enabled: NODE_ENV === "production" gate means you never see it fail in dev either. Six weeks live, zero issues in the dashboard — not because nothing broke, but because nothing was ever listening. A false green is worse than an empty dashboard: you trust it.

The fix is not “install Sentry.” It’s already installed. The fix is to make the DSN real in the environment that actually runs, and then to prove — with a live event — that it fires.

THE FIX · 20 MIN

Set SENTRY_DSN (and the public DSN, if your client config reads a NEXT_PUBLIC_ one) in the Vercel production environment, not just locally. Add it to .env.example with a placeholder so the requirement is documented and the next deploy doesn’t silently drop it. Keep the enabled gate only if you’ve confirmed prod carries the DSN — otherwise drop the gate so dev catches misconfiguration early. Then set an alert rule in Sentry (new issue → email or Slack) so a captured error actually reaches a human.

THE FIX · IN ORDER

Make it fire, then prove it fired

  1. Set the DSN in the deployed environment. Vercel → Project → Settings → Environment Variables → Production. Add it to .env.example too, so the requirement is visible in the repo.
  2. Confirm both sides init. sentry.client.config.ts for the white screens users see, sentry.server.config.ts for the API and database errors they don’t. One without the other is half-blind.
  3. Add an alert rule. Tracking with no alert is a diary nobody reads. Notify on a new issue, delivered somewhere a person looks.
  4. Throw a real error on the deployed app — a /debug-sentry route that calls throw new Error("sentry smoke test") — and watch the issue land in the dashboard within a minute. That landed event, not the presence of the SDK, is the proof.

Localhost does not count A test error that fires on your machine proves nothing about production — that’s exactly where the enabled gate and the unset DSN hide. Trigger the error against the live deployed URL and confirm the issue appears in Sentry within a minute. The evidence the audit wants is the issue title that showed up, or an honest “nothing arrived.” If nothing arrives, it’s still inert.

The false alarm: “we have Sentry, so error tracking is done”

Here is the credibility move, and the reason to trust an audit that gets this right instead of one that just greps package.json.

Point a generic “is my app production-ready?” pass at shipmate and it sees @sentry/nextjs in the dependencies, sees three config files, and checks the box. Error tracking: handled. Move on.

FALSE POSITIVE

✓ Error tracking configured (Sentry detected)

Installed is not wired. The presence of the SDK tells you someone ran the wizard; it tells you nothing about whether a single event has ever left the browser. A reviewer that marks this green is pattern-matching on the dependency list instead of asking the only question that matters — has an error ever actually landed? And this false green is more expensive than a missing one, because it converts “we should set up monitoring” into “monitoring is done,” and nobody revisits a solved problem.

The distinction is the whole game:

  • Sentry in package.json → tells you the wizard ran. Nothing more.
  • A live test error in the dashboard within a minute → the only proof error tracking exists.

Everything between those two — the config files, the DSN variable, the reassuring green checkmark — is just scaffolding that can be present and dead at the same time. This is the same discipline as never flagging a Supabase anon key as a leak: the surface signal (“there’s a key in the bundle,” “there’s an SDK installed”) is not the thing that matters. What matters is the layer behind it — whether RLS actually restricts that key, whether an event actually lands. Judge the behavior, not the artifact.

The rest of the punch list

The inert Sentry is the headline, but Operations & Recovery had more holes, and they compound. Ranked, so the scope stays honest:

FIX THIS WEEK

After the DSN is live and proven

  1. [MEDIUM] Tracking on the client only. sentry.server.config.ts existed but was never imported by the instrumentation hook, so API-route and server-action errors — the ones users never see but that corrupt data — went uncaptured even once the DSN was set. Wire both sides.
  2. [MEDIUM] No uptime monitoring, and no alerting that reaches a human. A dead server reports nothing to Sentry, because a dead server can’t run code. Add a free uptime check (UptimeRobot, Better Stack) on the health URL, and confirm — with a test alert — that it actually pages someone. Tracking with no alerting is a dashboard nobody opens.
  3. [HIGH] No global error handler. app/global-error.tsx was missing, so an error thrown in the root layout white-screens the whole app with nothing captured. Add it alongside app/error.tsx.

Three findings, one theme: shipmate had the appearance of an observed system and none of the substance. Every piece was a step short of doing its job — the DSN a step short of being set, the server config a step short of being imported, the alerting a step short of reaching a person.

ASK YOURSELF

Is your green checkmark earned, or is it just present? “The AI tool that built it wouldn’t ship something broken” is the exact instinct that leaves a Sentry install dead in production. AI builders optimize for a working demo — running the wizard is the demo of monitoring. Wiring the DSN into prod and proving an event lands is the part that doesn’t show up on your screen, so it’s the part that gets skipped. Confidence you haven’t tested isn’t confidence. It’s a guess wearing a checkmark.

Why this keeps happening

None of this means Claude Code did anything wrong, or that shipmate’s owner was careless. The wizard ran correctly. The config was textbook. The tools are genuinely excellent at producing the shape of a hardened app — the files, the dependencies, the reassuring green. What they cannot do is the one thing that actually proves it: throw a real error at the live deployment and watch it land. That step lives outside the code, in the deployed environment, on a URL that isn’t your laptop — which is precisely where it’s invisible while you build.

That gap is the whole reason launchworthy exists. It plays bouncer at the door of production. It knows that an installed SDK is a claim, not a proof; it refuses to mark error tracking handled just because the dependency is there; and it hands you the live-test procedure, not just the finding. Installed but silent counts as no tracking — and the only thing that changes that verdict is an event you can see in the dashboard, right now, from the app your users actually hit.