Skip to content
Open
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 @@ -12,3 +12,8 @@ DAILY_CALL_LIMIT=50

# Production frontend origin for CORS (e.g. https://your-app.railway.app)
# ALLOWED_ORIGIN=https://your-app.railway.app

# Sentry DSN for production error monitoring — optional, get one free at
# https://sentry.io. Leave unset to disable error monitoring entirely (safe
# default for local dev/CI).
# SENTRY_DSN=https://...@o0.ingest.sentry.io/0
2 changes: 2 additions & 0 deletions agent/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from datetime import date

import anthropic
import sentry_sdk
from dotenv import load_dotenv

from agent import db
Expand Down Expand Up @@ -249,6 +250,7 @@ def _run_tools(response_content: list, user_id: int, on_result=None, source: str
result = TOOL_HANDLERS[block.name](**kwargs)
except Exception:
logger.error("tool %s(%s) failed:\n%s", block.name, kwargs, traceback.format_exc())
sentry_sdk.capture_exception()
result = {"status": "error", "message": f"{block.name} failed unexpectedly — tell the user and don't retry automatically."}
db.record_usage(user_id, "tool", block.name, source)
print(f"[tool] {block.name}({kwargs}) -> {result}")
Expand Down
12 changes: 12 additions & 0 deletions api/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from datetime import date
from pathlib import Path

import sentry_sdk
from fastapi import Depends, FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse, StreamingResponse
Expand Down Expand Up @@ -37,6 +38,16 @@

logger = logging.getLogger(__name__)

# Error monitoring — a no-op until SENTRY_DSN is set (sentry_sdk.init(dsn=None)
# disables the SDK entirely, so this is safe to leave unconfigured in dev/CI).
# Error capture only, no performance tracing — that's a separate cost/scope
# this app doesn't need yet. send_default_pii is explicitly off: the SDK
# auto-instruments the anthropic client, and every Claude call here carries
# real expense/income descriptions — that must never leave this app for a
# third party, on top of the SYSTEM prompt/user messages being sensitive on
# their own.
sentry_sdk.init(dsn=os.environ.get("SENTRY_DSN"), traces_sample_rate=0.0, send_default_pii=False)

DAILY_CALL_LIMIT = int(os.environ.get("DAILY_CALL_LIMIT", 50))


Expand Down Expand Up @@ -110,6 +121,7 @@ def generate():
yield f"data: {json.dumps(event)}\n\n"
except Exception:
logger.error("stream_chat error:\n%s", traceback.format_exc())
sentry_sdk.capture_exception()
yield f"data: {json.dumps({'error': 'Something went wrong. Please try again.'})}\n\n"
yield "data: [DONE]\n\n"

Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ dependencies = [
"psycopg2-binary>=2.9.12",
"python-dotenv>=1.2.2",
"python-jose[cryptography]>=3.5.0",
"sentry-sdk>=2.65.0",
"uvicorn>=0.49.0",
]

Expand Down
15 changes: 15 additions & 0 deletions tests/test_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,21 @@ def failing_handler(**kw):
assert results[2]["content"] == str({"ok": True})


def test_run_tools_reports_failing_handler_to_sentry(monkeypatch):
captured = []
monkeypatch.setattr(main.sentry_sdk, "capture_exception", lambda: captured.append(True))

def failing_handler(**kw):
raise ValueError("boom")

monkeypatch.setitem(main.TOOL_HANDLERS, "save_expense", failing_handler)
block = make_block("tool_use", name="save_expense", input={"amount": 5}, id="tool_1")

main._run_tools([block], user_id=1)

assert captured == [True]


# --- _serialize_block --------------------------------------------------------
# Required for persisting assistant turns to Postgres — a live Anthropic SDK
# response block in production, but test doubles are plain dicts or
Expand Down
15 changes: 15 additions & 0 deletions tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,21 @@ def fake_stream_chat(message, user_id, username, images, source):
assert "data: [DONE]" in response.text


def test_chat_stream_endpoint_reports_exception_to_sentry(monkeypatch, auth_headers):
captured = []
monkeypatch.setattr(server.sentry_sdk, "capture_exception", lambda: captured.append(True))

def fake_stream_chat(message, user_id, username, images):
raise RuntimeError("boom")
yield # pragma: no cover — makes this a generator, never reached

monkeypatch.setattr(server, "stream_chat", fake_stream_chat)

client.post("/chat/stream", json={"message": "hi"}, headers=auth_headers)

assert captured == [True]


# --- check_rate_limit --------------------------------------------------

def test_check_rate_limit_allows_calls_under_limit(user_id):
Expand Down
24 changes: 24 additions & 0 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.