Skip to content

Commit 0052445

Browse files
committed
feat(sentry): Enhance error tracking with operation context and 500 handler
ENHANCEMENTS: - Generic 500 exception handler captures all unhandled errors - Operation context tagging (indexing, search) - Search errors now captured with query context - track_background_task decorator for async jobs - sentry_operation context manager for scoped tracking NEW HELPERS: - set_operation_context(): Tag current operation - capture_http_exception(): For custom exception handlers - track_background_task(): Decorator for async functions All 49 tests passing.
1 parent 4fa41ad commit 0052445

3 files changed

Lines changed: 212 additions & 24 deletions

File tree

backend/main.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,3 +112,18 @@ async def rate_limit_handler(request: Request, exc):
112112
status_code=429,
113113
content={"detail": "Rate limit exceeded. Please try again later."}
114114
)
115+
116+
117+
@app.exception_handler(Exception)
118+
async def generic_exception_handler(request: Request, exc: Exception):
119+
"""
120+
Catch-all handler for unhandled exceptions.
121+
Captures to Sentry and returns 500.
122+
"""
123+
from services.sentry import capture_http_exception
124+
capture_http_exception(request, exc, 500)
125+
126+
return JSONResponse(
127+
status_code=500,
128+
content={"detail": "Internal server error"}
129+
)

backend/services/indexer_optimized.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,9 @@ def _extract_functions(self, tree_node, source_code: bytes) -> List[Dict]:
194194

195195
async def index_repository(self, repo_id: str, repo_path: str):
196196
"""Index all code in a repository - OPTIMIZED VERSION"""
197+
from services.sentry import set_operation_context, capture_exception
198+
199+
set_operation_context("indexing", repo_id=repo_id)
197200
start_time = time.time()
198201
print(f"\n🚀 Starting optimized indexing for repo: {repo_id}")
199202
print(f"📂 Path: {repo_path}")
@@ -386,6 +389,8 @@ async def semantic_search(
386389
return formatted_results[:max_results]
387390

388391
except Exception as e:
392+
from services.sentry import capture_exception
393+
capture_exception(e, operation="search", repo_id=repo_id, query=query[:100])
389394
print(f"❌ Error searching: {e}")
390395
return []
391396

backend/services/sentry.py

Lines changed: 192 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,13 @@
33
Provides production error visibility and performance monitoring
44
"""
55
import os
6-
from typing import Optional
6+
import functools
7+
from typing import Optional, Callable, Any
8+
from contextlib import contextmanager
9+
10+
11+
# Global flag to track if Sentry is initialized
12+
_sentry_initialized = False
713

814

915
def init_sentry() -> bool:
@@ -13,6 +19,7 @@ def init_sentry() -> bool:
1319
Returns:
1420
bool: True if Sentry was initialized, False otherwise
1521
"""
22+
global _sentry_initialized
1623
sentry_dsn = os.getenv("SENTRY_DSN")
1724

1825
if not sentry_dsn:
@@ -30,13 +37,13 @@ def init_sentry() -> bool:
3037
dsn=sentry_dsn,
3138
environment=environment,
3239

33-
# Performance monitoring - sample 10% of transactions in production
40+
# Performance monitoring - sample 10% in production, 100% in dev
3441
traces_sample_rate=0.1 if environment == "production" else 1.0,
3542

3643
# Profile 10% of sampled transactions
3744
profiles_sample_rate=0.1,
3845

39-
# Send PII like user IDs (we need this for debugging)
46+
# Send PII like user IDs (needed for debugging)
4047
send_default_pii=True,
4148

4249
# Integrations
@@ -48,10 +55,11 @@ def init_sentry() -> bool:
4855
# Filter out health check noise
4956
before_send=_filter_events,
5057

51-
# Don't send in debug mode
58+
# Debug logging in development
5259
debug=environment == "development",
5360
)
5461

62+
_sentry_initialized = True
5563
print(f"✅ Sentry initialized (environment: {environment})")
5664
return True
5765

@@ -64,58 +72,115 @@ def init_sentry() -> bool:
6472

6573

6674
def _filter_events(event, hint):
67-
"""
68-
Filter out noisy events before sending to Sentry.
69-
"""
75+
"""Filter out noisy events before sending to Sentry."""
7076
# Don't send health check errors
71-
if "health" in event.get("request", {}).get("url", ""):
77+
request_url = event.get("request", {}).get("url", "")
78+
if "/health" in request_url:
7279
return None
7380

7481
# Don't send 404s for common bot paths
7582
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
83+
values = event["exception"].get("values", [{}])
84+
if values:
85+
exception_value = str(values[0].get("value", ""))
86+
bot_paths = ["/wp-admin", "/wp-login", "/.env", "/config", "/admin", "/phpmyadmin"]
87+
if any(path in exception_value for path in bot_paths):
88+
return None
8089

8190
return event
8291

8392

93+
# ---------------------------------------------------------------------------
94+
# User Context
95+
# ---------------------------------------------------------------------------
96+
8497
def set_user_context(user_id: Optional[str] = None, email: Optional[str] = None):
8598
"""
8699
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)
100+
Call after authentication to attach user info to errors.
92101
"""
102+
if not _sentry_initialized:
103+
return
104+
93105
try:
94106
import sentry_sdk
95107
sentry_sdk.set_user({
96108
"id": user_id,
97109
"email": email,
98110
})
111+
except Exception:
112+
pass
113+
114+
115+
# ---------------------------------------------------------------------------
116+
# Operation Context (for tagging operations like indexing, search)
117+
# ---------------------------------------------------------------------------
118+
119+
@contextmanager
120+
def sentry_operation(operation: str, **tags):
121+
"""
122+
Context manager to tag operations with context.
123+
124+
Usage:
125+
with sentry_operation("indexing", repo_id="abc", repo_name="zustand"):
126+
# do indexing work
127+
# any errors here will have repo_id and repo_name tags
128+
"""
129+
if not _sentry_initialized:
130+
yield
131+
return
132+
133+
try:
134+
import sentry_sdk
135+
with sentry_sdk.push_scope() as scope:
136+
scope.set_tag("operation", operation)
137+
for key, value in tags.items():
138+
scope.set_tag(key, str(value))
139+
yield
99140
except ImportError:
100-
pass # Sentry not installed
141+
yield
101142

102143

144+
def set_operation_context(operation: str, **tags):
145+
"""
146+
Set operation context without context manager.
147+
Useful when you can't use 'with' statement.
148+
"""
149+
if not _sentry_initialized:
150+
return
151+
152+
try:
153+
import sentry_sdk
154+
sentry_sdk.set_tag("operation", operation)
155+
for key, value in tags.items():
156+
sentry_sdk.set_tag(key, str(value))
157+
except Exception:
158+
pass
159+
160+
161+
# ---------------------------------------------------------------------------
162+
# Exception Capture
163+
# ---------------------------------------------------------------------------
164+
103165
def capture_exception(error: Exception, **extra_context):
104166
"""
105167
Manually capture an exception with additional context.
106168
107169
Args:
108170
error: The exception to capture
109-
**extra_context: Additional context to attach
171+
**extra_context: Additional context (repo_id, operation, etc.)
110172
"""
173+
if not _sentry_initialized:
174+
return
175+
111176
try:
112177
import sentry_sdk
113178
with sentry_sdk.push_scope() as scope:
114179
for key, value in extra_context.items():
115180
scope.set_extra(key, value)
116181
sentry_sdk.capture_exception(error)
117-
except ImportError:
118-
pass # Sentry not installed
182+
except Exception:
183+
pass
119184

120185

121186
def capture_message(message: str, level: str = "info", **extra_context):
@@ -125,13 +190,116 @@ def capture_message(message: str, level: str = "info", **extra_context):
125190
Args:
126191
message: The message to capture
127192
level: Severity level (info, warning, error)
128-
**extra_context: Additional context to attach
193+
**extra_context: Additional context
129194
"""
195+
if not _sentry_initialized:
196+
return
197+
130198
try:
131199
import sentry_sdk
132200
with sentry_sdk.push_scope() as scope:
133201
for key, value in extra_context.items():
134202
scope.set_extra(key, value)
135203
sentry_sdk.capture_message(message, level=level)
136-
except ImportError:
137-
pass # Sentry not installed
204+
except Exception:
205+
pass
206+
207+
208+
# ---------------------------------------------------------------------------
209+
# Background Task Decorator
210+
# ---------------------------------------------------------------------------
211+
212+
def track_background_task(operation: str):
213+
"""
214+
Decorator to track background tasks and capture any errors.
215+
216+
Usage:
217+
@track_background_task("indexing")
218+
async def index_repository(repo_id: str):
219+
# any unhandled exception here will be captured with context
220+
"""
221+
def decorator(func: Callable) -> Callable:
222+
@functools.wraps(func)
223+
async def async_wrapper(*args, **kwargs) -> Any:
224+
if not _sentry_initialized:
225+
return await func(*args, **kwargs)
226+
227+
try:
228+
import sentry_sdk
229+
with sentry_sdk.push_scope() as scope:
230+
scope.set_tag("operation", operation)
231+
scope.set_tag("background_task", "true")
232+
# Add function args as context
233+
scope.set_extra("args", str(args)[:500])
234+
scope.set_extra("kwargs", str(kwargs)[:500])
235+
236+
try:
237+
return await func(*args, **kwargs)
238+
except Exception as e:
239+
sentry_sdk.capture_exception(e)
240+
raise # Re-raise so caller knows it failed
241+
except ImportError:
242+
return await func(*args, **kwargs)
243+
244+
@functools.wraps(func)
245+
def sync_wrapper(*args, **kwargs) -> Any:
246+
if not _sentry_initialized:
247+
return func(*args, **kwargs)
248+
249+
try:
250+
import sentry_sdk
251+
with sentry_sdk.push_scope() as scope:
252+
scope.set_tag("operation", operation)
253+
scope.set_tag("background_task", "true")
254+
scope.set_extra("args", str(args)[:500])
255+
scope.set_extra("kwargs", str(kwargs)[:500])
256+
257+
try:
258+
return func(*args, **kwargs)
259+
except Exception as e:
260+
sentry_sdk.capture_exception(e)
261+
raise
262+
except ImportError:
263+
return func(*args, **kwargs)
264+
265+
# Return appropriate wrapper based on function type
266+
if asyncio_iscoroutinefunction(func):
267+
return async_wrapper
268+
return sync_wrapper
269+
270+
return decorator
271+
272+
273+
def asyncio_iscoroutinefunction(func):
274+
"""Check if function is async."""
275+
import asyncio
276+
return asyncio.iscoroutinefunction(func)
277+
278+
279+
# ---------------------------------------------------------------------------
280+
# HTTP Exception Handler Helper
281+
# ---------------------------------------------------------------------------
282+
283+
def capture_http_exception(request, exc, status_code: int):
284+
"""
285+
Capture HTTP exceptions that would otherwise be swallowed.
286+
Call this from FastAPI exception handlers for 500+ errors.
287+
288+
Args:
289+
request: FastAPI request object
290+
exc: The exception
291+
status_code: HTTP status code being returned
292+
"""
293+
# Only capture server errors (5xx)
294+
if status_code < 500 or not _sentry_initialized:
295+
return
296+
297+
try:
298+
import sentry_sdk
299+
with sentry_sdk.push_scope() as scope:
300+
scope.set_tag("http_status", str(status_code))
301+
scope.set_extra("path", str(request.url.path))
302+
scope.set_extra("method", request.method)
303+
sentry_sdk.capture_exception(exc)
304+
except Exception:
305+
pass

0 commit comments

Comments
 (0)