Skip to content

Commit bf40c59

Browse files
authored
Merge pull request #91 from DevanshuNEU/feat/issue-54-sentry-integration
feat(backend): Add Sentry error tracking integration
2 parents b305ec4 + 9403e06 commit bf40c59

18 files changed

Lines changed: 737 additions & 116 deletions

.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: 19 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

@@ -108,3 +112,18 @@ async def rate_limit_handler(request: Request, exc):
108112
status_code=429,
109113
content={"detail": "Rate limit exceeded. Please try again later."}
110114
)
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/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/routes/analysis.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
)
99
from services.input_validator import InputValidator
1010
from middleware.auth import require_auth, AuthContext
11+
from services.observability import logger, metrics
1112

1213
router = APIRouter(prefix="/repos", tags=["Analysis"])
1314

@@ -29,11 +30,11 @@ async def get_dependency_graph(
2930
# Try cache first
3031
cached_graph = dependency_analyzer.load_from_cache(repo_id)
3132
if cached_graph:
32-
print(f"✅ Using cached dependency graph for {repo_id}")
33+
logger.debug("Using cached dependency graph", repo_id=repo_id)
3334
return {**cached_graph, "cached": True}
3435

3536
# Build fresh
36-
print(f"🔄 Building fresh dependency graph for {repo_id}")
37+
logger.info("Building fresh dependency graph", repo_id=repo_id)
3738
graph_data = dependency_analyzer.build_dependency_graph(repo["local_path"])
3839
dependency_analyzer.save_to_cache(repo_id, graph_data)
3940

@@ -62,7 +63,7 @@ async def analyze_impact(
6263
# Get or build graph
6364
graph_data = dependency_analyzer.load_from_cache(repo_id)
6465
if not graph_data:
65-
print(f"🔄 Building dependency graph for impact analysis")
66+
logger.info("Building dependency graph for impact analysis", repo_id=repo_id)
6667
graph_data = dependency_analyzer.build_dependency_graph(repo["local_path"])
6768
dependency_analyzer.save_to_cache(repo_id, graph_data)
6869

@@ -89,7 +90,7 @@ async def get_repository_insights(
8990
# Get or build graph
9091
graph_data = dependency_analyzer.load_from_cache(repo_id)
9192
if not graph_data:
92-
print(f"🔄 Building dependency graph for insights")
93+
logger.info("Building dependency graph for insights", repo_id=repo_id)
9394
graph_data = dependency_analyzer.build_dependency_graph(repo["local_path"])
9495
dependency_analyzer.save_to_cache(repo_id, graph_data)
9596

@@ -121,11 +122,11 @@ async def get_style_analysis(
121122
# Try cache first
122123
cached_style = style_analyzer.load_from_cache(repo_id)
123124
if cached_style:
124-
print(f"✅ Using cached code style for {repo_id}")
125+
logger.debug("Using cached code style", repo_id=repo_id)
125126
return {**cached_style, "cached": True}
126127

127128
# Analyze fresh
128-
print(f"🔄 Analyzing code style for {repo_id}")
129+
logger.info("Analyzing code style", repo_id=repo_id)
129130
style_data = style_analyzer.analyze_repository_style(repo["local_path"])
130131
style_analyzer.save_to_cache(repo_id, style_data)
131132

backend/routes/playground.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from dependencies import indexer, cache, repo_manager
88
from services.input_validator import InputValidator
9+
from services.observability import logger
910

1011
router = APIRouter(prefix="/playground", tags=["Playground"])
1112

@@ -39,9 +40,9 @@ async def load_demo_repos():
3940
DEMO_REPO_IDS["express"] = repo["id"]
4041
elif "react" in name_lower:
4142
DEMO_REPO_IDS["react"] = repo["id"]
42-
print(f"📦 Loaded demo repos: {list(DEMO_REPO_IDS.keys())}")
43+
logger.info("Loaded demo repos", repos=list(DEMO_REPO_IDS.keys()))
4344
except Exception as e:
44-
print(f"⚠️ Could not load demo repos: {e}")
45+
logger.warning("Could not load demo repos", error=str(e))
4546

4647

4748
def _check_rate_limit(ip: str) -> tuple[bool, int]:

backend/routes/repos.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
)
1313
from services.input_validator import InputValidator
1414
from middleware.auth import require_auth, AuthContext
15+
from services.observability import logger, capture_exception
1516

1617
router = APIRouter(prefix="/repos", tags=["Repositories"])
1718

@@ -100,15 +101,15 @@ async def index_repository(
100101
last_commit = repo_manager.get_last_indexed_commit(repo_id)
101102

102103
if incremental and last_commit:
103-
print(f"🔄 Using INCREMENTAL indexing (last: {last_commit[:8]})")
104+
logger.info("Using INCREMENTAL indexing", repo_id=repo_id, last_commit=last_commit[:8])
104105
total_functions = await indexer.incremental_index_repository(
105106
repo_id,
106107
repo["local_path"],
107108
last_commit
108109
)
109110
index_type = "incremental"
110111
else:
111-
print(f"📦 Using FULL indexing")
112+
logger.info("Using FULL indexing", repo_id=repo_id)
112113
total_functions = await indexer.index_repository(repo_id, repo["local_path"])
113114
index_type = "full"
114115

@@ -204,8 +205,10 @@ async def progress_callback(files_processed: int, functions_indexed: int, total_
204205
pass
205206

206207
except WebSocketDisconnect:
207-
print(f"WebSocket disconnected for repo {repo_id}")
208+
logger.debug("WebSocket disconnected", repo_id=repo_id)
208209
except Exception as e:
210+
logger.error("WebSocket indexing error", repo_id=repo_id, error=str(e))
211+
capture_exception(e, operation="websocket_indexing", repo_id=repo_id)
209212
try:
210213
await websocket.send_json({"type": "error", "message": str(e)})
211214
except Exception:

backend/services/cache.py

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
import os
1010
from dotenv import load_dotenv
1111

12+
from services.observability import logger, metrics
13+
1214
load_dotenv()
1315

1416
# Configuration
@@ -30,7 +32,7 @@ def __init__(self):
3032
socket_connect_timeout=5,
3133
socket_timeout=5
3234
)
33-
print(f"✅ Redis connected via URL!")
35+
logger.info("Redis connected via URL")
3436
else:
3537
self.redis = redis.Redis(
3638
host=REDIS_HOST,
@@ -40,12 +42,12 @@ def __init__(self):
4042
socket_connect_timeout=5,
4143
socket_timeout=5
4244
)
43-
print(f"✅ Redis connected to {REDIS_HOST}:{REDIS_PORT}")
45+
logger.info("Redis connected", host=REDIS_HOST, port=REDIS_PORT)
4446

4547
# Test connection
4648
self.redis.ping()
4749
except redis.ConnectionError as e:
48-
print(f"⚠️ Redis not available - running without cache: {e}")
50+
logger.warning("Redis not available - running without cache", error=str(e))
4951
self.redis = None
5052

5153
def _make_key(self, prefix: str, *args) -> str:
@@ -64,9 +66,12 @@ def get_search_results(self, query: str, repo_id: str) -> Optional[List[Dict]]:
6466
key = self._make_key("search", repo_id, query)
6567
cached = self.redis.get(key)
6668
if cached:
69+
metrics.increment("cache_hits")
6770
return json.loads(cached)
71+
metrics.increment("cache_misses")
6872
except Exception as e:
69-
print(f"Cache read error: {e}")
73+
logger.error("Cache read error", operation="get_search_results", error=str(e))
74+
metrics.increment("cache_errors")
7075

7176
return None
7277

@@ -85,7 +90,8 @@ def set_search_results(
8590
key = self._make_key("search", repo_id, query)
8691
self.redis.setex(key, ttl, json.dumps(results))
8792
except Exception as e:
88-
print(f"Cache write error: {e}")
93+
logger.error("Cache write error", operation="set_search_results", error=str(e))
94+
metrics.increment("cache_errors")
8995

9096
def get_embedding(self, text: str) -> Optional[List[float]]:
9197
"""Get cached embedding"""
@@ -98,7 +104,8 @@ def get_embedding(self, text: str) -> Optional[List[float]]:
98104
if cached:
99105
return json.loads(cached)
100106
except Exception as e:
101-
print(f"Cache read error: {e}")
107+
logger.error("Cache read error", operation="get_embedding", error=str(e))
108+
metrics.increment("cache_errors")
102109

103110
return None
104111

@@ -111,7 +118,8 @@ def set_embedding(self, text: str, embedding: List[float], ttl: int = 86400):
111118
key = self._make_key("emb", text[:100])
112119
self.redis.setex(key, ttl, json.dumps(embedding))
113120
except Exception as e:
114-
print(f"Cache write error: {e}")
121+
logger.error("Cache write error", operation="set_embedding", error=str(e))
122+
metrics.increment("cache_errors")
115123

116124
def invalidate_repo(self, repo_id: str):
117125
"""Invalidate all cache for a repository"""
@@ -123,6 +131,6 @@ def invalidate_repo(self, repo_id: str):
123131
keys = self.redis.keys(pattern)
124132
if keys:
125133
self.redis.delete(*keys)
126-
print(f"Invalidated {len(keys)} cache entries")
134+
logger.info("Cache invalidated", repo_id=repo_id, keys_removed=len(keys))
127135
except Exception as e:
128-
print(f"Cache invalidation error: {e}")
136+
logger.error("Cache invalidation error", repo_id=repo_id, error=str(e))

backend/services/dependency_analyzer.py

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
import tree_sitter_javascript as tsjavascript
1212
from tree_sitter import Language, Parser
1313

14+
from services.observability import logger, capture_exception, track_time, metrics
15+
1416

1517
class DependencyAnalyzer:
1618
"""Analyze code dependencies and build dependency graph"""
@@ -22,7 +24,7 @@ def __init__(self):
2224
'javascript': Parser(Language(tsjavascript.language())),
2325
'typescript': Parser(Language(tsjavascript.language())),
2426
}
25-
print("✅ DependencyAnalyzer initialized!")
27+
logger.info("DependencyAnalyzer initialized")
2628

2729
def _detect_language(self, file_path: str) -> str:
2830
"""Detect language from file extension"""
@@ -117,7 +119,7 @@ def analyze_file_dependencies(self, file_path: str) -> Dict:
117119
}
118120

119121
except Exception as e:
120-
print(f"Error analyzing {file_path}: {e}")
122+
logger.error("Error analyzing file", file_path=file_path, error=str(e))
121123
return {"file": str(file_path), "imports": [], "language": language, "error": str(e)}
122124

123125
def build_dependency_graph(self, repo_path: str) -> Dict:
@@ -137,7 +139,7 @@ def build_dependency_graph(self, repo_path: str) -> Dict:
137139
if file_path.suffix in extensions:
138140
code_files.append(file_path)
139141

140-
print(f"📊 Building dependency graph for {len(code_files)} files...")
142+
logger.info("Building dependency graph", file_count=len(code_files))
141143

142144
# Analyze each file
143145
file_dependencies = {}
@@ -157,12 +159,12 @@ def build_dependency_graph(self, repo_path: str) -> Dict:
157159

158160
# DEBUG: Show sample of what we're working with
159161
sample_files = list(internal_files)[:3]
160-
print(f"📁 Sample internal files: {sample_files}")
162+
logger.debug("Sample internal files", sample=sample_files)
161163

162164
# Find a file with imports to debug
163165
for f, imports in list(file_dependencies.items())[:5]:
164166
if imports:
165-
print(f"📄 {f} imports: {imports[:3]}")
167+
logger.debug("Sample file imports", file=f, imports=imports[:3])
166168
break
167169

168170
# Create nodes
@@ -198,12 +200,13 @@ def build_dependency_graph(self, repo_path: str) -> Dict:
198200
else:
199201
failed_count += 1
200202

201-
print(f"🔗 Resolved {resolved_count} internal imports, {failed_count} external")
203+
logger.info("Import resolution complete", resolved=resolved_count, external=failed_count)
202204

203205
# Calculate metrics
204206
graph_metrics = self._calculate_graph_metrics(file_dependencies, edges)
205207

206-
print(f"✅ Graph built: {len(nodes)} nodes, {len(edges)} edges")
208+
logger.info("Dependency graph built", nodes=len(nodes), edges=len(edges))
209+
metrics.increment("dependency_graphs_built")
207210

208211
return {
209212
"nodes": nodes,
@@ -440,7 +443,7 @@ def save_to_cache(self, repo_id: str, graph_data: Dict):
440443
db.clear_file_dependencies(repo_id)
441444

442445
# Bulk insert new dependencies
443-
print(f"💾 Saving {len(file_deps)} file dependencies to Supabase")
446+
logger.info("Saving file dependencies to Supabase", repo_id=repo_id, count=len(file_deps))
444447
db.upsert_file_dependencies(repo_id, file_deps)
445448

446449
# Save repository insights
@@ -457,7 +460,7 @@ def save_to_cache(self, repo_id: str, graph_data: Dict):
457460
}
458461

459462
db.upsert_repository_insights(repo_id, insights)
460-
print(f"✅ Cached dependency graph for {repo_id} in Supabase")
463+
logger.info("Cached dependency graph in Supabase", repo_id=repo_id)
461464

462465
def load_from_cache(self, repo_id: str) -> Dict:
463466
"""Load dependency graph from Supabase cache"""
@@ -467,7 +470,7 @@ def load_from_cache(self, repo_id: str) -> Dict:
467470

468471
# Get file dependencies
469472
file_deps = db.get_file_dependencies(repo_id)
470-
print(f"🔍 Loading cache for {repo_id}: found {len(file_deps) if file_deps else 0} file dependencies")
473+
logger.debug("Loading cache", repo_id=repo_id, found=len(file_deps) if file_deps else 0)
471474

472475
if not file_deps:
473476
return None
@@ -492,7 +495,7 @@ def load_from_cache(self, repo_id: str) -> Dict:
492495
"total_edges": len(edges)
493496
}
494497

495-
print(f"✅ Loaded cached dependency graph for {repo_id} from Supabase")
498+
logger.info("Loaded cached dependency graph", repo_id=repo_id)
496499

497500
return {
498501
"dependencies": dependencies,

0 commit comments

Comments
 (0)