Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,8 @@ ALLOWED_ORIGINS=http://localhost:3000
# Redis (auto-configured in Docker, set REDIS_URL in Railway)
REDIS_HOST=redis
REDIS_PORT=6379

# Sentry Error Tracking (Optional but recommended for production)
# Get DSN from: https://sentry.io → Settings → Projects → Client Keys
SENTRY_DSN=
ENVIRONMENT=development # development, staging, production
5 changes: 5 additions & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,8 @@ ALLOWED_ORIGINS=http://localhost:3000
# Redis Cache
REDIS_HOST=localhost
REDIS_PORT=6379

# Sentry Error Tracking (Optional)
# Get DSN from https://sentry.io → Settings → Projects → Client Keys
SENTRY_DSN=
ENVIRONMENT=development
19 changes: 19 additions & 0 deletions backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
from starlette.responses import JSONResponse
import os

# Initialize Sentry FIRST (before other imports to catch all errors)
from services.sentry import init_sentry
init_sentry()

# Import API config (single source of truth for versioning)
from config.api import API_PREFIX, API_VERSION

Expand Down Expand Up @@ -108,3 +112,18 @@ async def rate_limit_handler(request: Request, exc):
status_code=429,
content={"detail": "Rate limit exceeded. Please try again later."}
)


@app.exception_handler(Exception)
async def generic_exception_handler(request: Request, exc: Exception):
"""
Catch-all handler for unhandled exceptions.
Captures to Sentry and returns 500.
"""
from services.sentry import capture_http_exception
capture_http_exception(request, exc, 500)

return JSONResponse(
status_code=500,
content={"detail": "Internal server error"}
)
6 changes: 6 additions & 0 deletions backend/middleware/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,11 +122,17 @@ def _authenticate(token: str) -> AuthContext:
# Try JWT (Supabase tokens)
ctx = _validate_jwt(token)
if ctx:
# Set Sentry user context for error tracking
from services.sentry import set_user_context
set_user_context(user_id=ctx.user_id, email=ctx.email)
return ctx

# Try API key
ctx = _validate_api_key(token)
if ctx:
# Set Sentry user context for error tracking
from services.sentry import set_user_context
set_user_context(user_id=ctx.user_id or ctx.api_key_name)
return ctx

# Neither worked
Expand Down
3 changes: 3 additions & 0 deletions backend/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,6 @@ pyjwt>=2.8.0 # JWT token verification for Supabase Auth
pytest>=8.0.0
pytest-asyncio>=0.24.0
pytest-cov>=6.0.0

# Observability
sentry-sdk[fastapi]>=2.0.0
13 changes: 7 additions & 6 deletions backend/routes/analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
)
from services.input_validator import InputValidator
from middleware.auth import require_auth, AuthContext
from services.observability import logger, metrics

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

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

# Build fresh
print(f"🔄 Building fresh dependency graph for {repo_id}")
logger.info("Building fresh dependency graph", repo_id=repo_id)
graph_data = dependency_analyzer.build_dependency_graph(repo["local_path"])
dependency_analyzer.save_to_cache(repo_id, graph_data)

Expand Down Expand Up @@ -62,7 +63,7 @@ async def analyze_impact(
# Get or build graph
graph_data = dependency_analyzer.load_from_cache(repo_id)
if not graph_data:
print(f"🔄 Building dependency graph for impact analysis")
logger.info("Building dependency graph for impact analysis", repo_id=repo_id)
graph_data = dependency_analyzer.build_dependency_graph(repo["local_path"])
dependency_analyzer.save_to_cache(repo_id, graph_data)

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

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

# Analyze fresh
print(f"🔄 Analyzing code style for {repo_id}")
logger.info("Analyzing code style", repo_id=repo_id)
style_data = style_analyzer.analyze_repository_style(repo["local_path"])
style_analyzer.save_to_cache(repo_id, style_data)

Expand Down
5 changes: 3 additions & 2 deletions backend/routes/playground.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from dependencies import indexer, cache, repo_manager
from services.input_validator import InputValidator
from services.observability import logger

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

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


def _check_rate_limit(ip: str) -> tuple[bool, int]:
Expand Down
9 changes: 6 additions & 3 deletions backend/routes/repos.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
)
from services.input_validator import InputValidator
from middleware.auth import require_auth, AuthContext
from services.observability import logger, capture_exception

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

Expand Down Expand Up @@ -100,15 +101,15 @@ async def index_repository(
last_commit = repo_manager.get_last_indexed_commit(repo_id)

if incremental and last_commit:
print(f"🔄 Using INCREMENTAL indexing (last: {last_commit[:8]})")
logger.info("Using INCREMENTAL indexing", repo_id=repo_id, last_commit=last_commit[:8])
total_functions = await indexer.incremental_index_repository(
repo_id,
repo["local_path"],
last_commit
)
index_type = "incremental"
else:
print(f"📦 Using FULL indexing")
logger.info("Using FULL indexing", repo_id=repo_id)
total_functions = await indexer.index_repository(repo_id, repo["local_path"])
index_type = "full"

Expand Down Expand Up @@ -204,8 +205,10 @@ async def progress_callback(files_processed: int, functions_indexed: int, total_
pass

except WebSocketDisconnect:
print(f"WebSocket disconnected for repo {repo_id}")
logger.debug("WebSocket disconnected", repo_id=repo_id)
except Exception as e:
logger.error("WebSocket indexing error", repo_id=repo_id, error=str(e))
capture_exception(e, operation="websocket_indexing", repo_id=repo_id)
try:
await websocket.send_json({"type": "error", "message": str(e)})
except Exception:
Expand Down
26 changes: 17 additions & 9 deletions backend/services/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
import os
from dotenv import load_dotenv

from services.observability import logger, metrics

load_dotenv()

# Configuration
Expand All @@ -30,7 +32,7 @@ def __init__(self):
socket_connect_timeout=5,
socket_timeout=5
)
print(f"✅ Redis connected via URL!")
logger.info("Redis connected via URL")
else:
self.redis = redis.Redis(
host=REDIS_HOST,
Expand All @@ -40,12 +42,12 @@ def __init__(self):
socket_connect_timeout=5,
socket_timeout=5
)
print(f"✅ Redis connected to {REDIS_HOST}:{REDIS_PORT}")
logger.info("Redis connected", host=REDIS_HOST, port=REDIS_PORT)

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

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

return None

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

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

return None

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

def invalidate_repo(self, repo_id: str):
"""Invalidate all cache for a repository"""
Expand All @@ -123,6 +131,6 @@ def invalidate_repo(self, repo_id: str):
keys = self.redis.keys(pattern)
if keys:
self.redis.delete(*keys)
print(f"Invalidated {len(keys)} cache entries")
logger.info("Cache invalidated", repo_id=repo_id, keys_removed=len(keys))
except Exception as e:
print(f"Cache invalidation error: {e}")
logger.error("Cache invalidation error", repo_id=repo_id, error=str(e))
25 changes: 14 additions & 11 deletions backend/services/dependency_analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
import tree_sitter_javascript as tsjavascript
from tree_sitter import Language, Parser

from services.observability import logger, capture_exception, track_time, metrics


class DependencyAnalyzer:
"""Analyze code dependencies and build dependency graph"""
Expand All @@ -22,7 +24,7 @@ def __init__(self):
'javascript': Parser(Language(tsjavascript.language())),
'typescript': Parser(Language(tsjavascript.language())),
}
print("✅ DependencyAnalyzer initialized!")
logger.info("DependencyAnalyzer initialized")

def _detect_language(self, file_path: str) -> str:
"""Detect language from file extension"""
Expand Down Expand Up @@ -117,7 +119,7 @@ def analyze_file_dependencies(self, file_path: str) -> Dict:
}

except Exception as e:
print(f"Error analyzing {file_path}: {e}")
logger.error("Error analyzing file", file_path=file_path, error=str(e))
return {"file": str(file_path), "imports": [], "language": language, "error": str(e)}

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

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

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

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

# Find a file with imports to debug
for f, imports in list(file_dependencies.items())[:5]:
if imports:
print(f"📄 {f} imports: {imports[:3]}")
logger.debug("Sample file imports", file=f, imports=imports[:3])
break

# Create nodes
Expand Down Expand Up @@ -198,12 +200,13 @@ def build_dependency_graph(self, repo_path: str) -> Dict:
else:
failed_count += 1

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

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

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

return {
"nodes": nodes,
Expand Down Expand Up @@ -440,7 +443,7 @@ def save_to_cache(self, repo_id: str, graph_data: Dict):
db.clear_file_dependencies(repo_id)

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

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

db.upsert_repository_insights(repo_id, insights)
print(f"✅ Cached dependency graph for {repo_id} in Supabase")
logger.info("Cached dependency graph in Supabase", repo_id=repo_id)

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

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

if not file_deps:
return None
Expand All @@ -492,7 +495,7 @@ def load_from_cache(self, repo_id: str) -> Dict:
"total_edges": len(edges)
}

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

return {
"dependencies": dependencies,
Expand Down
Loading