Events not showing up after the DSN swap: a debug playbook
Your SDK reports success and the dashboard stays empty. An event can die in one of three places between your code and storage. This playbook checks them in cheapest-first order, so you stop reading server logs when a one-line SDK flag would have told you the SDK never tried to send.
20 seconds. You pointed SENTRY_DSN at your self-hosted backend, your app threw a test error, and nothing arrived. The event died in one of three places: the SDK discarded it before sending, the ingest endpoint rejected it with a 4xx, or it landed under a project, environment, or time window you are not looking at. Turn on SDK debug logging first. It usually names the cause in one line.
60 seconds. The highest-leverage move is debug: true in your SDK init. The SDK then prints whether it built a transport, what host it parsed from the DSN, and the HTTP status it got back. From there the status code picks the branch: 401 means the public key is wrong or missing, 403 means the key or project is disabled, 413 means the payload is too large for your proxy, 429 means a rate limit or quota, and a plain 400-class invalid means the envelope did not parse. If the SDK never logs a send at all, the event was dropped client-side by a beforeSend that returned null, a zero sample rate, an unset environment variable, or an ad-blocker stripping the request in the browser. If the SDK logs a 200 and you still cannot find the event, you are looking in the wrong place.
This guide covers turning on debug logging, reading the SDK's own discard counters, decoding every ingest status code you will see, the .NET DSN-rejection case that makes people give up mid-evaluation, and a raw curl probe that takes the SDK out of the equation.
What you are actually debugging
The switch proof produces one event. When it does not, that event is sitting dead somewhere on a short path, and the whole job is finding which hop.
The path has five hops. Your code calls captureException. The SDK serializes the error into an envelope. The transport POSTs that envelope to the ingest endpoint it derived from the DSN. The backend validates the envelope and writes it to storage. The UI queries storage and renders the issue. A missing event is alive at the start of one of those hops and dead by the end of it.
Work the path cheapest-first. Client-side checks cost you a console log. Network checks cost one curl. Backend checks cost a look at the ingest log. Most teams burn an afternoon reading server logs for an event that the SDK never sent, because they started at hop four instead of hop one.
Turn on SDK debug logging before anything else
One flag answers the first and most useful question: did the SDK try to send at all, and what did it get back?
Set the debug option in your SDK init. The SDK then logs, on startup and on every capture, whether it built a transport, the host it parsed out of the DSN, the envelope it assembled, and the HTTP status of each send. The flag is safe to leave on briefly in any environment; it writes to your console or log file and sends nothing extra to the backend.
// JavaScript / Node
Sentry.init({ dsn: process.env.SENTRY_DSN, debug: true });
# Python
sentry_sdk.init(dsn=os.environ["SENTRY_DSN"], debug=True)
// Go
sentry.Init(sentry.ClientOptions{ Dsn: os.Getenv("SENTRY_DSN"), Debug: true })
// .NET
options.Dsn = config["SENTRY_DSN"];
options.Debug = true;
Read the output before you touch anything else. If the SDK logs [Sentry] Sending envelope to https://... followed by a status, you are debugging the network or the backend. If it logs nothing about sending, or logs a reason for skipping, the event never left the process and you can stop thinking about the server entirely.
The event never left the process
When debug mode shows no send attempt, the SDK threw the event away locally. There are four common reasons, and the SDK will usually name the one that bit you.
- The DSN is unset. A missing or empty DSN makes most SDKs initialize into a no-op mode where every capture silently succeeds and goes nowhere. In framework setups the variable needs the framework's prefix to reach the client bundle:
NEXT_PUBLIC_for Next.js,VITE_for Vite,REACT_APP_for Create React App,NUXT_for Nuxt. A DSN set in your shell but not exported to the build is the single most common cause of an empty dashboard. - beforeSend returned null. If you added a
beforeSendhook to scrub PII, an earlyreturn nullon a code path drops every event that hits it. Log inside the hook to confirm it is returning the event. - The sample rate is zero. A
sampleRateof0, or atracesSampleRateconfused for the errorsampleRate, discards events before transport. These are separate dials; setting the trace one to zero does not stop errors, and setting the error one to zero stops everything. - An ad-blocker stripped the request. In the browser, blockers match the
sentry_keyquery parameter and kill the request before it leaves the tab. The fix is the SDKtunneloption, which routes events through a same-origin path on your own server so there is no third-party host for the blocker to match.
You do not have to guess between these. The SDK keeps client reports, a small telemetry payload counting what it discarded locally and why. The reasons read directly: sample_rate, before_send, event_processor, ratelimit_backoff. If a number is climbing next to one of those, that is your answer.
The event left but ingest rejected it
If debug mode shows a send and a non-2xx status, the backend saw the event and refused it. The status code tells you which refusal.
- 401 Unauthorized. The public key is wrong, missing, or does not belong to the project in the DSN path. Re-copy the DSN from the backend's project settings rather than hand-editing it.
- 403 Forbidden. The key or the project is disabled, or you are blocked by a plan-level limit. On a self-hosted backend this usually means the project was deleted or the key revoked.
- 413 Payload Too Large. The envelope exceeded a body limit, almost always at the reverse proxy rather than the backend. nginx defaults to a 1 MB
client_max_body_size; minidumps and large stack traces blow past it. Raise it to 20 MB. - 429 Too Many Requests. A rate limit or quota. The response carries a
Retry-Afterheader and anX-Sentry-Rate-Limitsheader listing which data categories are limited and for how long. The SDK backs off and discards affected payloads until the window clears, so this shows up as a gap, not a crash. - 400-class invalid. The envelope did not parse or failed validation. Sentry-compatible backends sort these under an "invalid" outcome with concrete reasons: invalid data that does not meet the schema, an empty event missing its primary payload, a duplicate event ID, or a disallowed origin.
One trap hides inside the 200 case. The X-Sentry-Rate-Limits header can ride along on a 200 OK to tell the SDK, ahead of time, that a payload type is rate-limited. The SDK treats that 200 like a 429 and drops the next batch. So a green status in your network tab does not prove storage. Read the headers, not just the code.
This is exactly the wall one .NET evaluator hit publicly: the Sentry .NET SDK rejected the DSN, and then the requests that did go out came back invalid. Those are two separate failures stacked on top of each other, and treating them as one is why the evaluation got abandoned. A DSN the SDK refuses to parse, from a stray space, an http scheme where the backend wants https, or a missing project ID, never produces a transport, so there is nothing to debug on the wire yet. Fix the parse first. Only once the SDK builds a transport and sends does the "invalid envelope" branch apply, and that one points at a content-type or envelope-framing mismatch between SDK version and backend. Resolve them in order and the wall comes down.
The event was accepted but you cannot find it
A clean 200, an X-Sentry-Rate-Limits header you have ruled out, and still no issue on screen means the event is stored and you are querying the wrong slice of it.
- Wrong project. The numeric project ID in the DSN maps to a different project than the dashboard you have open. Check that the ID at the end of the DSN matches the project in the URL bar.
- An environment filter. The issue stream is filtered to
productionwhile your test event is taggeddevelopment, or your laptop never set an environment at all. Clear the environment filter and widen it to all. - The time range. A default "last 24 hours" view plus clock skew on the sending host can park a fresh event just outside the window. Widen the range to 7 days.
- Grouping put it where you stopped looking. If the new event grouped into an existing issue you already marked resolved or ignored, it will not appear in the default unresolved view. Search by the event message across all states.
A probe that takes the SDK out of the loop
When you cannot tell whether the problem is your SDK config or the backend, cut the SDK out and POST a hand-built envelope with curl. If curl lands and your app does not, the bug is in your SDK config. If curl also fails, the bug is on the wire or in the backend, and you have a reproducible case to file.
curl -i https://errors.example.com/api/1/envelope/ \
-H 'Content-Type: application/x-sentry-envelope' \
-H 'X-Sentry-Auth: Sentry sentry_version=7, sentry_key=YOUR_PUBLIC_KEY, sentry_client=curl/1.0' \
--data-binary $'{"event_id":"00000000000000000000000000000000"}\n{"type":"event"}\n{"message":"probe from curl","level":"error"}\n'
Replace the host, the project ID in the path, and the public key. A clean response with an event ID in the body, and a matching issue in the dashboard a few seconds later, proves the whole backend path end to end. A 4xx here gives you the same status decoder from the section above, with none of the SDK between you and the answer.
Where urgentry changes the picture
urgentry speaks the same envelope contract and returns the same status codes, so every branch above applies to it unchanged. The DSN swap is still one environment variable, and the curl probe works against it byte for byte.
Two things make the backend branch shorter. urgentry writes one ingest log line per rejected envelope with the reason attached, so the server-side check is a tail -f rather than guesswork about why Relay dropped something. And it does not answer a parse failure with a cheerful 200; a malformed envelope gets a 4xx with a reason, which keeps you from chasing a "successful" send that quietly went nowhere. The point of running the proof before you cutover is to hit these walls on a test project, not on the day you cancel the SaaS contract.
Frequently asked questions
Why does the Sentry SDK report the event sent but it never shows up?
A 200 from the SDK's transport does not always mean stored. The ingest endpoint can return an X-Sentry-Rate-Limits header on a 200 to tell the SDK a payload type is rate-limited, and the SDK will quietly drop it. The SDK also keeps its own discard counters for events killed by beforeSend, sampling, or backoff before they ever hit the network. Turn on debug mode to see which of these happened.
How do I turn on Sentry SDK debug logging?
Pass the debug option to init: debug: true in JavaScript and .NET, debug=True in Python, Debug: true in Go. The SDK then prints whether it built a transport, what host it parsed from the DSN, and the HTTP status each send received. This single flag resolves most missing-event reports in one line of output.
What does a 429 from the ingest endpoint mean?
The event was rejected by a rate limit or quota. Sentry-compatible backends return 429 with a Retry-After header and an X-Sentry-Rate-Limits header that lists which data categories are limited and for how long. Well-behaved SDKs honor it by backing off and discarding affected payloads until the window expires, so a burst of 429s shows up as a gap in your dashboard rather than an error in your app.
My events worked on Sentry SaaS but stopped after I pointed the DSN at a self-hosted backend. What changed?
Only the host in the DSN changed, so the failure is almost always at that new host: DNS that does not resolve, TLS the SDK rejects, a reverse proxy with a body limit below your payload size, or a project ID in the DSN that does not exist on the new backend. Run debug mode and read the status code. A connection error points at DNS or TLS, a 404 at a missing project, a 413 at a proxy body limit.
How do I tell whether the SDK dropped the event before it was ever sent?
Read the SDK's client reports, the small telemetry payload it sends listing what it discarded locally and why. Reasons like sample_rate, before_send, event_processor, and ratelimit_backoff mean the event never left the process. If you see those, stop debugging the backend and fix the client config.
Sources
- Sentry JavaScript troubleshooting — the official checklist for events not arriving: debug mode, env-var prefixes, ad-blockers, and the tunnel option.
- Sentry rate limiting (developer docs) — the 429 contract, the
Retry-AfterandX-Sentry-Rate-Limitsheaders, and why a 200 can still carry a rate-limit header. - Sentry client reports — the SDK-side discard telemetry and the reason codes (
sample_rate,before_send,ratelimit_backoff) used to detect client-side drops. - Sentry stats and outcomes — the Accepted, Filtered, Rate Limited, Invalid, and Client Discard outcome categories and the reasons behind each.
- Sentry forum: events not displayed in the dashboard — a representative self-hosted thread showing the same accepted-but-not-visible failure this guide walks through.
One DSN swap. Full Sentry SDK compatibility.
urgentry accepts the Sentry SDK envelope format on a $5 VPS and logs the reason for every rejected event, so a failed cutover is a one-line answer instead of an afternoon. 218 API operations covered. SQLite by default, Postgres optional.