Sentry Configuration and Error Grouping
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):
- New Issue Alert: Send a notification when a new issue is created. This catches novel errors.
- Critical Error Alert: When an issue with the tag
level:criticaloccurs, send an immediate notification. - 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 Feature | Free Tier Limit | Marketflow Usage |
|---|---|---|
| Error events | 5,000/month | ~100-500 |
| Performance transactions | 10,000/month | ~5,000 (at 10% sample) |
| Team members | 1 | 1 |
| Data retention | 30 days | Sufficient |