#106

feat: report errors and logs to Sentry

Openjasonappahwants to merge intofrom

Stack — 1 of 2. Part of #103; the stack is closed out by #110.

(main) <- claude/sentry-python-sdk-setup-e45ee2 <- claude/sentry-error-pages

This 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

ProcessHow
Django siteDjangoIntegration (auto-enabled), initialized at the top of settings.py
Discord botpy-cord has no Sentry integration, so its two error hooks are wired by hand
FastAPI server the bot runsFastApiIntegration / StarletteIntegration (auto-enabled)
Sibling asyncio tasksAsyncioIntegration — without it, a task dying takes the error with it
Existing logging callsLoggingIntegration; no call sites change

Requirements from #103 met here

  • Never runs in local development. Reporting is skipped entirely whenever DEBUG is on. DEBUG wins even if SENTRY_ENABLED=1 is set explicitly.
  • Env var to disable. SENTRY_ENABLED=0, or a blank SENTRY_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_DSN points 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 the PUBLIC_URL hostname, overridable with SENTRY_TENANT
  • service — web or discord-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:

  1. The legacy start_span / start_transaction API is a deliberate no-op in stream mode — it returns a NoOpSpan with a DeprecationWarning. 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.
  2. Scope tags don't reach spans. Spans are their own envelope items and never pass through the event scope, so the tenant/service tags that cover errors and logs were arriving empty on all trace data — which would defeat the point of sharing one project. before_send_span now 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