# Python (Sentry-compatible SDK)

> Use the official sentry-sdk for Python with a Belvador DSN and the data-scrubbing hook Belvador provides.

For Python Belvador uses the **official `sentry-sdk`** with your Belvador DSN, plus the init options and scrubbing hook below.

> [!NOTE]
> This is a compatibility setup, not a Belvador package. Belvador's tests run the `scrub_event` hook with Python on hostile data, but Belvador does not certify `sentry-sdk` itself or its framework integrations. Your node scrubs again before storing anything — see [Data protection](https://belvador.dev/en/docs/privacy).

## Before you start

- A project with platform **Python** and its DSN.
- A Python version supported by the current `sentry-sdk` release (see [Sentry's Python guide](https://docs.sentry.io/platforms/python/)).

## 1. Install

```bash
pip install --upgrade "sentry-sdk"
```

## 2. Initialize at start-up

Put this where your app starts — before your web framework is imported is best, so its integration is set up too.

```python title="app.py"
import os

import sentry_sdk

import re

SENSITIVE_KEY = re.compile(r"password|passwd|pwd|secret|api_key|apikey|api-key|access_token|auth|credentials|credit_card|card_number|cvv|private_key|privatekey|session|cookie|token|bearer|ssn|dob", re.I)
DROP_HEADER = re.compile(r"^(authorization|proxy-authorization|cookie|set-cookie|x-forwarded-for|x-real-ip|cf-connecting-ip|true-client-ip)$", re.I)
IN_TEXT = [
    (re.compile(r"\bBearer\s+[A-Za-z0-9._~+/-]+=*", re.I), "Bearer [Filtered]"),
    (re.compile(r"\beyJ[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{5,}"), "[Filtered]"),
    (re.compile(r"\b[sr]k_(live|test)_[A-Za-z0-9]{8,}\b"), "[Filtered]"),
    (re.compile(r"([?&#;\s\"']?[^=&#\s\"':]*(?:token|key|secret|password|session|auth)[^=&#\s\"':]*[\"']?\s*[=:]\s*[\"']?)[^&#\s\"',}]+", re.I), r"\1[Filtered]"),
]


def scrub_text(text):
    for pattern, repl in IN_TEXT:
        text = pattern.sub(repl, text)
    return text


def scrub_value(value, depth=0):
    if isinstance(value, str):
        return scrub_text(value)
    if depth > 8:
        return value
    if isinstance(value, (list, tuple)):
        return [scrub_value(v, depth + 1) for v in value]
    if isinstance(value, dict):
        return {k: "[Filtered]" if SENSITIVE_KEY.search(str(k)) else scrub_value(v, depth + 1) for k, v in value.items()}
    return value


def scrub_event(event, hint):
    request = event.get("request")
    if request:
        for field in ("data", "cookies", "env"):
            request.pop(field, None)
        headers = request.get("headers") or {}
        for name in list(headers):
            if DROP_HEADER.match(name):
                del headers[name]
            elif SENSITIVE_KEY.search(name):
                headers[name] = "[Filtered]"
        if isinstance(request.get("url"), str):
            request["url"] = scrub_text(request["url"])
        if request.get("query_string"):
            request["query_string"] = scrub_value(request["query_string"])
    user = event.get("user")
    if user is not None:
        event["user"] = {"id": user["id"]} if "id" in user else None
    if event.get("message"):
        event["message"] = scrub_text(event["message"])
    if event.get("logentry"):
        event["logentry"] = scrub_value(event["logentry"])
    for ex in (event.get("exception") or {}).get("values") or []:
        if ex.get("value"):
            ex["value"] = scrub_text(ex["value"])
    for field in ("extra", "contexts", "tags"):
        if event.get(field):
            event[field] = scrub_value(event[field])
    crumbs = event.get("breadcrumbs")
    if isinstance(crumbs, dict) and crumbs.get("values"):
        crumbs["values"] = [scrub_value(b) for b in crumbs["values"]]
    return event


sentry_sdk.init(
    dsn=os.environ.get("BELVADOR_DSN", "https://<public-key>@ingest.belvador.dev/<project-id>"),
    release=os.environ.get("APP_VERSION"),
    environment=os.environ.get("APP_ENV", "production"),
    send_default_pii=False,
    traces_sample_rate=0,
    max_breadcrumbs=50,
    max_value_length=8192,
    before_send=scrub_event,
)
```

| Option | Why |
|---|---|
| `dsn` | Reads `BELVADOR_DSN` from the environment, with the project DSN as fallback |
| `release`, `environment` | From `APP_VERSION` and `APP_ENV` (default `production`) |
| `send_default_pii=False` | No IP addresses, cookies or user details added by the SDK |
| `traces_sample_rate=0` | The Sentry SDK sends no spans; for traces use [OpenTelemetry](https://belvador.dev/en/docs/otel/python) |
| `max_breadcrumbs=50`, `max_value_length=8192` | Same bounds as Belvador's own SDKs |
| `before_send=scrub_event` | Removes request bodies, cookies, auth headers and client IP headers; keeps only `user.id`; masks values under sensitive names and credentials in text |

## Frameworks

`sentry-sdk` turns on integrations for frameworks it finds installed, such as Django, Flask and FastAPI, so unhandled errors in requests are reported. Framework-specific options are in [Sentry's Python guide](https://docs.sentry.io/platforms/python/). Belvador has not verified each framework.

## Report errors you handle

```python
import sentry_sdk

try:
    charge_card(order)
except PaymentError as error:
    sentry_sdk.capture_exception(error)
    raise
```

## Check that it works

```python
import sentry_sdk

sentry_sdk.capture_exception(Exception("Belvador Python test"))
sentry_sdk.flush(timeout=2)
```

Run it with the same `sentry_sdk.init(...)` as your app. The issue appears in the project within a few seconds.

## Limits to know

- The scrubbing hook is pattern-based. A secret under an unusual name, or inside a free-text message, can slip through.
- Keep `traces_sample_rate=0`; tracing data is not stored and is counted as **Not supported yet** in the project's intake numbers.
- Scheduled jobs and Celery beat tasks can report runs with `sentry_sdk.crons` — see [Monitors with the official Sentry SDKs](https://belvador.dev/en/docs/monitors/sentry-sdks).
