Skip to content

Commit 4fa41ad

Browse files
committed
feat(backend): Add Sentry error tracking integration (#54)
FEATURES: - Sentry SDK integration with FastAPI - Automatic error capture with full stack traces - User context attached to errors (user_id, email) - Performance monitoring (10% sample in production) - Smart filtering (ignores health checks, bot paths) IMPLEMENTATION: - services/sentry.py: init_sentry(), set_user_context(), capture_exception() - main.py: Initialize Sentry before other imports - middleware/auth.py: Set user context after authentication CONFIGURATION: - SENTRY_DSN: Your Sentry project DSN - ENVIRONMENT: development/staging/production Sentry is optional - app works fine without SENTRY_DSN set. Closes #54
1 parent b305ec4 commit 4fa41ad

6 files changed

Lines changed: 160 additions & 0 deletions

File tree

.env.example

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,8 @@ ALLOWED_ORIGINS=http://localhost:3000
3434
# Redis (auto-configured in Docker, set REDIS_URL in Railway)
3535
REDIS_HOST=redis
3636
REDIS_PORT=6379
37+
38+
# Sentry Error Tracking (Optional but recommended for production)
39+
# Get DSN from: https://sentry.io → Settings → Projects → Client Keys
40+
SENTRY_DSN=
41+
ENVIRONMENT=development # development, staging, production

backend/.env.example

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,8 @@ ALLOWED_ORIGINS=http://localhost:3000
1818
# Redis Cache
1919
REDIS_HOST=localhost
2020
REDIS_PORT=6379
21+
22+
# Sentry Error Tracking (Optional)
23+
# Get DSN from https://sentry.io → Settings → Projects → Client Keys
24+
SENTRY_DSN=
25+
ENVIRONMENT=development

backend/main.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010
from starlette.responses import JSONResponse
1111
import os
1212

13+
# Initialize Sentry FIRST (before other imports to catch all errors)
14+
from services.sentry import init_sentry
15+
init_sentry()
16+
1317
# Import API config (single source of truth for versioning)
1418
from config.api import API_PREFIX, API_VERSION
1519

backend/middleware/auth.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,11 +122,17 @@ def _authenticate(token: str) -> AuthContext:
122122
# Try JWT (Supabase tokens)
123123
ctx = _validate_jwt(token)
124124
if ctx:
125+
# Set Sentry user context for error tracking
126+
from services.sentry import set_user_context
127+
set_user_context(user_id=ctx.user_id, email=ctx.email)
125128
return ctx
126129

127130
# Try API key
128131
ctx = _validate_api_key(token)
129132
if ctx:
133+
# Set Sentry user context for error tracking
134+
from services.sentry import set_user_context
135+
set_user_context(user_id=ctx.user_id or ctx.api_key_name)
130136
return ctx
131137

132138
# Neither worked

backend/requirements.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,6 @@ pyjwt>=2.8.0 # JWT token verification for Supabase Auth
3636
pytest>=8.0.0
3737
pytest-asyncio>=0.24.0
3838
pytest-cov>=6.0.0
39+
40+
# Observability
41+
sentry-sdk[fastapi]>=2.0.0

backend/services/sentry.py

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
"""
2+
Sentry Error Tracking Integration
3+
Provides production error visibility and performance monitoring
4+
"""
5+
import os
6+
from typing import Optional
7+
8+
9+
def init_sentry() -> bool:
10+
"""
11+
Initialize Sentry SDK if SENTRY_DSN is configured.
12+
13+
Returns:
14+
bool: True if Sentry was initialized, False otherwise
15+
"""
16+
sentry_dsn = os.getenv("SENTRY_DSN")
17+
18+
if not sentry_dsn:
19+
print("ℹ️ Sentry DSN not configured - error tracking disabled")
20+
return False
21+
22+
try:
23+
import sentry_sdk
24+
from sentry_sdk.integrations.fastapi import FastApiIntegration
25+
from sentry_sdk.integrations.starlette import StarletteIntegration
26+
27+
environment = os.getenv("ENVIRONMENT", "development")
28+
29+
sentry_sdk.init(
30+
dsn=sentry_dsn,
31+
environment=environment,
32+
33+
# Performance monitoring - sample 10% of transactions in production
34+
traces_sample_rate=0.1 if environment == "production" else 1.0,
35+
36+
# Profile 10% of sampled transactions
37+
profiles_sample_rate=0.1,
38+
39+
# Send PII like user IDs (we need this for debugging)
40+
send_default_pii=True,
41+
42+
# Integrations
43+
integrations=[
44+
FastApiIntegration(transaction_style="endpoint"),
45+
StarletteIntegration(transaction_style="endpoint"),
46+
],
47+
48+
# Filter out health check noise
49+
before_send=_filter_events,
50+
51+
# Don't send in debug mode
52+
debug=environment == "development",
53+
)
54+
55+
print(f"✅ Sentry initialized (environment: {environment})")
56+
return True
57+
58+
except ImportError:
59+
print("⚠️ sentry-sdk not installed - error tracking disabled")
60+
return False
61+
except Exception as e:
62+
print(f"⚠️ Failed to initialize Sentry: {e}")
63+
return False
64+
65+
66+
def _filter_events(event, hint):
67+
"""
68+
Filter out noisy events before sending to Sentry.
69+
"""
70+
# Don't send health check errors
71+
if "health" in event.get("request", {}).get("url", ""):
72+
return None
73+
74+
# Don't send 404s for common bot paths
75+
if event.get("exception"):
76+
exception_value = str(event["exception"].get("values", [{}])[0].get("value", ""))
77+
bot_paths = ["/wp-admin", "/wp-login", "/.env", "/config", "/admin"]
78+
if any(path in exception_value for path in bot_paths):
79+
return None
80+
81+
return event
82+
83+
84+
def set_user_context(user_id: Optional[str] = None, email: Optional[str] = None):
85+
"""
86+
Set user context for error tracking.
87+
Call this after authentication to attach user info to errors.
88+
89+
Args:
90+
user_id: The authenticated user's ID
91+
email: The user's email (optional)
92+
"""
93+
try:
94+
import sentry_sdk
95+
sentry_sdk.set_user({
96+
"id": user_id,
97+
"email": email,
98+
})
99+
except ImportError:
100+
pass # Sentry not installed
101+
102+
103+
def capture_exception(error: Exception, **extra_context):
104+
"""
105+
Manually capture an exception with additional context.
106+
107+
Args:
108+
error: The exception to capture
109+
**extra_context: Additional context to attach
110+
"""
111+
try:
112+
import sentry_sdk
113+
with sentry_sdk.push_scope() as scope:
114+
for key, value in extra_context.items():
115+
scope.set_extra(key, value)
116+
sentry_sdk.capture_exception(error)
117+
except ImportError:
118+
pass # Sentry not installed
119+
120+
121+
def capture_message(message: str, level: str = "info", **extra_context):
122+
"""
123+
Capture a message (not an exception) for tracking.
124+
125+
Args:
126+
message: The message to capture
127+
level: Severity level (info, warning, error)
128+
**extra_context: Additional context to attach
129+
"""
130+
try:
131+
import sentry_sdk
132+
with sentry_sdk.push_scope() as scope:
133+
for key, value in extra_context.items():
134+
scope.set_extra(key, value)
135+
sentry_sdk.capture_message(message, level=level)
136+
except ImportError:
137+
pass # Sentry not installed

0 commit comments

Comments
 (0)