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
17 changes: 17 additions & 0 deletions test-fastapi-with-database/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
FROM python:3.12-slim

WORKDIR /app

# Install uv
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/

# Copy project files
COPY pyproject.toml .
COPY main.py .

# Dependencies are installed at startup via entrypoint because
# sentry-python is mounted as a volume (not available at build time).
COPY entrypoint.sh .
RUN chmod +x entrypoint.sh

CMD ["./entrypoint.sh"]
28 changes: 28 additions & 0 deletions test-fastapi-with-database/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# test-fastapi-with-database

## Prerequisites

- Docker and Docker Compose
- A Sentry DSN

## Running

```bash
SENTRY_DSN=<your-dsn> ./run.sh
```

This starts two containers:
- **db**: PostgreSQL 16 with a `test_multiline` database
- **app**: FastAPI server on port 5000, using a local editable install of `sentry-python` (mounted from `../../sentry-python`)

On first startup, the app creates `users` and `posts` tables and seeds sample data.

## Reproducing the issue

1. Hit the query endpoint:

```bash
curl http://localhost:5000/query
```

2. Watch the app container logs for the `[before_send_transaction]` output. The callback attempts to match a multiline SQL string against span descriptions but fails to find a match.
31 changes: 31 additions & 0 deletions test-fastapi-with-database/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
services:
db:
image: postgres:16
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: test_multiline
ports:
- "5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 2s
timeout: 5s
retries: 5

app:
build: .
ports:
- "5000:5000"
environment:
DATABASE_HOST: db
DATABASE_USER: postgres
DATABASE_PASSWORD: postgres
DATABASE_NAME: test_multiline
SENTRY_DSN: ${SENTRY_DSN:-}
ENV: local
volumes:
- ~/sentry-python:/sentry-python
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Volume mount path inconsistent with repo convention

Medium Severity

The volume mount in docker-compose.yml uses ~/sentry-python:/sentry-python, but the README states it's "mounted from ../../sentry-python". Every other test app in this repository references sentry-python at ../../sentry-python (a relative path two directories up). The ~/sentry-python path assumes the repo is cloned directly in the user's home directory, which won't be the case for most developers. This will cause the container to start with an empty or missing /sentry-python volume, and uv sync will fail.

Additional Locations (1)
Fix in Cursor Fix in Web

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The docker-compose.yml volume mount ~/sentry-python is inconsistent with the README.md and repository conventions, which will cause the application startup to fail.
Severity: CRITICAL

Suggested Fix

Update the volume mount in docker-compose.yml from ~/sentry-python:/sentry-python to ../../sentry-python:/sentry-python. This aligns the configuration with the README.md documentation and the established convention used by all other test applications in the repository, ensuring the local sentry-python source is correctly mounted.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: test-fastapi-with-database/docker-compose.yml#L28

Potential issue: The `docker-compose.yml` specifies a volume mount from
`~/sentry-python`, which resolves to the user's home directory. This contradicts the
`README.md` and the established pattern in over 50 other test apps, which use a relative
path `../../sentry-python`. Inside the container, the `pyproject.toml` expects the
package at `/sentry-python`. When a developer follows the standard repository setup, the
mount will be incorrect, causing the `uv sync` command in the entrypoint to fail when it
cannot resolve the editable `sentry-sdk` package. This will prevent the test application
from starting.

Did we get this right? 👍 / 👎 to inform future reviews.

depends_on:
db:
condition: service_healthy
8 changes: 8 additions & 0 deletions test-fastapi-with-database/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -e

# Install dependencies (sentry-python is mounted as a volume at /sentry-python)
uv sync --no-dev

# Start the app
uv run uvicorn main:app --host 0.0.0.0 --port 5000
143 changes: 143 additions & 0 deletions test-fastapi-with-database/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import os

import asyncpg
import sentry_sdk
from fastapi import FastAPI
from sentry_sdk.integrations.asyncpg import AsyncPGIntegration
from sentry_sdk.integrations.fastapi import FastApiIntegration
from sentry_sdk.integrations.starlette import StarletteIntegration
import logging

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

DATABASE_USER = os.environ.get("DATABASE_USER", "postgres")
DATABASE_PASSWORD = os.environ.get("DATABASE_PASSWORD", "postgres")
DATABASE_HOST = os.environ.get("DATABASE_HOST", "localhost")
DATABASE_PORT = os.environ.get("DATABASE_PORT", "5432")
DATABASE_NAME = os.environ.get("DATABASE_NAME", "test_multiline")

DATABASE_URL = f"postgresql://{DATABASE_USER}:{DATABASE_PASSWORD}@{DATABASE_HOST}:{DATABASE_PORT}/{DATABASE_NAME}"


# This multiline string simulates what a user would copy from the Sentry UI
# "Traces" page and paste into their before_send_transaction callback.
# The Sentry UI displays the span description (the SQL query) with its
# original newlines/whitespace intact.
QUERY_TO_FILTER = "SELECT u.id, u.name, u.email, p.title AS post_title, p.created_at AS post_date FROM users u JOIN posts p ON u.id = p.user_id WHERE u.active = true ORDER BY p.created_at DESC"

def before_send_transaction(event, hint):
"""
User's before_send_transaction callback that attempts to filter out
transactions containing a specific multiline SQL query.

The user copied the query string from the Sentry UI Traces page
and wants to drop transactions that contain this query as a span.
"""
for span in event.get("spans", []):
description = span.get("description", "")
if description and QUERY_TO_FILTER in description:
logger.info(f"[before_send_transaction] MATCH FOUND - dropping transaction")
return None

logger.info(f"[before_send_transaction] No match found - keeping transaction")
return event


sentry_sdk.init(
dsn=os.environ.get("SENTRY_DSN"),
environment=os.environ.get("ENV", "local"),
traces_sample_rate=1.0,
debug=True,
integrations=[
StarletteIntegration(),
FastApiIntegration(),
AsyncPGIntegration(),
],
before_send_transaction=before_send_transaction,
)

app = FastAPI()


@app.on_event("startup")
async def startup():
app.state.pool = await asyncpg.create_pool(DATABASE_URL)

# Create tables if they don't exist
async with app.state.pool.acquire() as conn:
await conn.execute("""
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL,
active BOOLEAN DEFAULT true
)
""")
await conn.execute("""
CREATE TABLE IF NOT EXISTS posts (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id),
title TEXT NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
)
""")

# Seed some data if tables are empty
count = await conn.fetchval("SELECT COUNT(*) FROM users")
if count == 0:
await conn.execute("""
INSERT INTO users (name, email, active) VALUES
('Alice', 'alice@example.com', true),
('Bob', 'bob@example.com', true),
('Charlie', 'charlie@example.com', false)
""")
await conn.execute("""
INSERT INTO posts (user_id, title) VALUES
(1, 'First Post'),
(1, 'Second Post'),
(2, 'Hello World')
""")


@app.on_event("shutdown")
async def shutdown():
await app.state.pool.close()


@app.get("/")
async def root():
return {
"endpoints": {
"multiline_query": "http://localhost:5000/query",
}
}


@app.get("/query")
async def multiline_query():
"""
Executes a multiline SQL query against the database.
The span description captured by Sentry will contain the query
with its original newlines.
"""
async with app.state.pool.acquire() as conn:
rows = await conn.fetch("""SELECT
u.id,
u.name,
u.email,
p.title AS post_title,
p.created_at AS post_date
FROM
users u
JOIN
posts p ON u.id = p.user_id
WHERE
u.active = true
ORDER BY
p.created_at DESC""")

return {
"count": len(rows),
"results": [dict(r) for r in rows],
}
14 changes: 14 additions & 0 deletions test-fastapi-with-database/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[project]
name = "test-fastapi-with-database"
version = "0"
requires-python = ">=3.12"

dependencies = [
"fastapi>=0.115.11",
"asyncpg>=0.30.0",
"sentry-sdk[fastapi,asyncpg]",
"uvicorn>=0.34.0",
]

[tool.uv.sources]
sentry-sdk = { path = "/sentry-python", editable = true }
4 changes: 4 additions & 0 deletions test-fastapi-with-database/run.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/usr/bin/env bash
set -euo pipefail

docker compose up --build
Loading