DEV Community

Cover image for The Sentry dashboard was empty for months — here's why (and the 2-line fix that mattered most)
Alkhassim Lawal Umar
Alkhassim Lawal Umar

Posted on

The Sentry dashboard was empty for months — here's why (and the 2-line fix that mattered most)

Summer Bug Smash: Clear the Lineup 🐛🛹

This is a submission for DEV's Summer Bug Smash: Clear the Lineup powered by Sentry.

Project Overview

This backend powers K-XpertAI, the AI coding assistant at the center of KingxTech's developer-tools ecosystem — a chat-first assistant that turns a prompt into working code in a live artifact panel, with SynthCode IDE as the editor you take it into next. The service in question is the Node/Express backend (internally kx-neurocore) that routes every chat turn across multiple model providers — Claude, Gemini, GPT, and a free open-weights tier — while also handling credits, billing, chat history, GitHub push/import, and per-project file storage. It runs on Google Cloud Run.

Bug Fix or Performance Improvement

TL;DR: Sentry had a valid DSN, was fully configured, and still saw nothing. Zero errors, zero transactions, for months. The DSN wasn't the problem — 18 routes silently never told it anything was wrong.

I went looking for why the dashboard was a ghost town, and traced it to one line of asymmetry in the code: only one route out of nineteen ever called Sentry.captureException(). Everything else — billing checkout, credit checks, chat sessions, chat history, GitHub push/import, project file reads/writes — caught its own errors and quietly did console.error(...), then returned a clean JSON error to the client. Functionally fine for the user. Invisible to Sentry.

That's the dangerous kind of bug: nothing crashes, nothing 500s in a way anyone notices, the app "works." But the one signal that would have told me a Paystack checkout was failing, or a GitHub push was silently erroring for a specific user, never left stdout. Whatever was actually breaking in production was a total blind spot.

Code

fix: Sentry error reporting gap across API routes #1

Sentry was correctly configured with a valid DSN, but the project dashboard showed zero transactions and zero errors, even in production. Digging through the codebase, I found the actual cause: only one route (/api/ai/generate) called Sentry.captureException(). Every other route — 18 of them, covering billing, credits, sessions, chat history, GitHub integration, and project file operations — only did console.error(...). Errors on those routes were logged to stdout and then simply vanished; Sentry never saw them at all.

This meant that whatever was actually breaking for real users — a failed Paystack checkout, a broken GitHub push, a corrupted session — was completely invisible in Sentry, no matter how well-configured the DSN was.

My Improvements

Rather than adding a one-off Sentry.captureException() call at each of the 18 sites individually, I centralized the fix:

13 of the 18 routes already funneled their errors through a shared handleFsError() helper — I fixed the instrumentation gap there once, which covered all 13 in a single change. The remaining 5 routes built their own status codes/messages, so I added a small reportError() helper with the same signature and reused it across those. Both helpers use the identical await Sentry.flush(2000).catch(() => {}) pattern that the one correctly-instrumented route already had — this matters specifically on Cloud Run, which freezes a container's CPU immediately after the HTTP response is sent. Sentry delivers events asynchronously in the background, so without an explicit flush before the response ends, a scaled-down instance can silently drop the event mid-delivery. I also added a one-line startup log ([sentry] initialized / [sentry] disabled — SENTRY_DSN not set) — previously there was no way to tell from Cloud Run's logs whether Sentry had actually picked up the DSN on a given deploy.

Typechecked clean against the existing tsconfig.json with no other behavior changes — every route still returns the same status codes and error messages to the client; the only difference is that unexpected errors now actually reach Sentry.

My Improvements

Instead of bolting Sentry.captureException() onto all 18 catch blocks one by one, I collapsed the fix into two places:

  • 13 of the 18 already funneled through one shared handleFsError() helper — fixing the gap there covered all 13 routes in a single change.
  • The remaining 5 built their own status codes and messages, so I extracted a small reportError() helper with the same shape and swapped them over.
  • Both helpers share the exact await Sentry.flush(2000).catch(() => {}) pattern the one correctly-instrumented route already had — and that flush isn't decorative. Cloud Run freezes a container's CPU the instant the HTTP response is sent. Sentry ships events asynchronously in the background, so a fire-and-forget captureException() is racing against the container being frozen mid-delivery. Skip the flush, and even a correctly called Sentry report can vanish.
  • I also added one line at startup — [sentry] initialized or [sentry] disabled — SENTRY_DSN not set — because there was previously no way to tell from Cloud Run's logs whether Sentry had even picked up the DSN on a given deploy. The exact class of bug I'd just spent an afternoon hunting down would have taken thirty seconds to diagnose with that log line in place.

Before — every route silently swallowed its own errors:

function handleFsError(res: express.Response, error: unknown) {
  if (error instanceof ProjectAccessError) {
    res.status(403).json({ success: false, error: error.message });
    return;
  }
  console.error('Project FS error:', error);
  res.status(500).json({ success: false, error: 'Internal error.' });
}
Enter fullscreen mode Exit fullscreen mode
} catch (error) {
  console.error('Request cap check error:', error);
  res.status(500).json({ success: false, error: 'Failed to check model usage limit' });
  return;
}
Enter fullscreen mode Exit fullscreen mode

After — the same two shared helpers, actually reporting to Sentry with the Cloud-Run-safe flush:

async function handleFsError(res: express.Response, error: unknown) {
  if (error instanceof ProjectAccessError) {
    // Expected access-control rejection, not a bug — no Sentry report.
    res.status(403).json({ success: false, error: error.message });
    return;
  }
  console.error('Project FS error:', error);
  Sentry.captureException(error, { tags: { route: res.req?.path } });
  // Cloud Run can freeze/scale an instance down right after the response is
  // sent, and Sentry delivers events asynchronously in the background —
  // without this flush, errors on a scaled-down instance can silently never
  // reach Sentry at all. 2s cap so a slow network never hangs the response.
  await Sentry.flush(2000).catch(() => {});
  res.status(500).json({ success: false, error: 'Internal error.' });
}

// Same reporting + flush pattern as handleFsError, for routes that build
// their own status code / message instead of the generic "Internal error."
async function reportError(
  res: express.Response,
  status: number,
  message: string,
  error: unknown,
  logLabel: string
) {
  console.error(logLabel, error);
  Sentry.captureException(error, { tags: { route: res.req?.path } });
  await Sentry.flush(2000).catch(() => {});
  res.status(status).json({ success: false, error: message });
}
Enter fullscreen mode Exit fullscreen mode
} catch (error) {
  await reportError(res, 500, 'Failed to check model usage limit', error, 'Request cap check error:');
  return;
}
Enter fullscreen mode Exit fullscreen mode

And the startup visibility fix, in lib/sentry.ts:

export function initSentry(): void {
  const dsn = process.env.SENTRY_DSN;
  if (!dsn) {
    console.log('[sentry] disabled — SENTRY_DSN not set');
    return;
  }

  Sentry.init({
    dsn,
    tracesSampleRate: 1.0,
    sendDefaultPii: false,
  });
  console.log('[sentry] initialized');
}
Enter fullscreen mode Exit fullscreen mode

Typechecked clean against the project's existing tsconfig.json. Zero behavior change from the client's point of view — same status codes, same error messages. The only difference: every error the API actually hits now has a chance of reaching Sentry instead of none.

Best Use of Sentry

The core insight here isn't "add more captureException calls" — it's that Sentry's async delivery model and Cloud Run's freeze-after-response model are directly in tension, and the fix has to account for both at once:

  • Flush before you finishawait Sentry.flush(2000) in a finally block, capped at 2 seconds so a slow network never hangs a real user's response, is the difference between "Sentry looks configured" and "Sentry actually receives events" on any serverless/scale-to-zero platform.
  • Instrument by construction, not by memory — routing every route's error handling through two small shared helpers means the next route added to this API gets Sentry coverage automatically, instead of depending on someone remembering to add it.
  • Make init state visible — a one-line startup log turns "is Sentry even working right now?" from a guessing game into something you read off the Cloud Run logs in seconds.

Top comments (0)