Stack — 1 of 2. Part of #103; the stack is closed out by #110.
(main) <- claude/sentry-python-sdk-setup-e45ee2 <- claude/sentry-error-pagesThis layer is the SDK wiring on its own. The user-facing error pages that surface a reference code build on it and land in #110.
Debugging production has meant guessing — few logs, and no way to connect a user's report to what actually happened.
What's covered
| Process | How |
|---|---|
| Django site | DjangoIntegration (auto-enabled), initialized at the top of settings.py |
| Discord bot | py-cord has no Sentry integration, so its two error hooks are wired by hand |
| FastAPI server the bot runs | FastApiIntegration / StarletteIntegration (auto-enabled) |
| Sibling asyncio tasks | AsyncioIntegration — without it, a task dying takes the error with it |
Existing logging calls | LoggingIntegration; no call sites change |
Requirements from #103 met here
- Never runs in local development. Reporting is skipped entirely whenever
DEBUGis on.DEBUGwins even ifSENTRY_ENABLED=1is set explicitly. - Env var to disable.
SENTRY_ENABLED=0, or a blankSENTRY_DSN. - All instances report out of the box. The shared Comet Robotics DSN is the default, so a deployment is debuggable with zero per-instance setup.
SENTRY_DSNpoints an instance elsewhere.
The remaining requirement — a request ID on custom error pages — is #110.
Telling instances apart
Every event carries two tags rather than splitting instances across projects:
tenant— defaults to thePUBLIC_URLhostname, overridable withSENTRY_TENANTservice—webordiscord-bot
The bot already initializes the SDK by importing settings, so it re-tags itself rather than initializing twice.
Streamed spans
Tracing runs in stream mode (trace_lifecycle="stream"): spans go out in batches as they finish rather than buffering until the root span closes. Lifts the 1000-span-per-transaction cap, cuts memory, surfaces trace data sooner.
Two consequences that took some digging and are worth knowing before anyone writes custom instrumentation:
- The legacy
start_span/start_transactionAPI is a deliberate no-op in stream mode — it returns aNoOpSpanwith aDeprecationWarning. We have no custom instrumentation, and the Django, FastAPI, Starlette and asyncio integrations are all stream-aware, so there's nothing to migrate. But this is a trap for future custom spans. - Scope tags don't reach spans. Spans are their own envelope items and never pass through the event scope, so the
tenant/servicetags that cover errors and logs were arriving empty on all trace data — which would defeat the point of sharing one project.before_send_spannow stamps them onto every span. Conveniently that hook is only honoured in stream mode, so enabling streaming is what makes the fix available.
Worth knowing: in stream mode breadcrumbs are no longer attached to spans. They stay on errors, which is where we read them.
Sampling
Traces default to 0.2 and profile sessions to 0.5, both env-overridable. Errors and logs are never sampled.
These started at 1.0 for both, copied from the Sentry onboarding wizard — which emits values chosen to surface data immediately in a demo, not to run in production. That tripped a real billing quota: the free Developer plan includes zero continuous profile hours, so any profiling at all exceeds it. SENTRY_PROFILE_SESSION_SAMPLE_RATE=0 disables profiling outright if the org's plan doesn't cover it.
Dependency change
sentry-sdk moves from a transitive dependency to a direct one, resolving to 2.72.0 within the declared >=2.62,<3 range (stream mode needs 2.62+).
Rebasing onto latest main re-resolved the lock from main's tree: the only material changes vs main are the sentry-sdk entry plus boto3/botocore patch bumps from re-resolution. pipenv verify reports the lock up to date.
Rebased onto latest main
Rebased onto main at the pgserver-CI commit, picking up the DRF/posters removal, mypy, the pip-group dep bumps, and the dev-docs rewrite. Conflict resolutions: discord_bot.py keeps main's import order with the Sentry block; Pipfile.lock was rebuilt from main's tree plus sentry-sdk; the Sentry README docs were dropped entirely (README is unchanged vs main); the observability test helper gained explicit types for mypy.
Verification
manage.py check clean; 16 observability tests pass at this layer on the rebased tree (81 across the full suite at the top of the stack). ruff format --check and mypy (175 files) are clean.
Sentry has now genuinely received data from this instrumentation — errors arrive grouped, with stack traces, tenant/service tags, release and trace context. Beyond the unit tests I exercised the real WSGI application and inspected the resulting envelopes: a request emits a v2 span envelope item containing the http.server root span plus children, each carrying tenant and service.
One cleanup note: two DisallowedHost events (CLUB-MANAGER-1) in the project came from those local verification runs, which were pointed at the production DSN by mistake. That issue is resolved with an explanatory comment; no ALLOWED_HOSTS change is needed, and later runs use SENTRY_ENABLED=0.
🤖 Generated with Claude Code