Skip to main content
ship before you scale

Sentry Configuration and Error Grouping

4 min read Chapter 35 of 42

Sentry Configuration and Error Grouping

The Feature

Sentry groups related errors into issues. A ConnectionRefusedError from the database is one issue, not 50 individual error reports. The developer receives one alert per new issue. Subsequent occurrences increment the counter without generating new alerts. Critical errors (payment failures, authentication errors, database connection loss) trigger immediate alerts.

The Decision

Sentry’s default error grouping works well for Python exceptions. Errors with the same exception type and stack trace are grouped into one issue. Custom fingerprinting is needed only when the same root cause produces different stack traces (for example, database timeouts in different endpoints).

The Implementation

Sentry Project Configuration

# backend/app/config.py
from pydantic_settings import BaseSettings


class Settings(BaseSettings):
    # ... other settings ...
    sentry_dsn: str = ""
    app_version: str = "0.1.0"
    environment: str = "production"

    model_config = {"env_file": ".env"}

Custom Error Fingerprinting

# backend/app/sentry_config.py
import sentry_sdk


def before_send(event, hint):
    """Customize error events before sending to Sentry."""
    exception = hint.get("exc_info")
    if exception is None:
        return event

    exc_type, exc_value, _ = exception

    # Group all database connection errors together
    if "connection" in str(exc_value).lower() and "refused" in str(exc_value).lower():
        event["fingerprint"] = ["database-connection-error"]

    # Group all Stripe API errors by error code
    if exc_type.__name__ == "StripeError":
        stripe_code = getattr(exc_value, "code", "unknown")
        event["fingerprint"] = ["stripe-error", stripe_code]

    # Group all Redis connection errors together
    if exc_type.__name__ in ("ConnectionError", "TimeoutError") and "redis" in str(exc_value).lower():
        event["fingerprint"] = ["redis-connection-error"]

    return event


def before_send_transaction(event, hint):
    """Filter out health check transactions to save quota."""
    if event.get("transaction") == "/health":
        return None
    return event

Initialization with Custom Hooks

# backend/app/main.py (updated)
import sentry_sdk
from app.sentry_config import before_send, before_send_transaction

if settings.sentry_dsn:
    sentry_sdk.init(
        dsn=settings.sentry_dsn,
        integrations=[
            FastApiIntegration(transaction_style="endpoint"),
            SqlalchemyIntegration(),
        ],
        traces_sample_rate=0.1,
        environment=settings.environment,
        release=settings.app_version,
        send_default_pii=False,
        before_send=before_send,
        before_send_transaction=before_send_transaction,
    )

Alert Rules

Configure in the Sentry dashboard (Settings > Alerts):

  1. New Issue Alert: Send a notification when a new issue is created. This catches novel errors.
  2. Critical Error Alert: When an issue with the tag level:critical occurs, send an immediate notification.
  3. High Volume Alert: When an issue occurs more than 100 times in 1 hour, send a notification. This catches error storms.
# Tag critical errors explicitly in the code
import sentry_sdk


async def process_stripe_webhook(event):
    try:
        # ... process webhook ...
        pass
    except Exception as e:
        sentry_sdk.set_tag("level", "critical")
        sentry_sdk.set_tag("domain", "payments")
        sentry_sdk.capture_exception(e)
        raise

Filtering Noise

# backend/app/sentry_config.py (additions)

# Errors to ignore (not worth tracking)
IGNORED_ERRORS = [
    "ConnectionResetError",  # Client disconnected mid-request
    "BrokenPipeError",       # Client disconnected mid-response
]


def before_send(event, hint):
    exception = hint.get("exc_info")
    if exception is None:
        return event

    exc_type = exception[0]

    # Drop ignored errors
    if exc_type.__name__ in IGNORED_ERRORS:
        return None

    # Drop 404s (not errors, just missing routes)
    if exc_type.__name__ == "HTTPException":
        status_code = getattr(exception[1], "status_code", 0)
        if status_code == 404:
            return None

    # ... custom fingerprinting from above ...

    return event

Release Tracking

# .github/workflows/deploy-production.yml (addition)
- name: Create Sentry release
  env:
    SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
    SENTRY_ORG: marketflow
    SENTRY_PROJECT: marketflow-api
  run: |
    VERSION=$(git rev-parse --short HEAD)
    npx @sentry/cli releases new "$VERSION"
    npx @sentry/cli releases set-commits "$VERSION" --auto
    npx @sentry/cli releases finalize "$VERSION"

Release tracking links errors to specific deployments. When a new error appears, Sentry shows which release introduced it, what commits were included, and which developer authored the change that caused the error.

The Trap

# TRAP: Logging expected errors to Sentry
@router.post("/login")
async def login(email: str, password: str):
    user = await get_user_by_email(email)
    if not user or not verify_password(password, user.hashed_password):
        sentry_sdk.capture_message("Login failed")  # Floods Sentry
        raise HTTPException(status_code=401, detail="Invalid credentials")
    # 100 failed login attempts per day = 100 Sentry events wasted

# SAFE: Log expected errors locally, reserve Sentry for unexpected errors
@router.post("/login")
async def login(email: str, password: str):
    user = await get_user_by_email(email)
    if not user or not verify_password(password, user.hashed_password):
        logger.info(f"Failed login attempt for {email}")  # Local log only
        raise HTTPException(status_code=401, detail="Invalid credentials")

Sentry is for unexpected errors. Failed logins, 404s, validation errors, and rate limit rejections are expected behavior. Sending them to Sentry wastes the 5,000 monthly error quota and buries real issues in noise.

The Cost

Sentry FeatureFree Tier LimitMarketflow Usage
Error events5,000/month~100-500
Performance transactions10,000/month~5,000 (at 10% sample)
Team members11
Data retention30 daysSufficient