33Provides production error visibility and performance monitoring
44"""
55import 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
915def 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
6674def _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+
8497def 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+
103165def 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
121186def 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