# Tracing — Sentry Python SDK

> Minimum SDK: `sentry-sdk` 0.11.2+ for basic tracing; 2.x for `update_current_span()` and enhanced `@trace`

## Configuration

| Option | Type | Default | Purpose |
|--------|------|---------|---------|
| `traces_sample_rate` | `float` | `None` | Fraction of transactions to trace (0.0–1.0); `None` disables tracing |
| `traces_sampler` | `callable` | `None` | Per-transaction sampling function; overrides `traces_sample_rate` |
| `trace_propagation_targets` | `list` | all origins | URLs to inject `sentry-trace` / `baggage` headers into |
| `functions_to_trace` | `list` | `[]` | Fully-qualified function names to auto-wrap with spans |
| `instrumenter` | `str` | `"sentry"` | Set to `"otel"` to use OpenTelemetry bridge |

## Code Examples

### Enable tracing

```python
import sentry_sdk

sentry_sdk.init(
    dsn="https://<key>@<org>.ingest.sentry.io/<project>",
    traces_sample_rate=1.0,   # 1.0 = 100% of transactions; reduce in production
)
```

### Custom spans with context manager

```python
import sentry_sdk

# Top-level transaction
with sentry_sdk.start_transaction(op="task", name="Process Order"):
    with sentry_sdk.start_span(name="Validate Items") as span:
        span.set_data("item_count", len(items))
        validate(items)

    with sentry_sdk.start_span(name="Charge Card"):
        charge_card(order)
```

### `@sentry_sdk.trace` decorator

```python
import sentry_sdk

# Simple decorator — uses function name as span name
@sentry_sdk.trace
def process_payment(order_id: str):
    ...

# With custom op, name, and attributes (SDK 2.35.0+)
@sentry_sdk.trace(op="payment", name="Charge Customer", attributes={"version": 2})
def charge_customer(amount: int):
    ...

# Static / class methods — decorator order matters!
class PaymentService:
    @staticmethod
    @sentry_sdk.trace          # MUST come AFTER @staticmethod
    def process():
        ...

    @classmethod
    @sentry_sdk.trace          # MUST come AFTER @classmethod
    def batch_process(cls):
        ...
```

### Adding data to spans

```python
import sentry_sdk

with sentry_sdk.start_span(name="db-query") as span:
    span.set_data("db.system", "postgresql")
    span.set_data("db.table", "orders")
    span.set_data("row_count", 42)
    span.set_data("cache_hit", False)
    # Only primitive types: str, int, float, bool, or homogeneous lists
```

### Accessing the active span/transaction

```python
import sentry_sdk

# Get active span
span = sentry_sdk.get_current_span()
if span:
    span.set_data("key", "value")

# Get active transaction
txn = sentry_sdk.get_current_scope().transaction
if txn:
    txn.set_tag("order.type", "subscription")

# Update active span in-place (SDK 2.x)
sentry_sdk.update_current_span(
    op="http.client",
    name="POST /api/charge",
    attributes={"http.status_code": 200},
)
```

### Auto-trace multiple functions without decorators

```python
import sentry_sdk

sentry_sdk.init(
    dsn="...",
    traces_sample_rate=1.0,
    functions_to_trace=[
        {"qualified_name": "myapp.billing.processor.charge_card"},
        {"qualified_name": "myapp.billing.processor.BillingService.validate"},
    ],
)
# No changes to the functions themselves required
```

### Dynamic sampling with `traces_sampler`

```python
import sentry_sdk

def traces_sampler(sampling_context):
    # Honour parent's sampling decision in distributed traces
    parent = sampling_context.get("parent_sampled")
    if parent is not None:
        return float(parent)

    op = sampling_context["transaction_context"].get("op", "")

    if op == "http.server":
        name = sampling_context["transaction_context"].get("name", "")
        if name in ("/health", "/ping", "/readyz"):
            return 0          # drop health checks
        return 0.5            # sample 50% of HTTP requests
    elif op == "task":
        return 0.1            # sample 10% of background tasks
    else:
        return 0.01           # default 1%

sentry_sdk.init(dsn="...", traces_sampler=traces_sampler)
```

### Auto-instrumented frameworks

| Framework | Auto-enabled | What is traced |
|-----------|-------------|----------------|
| Django | ✅ | Requests, DB queries (ORM), cache, signals |
| Flask | ✅ | Requests, Jinja2 rendering |
| FastAPI / Starlette | ✅ | Requests, background tasks |
| Celery | ✅ | Task execution, queue operations |
| SQLAlchemy | ✅ | All queries as spans + breadcrumbs |
| Redis | ✅ | All commands as spans + breadcrumbs |
| PyMongo | ✅ | All queries (covers mongoengine, Motor) |
| requests / httpx | ✅ | Outbound HTTP calls with trace propagation |

### Database auto-instrumentation

```python
import sentry_sdk
from sentry_sdk.integrations.redis import RedisIntegration

# SQLAlchemy — auto-enabled, no config needed
sentry_sdk.init(dsn="...", traces_sample_rate=1.0)
# All queries appear as spans automatically

# Redis — with optional cache monitoring
sentry_sdk.init(
    dsn="...",
    traces_sample_rate=1.0,
    send_default_pii=True,       # enables full command data in spans
    integrations=[
        RedisIntegration(
            max_data_size=1024,                         # truncate large values
            cache_prefixes=["mycache:", "template."],   # appears in cache dashboard
        ),
    ],
)
```

### Distributed tracing

Frameworks propagate `sentry-trace` and `baggage` headers automatically. For manual HTTP calls:

```python
import sentry_sdk

# Outgoing: inject headers into arbitrary request
headers = {}
headers["sentry-trace"] = sentry_sdk.get_traceparent()
headers["baggage"] = sentry_sdk.get_baggage()
make_request(url="https://internal-api.example.com", headers=headers)

# Incoming: continue a trace from arbitrary headers
incoming_headers = get_headers_from_request()
transaction = sentry_sdk.continue_trace(incoming_headers)  # does NOT start it
with sentry_sdk.start_transaction(transaction):
    handle_request()

# Browser: inject into HTML meta tags for JS frontend pickup
meta = f'<meta name="sentry-trace" content="{sentry_sdk.get_traceparent()}">'
meta += f'<meta name="baggage" content="{sentry_sdk.get_baggage()}">'
```

### Limit propagation targets

```python
import sentry_sdk, re

sentry_sdk.init(
    dsn="...",
    traces_sample_rate=1.0,
    trace_propagation_targets=[
        "localhost",
        "https://api.myservice.com",
        re.compile(r"^https://internal\."),
    ],
    # Set to [] to disable outgoing propagation entirely
)
```

### OpenTelemetry — OTLP Integration (recommended)

> Minimum SDK: `sentry-sdk` 2.x with `sentry-sdk[opentelemetry-otlp]`

If the project already uses OpenTelemetry for tracing, **use the OTLP integration instead of Sentry's native tracing**. Sentry ingests OTel spans directly via its OTLP endpoint — no span conversion, no dual instrumentation.

**When to use this path:** OTel packages (`opentelemetry-sdk`, `opentelemetry-distro`, `opentelemetry-instrumentation-*`) detected in requirements, or `TracerProvider` / `start_as_current_span` found in source.

**When NOT to use this path:** No OpenTelemetry in the project — use Sentry's native `traces_sample_rate` instead.

#### Setup

```bash
pip install "sentry-sdk[opentelemetry-otlp]"
```

```python
import sentry_sdk
from sentry_sdk.integrations.otlp import OTLPIntegration

sentry_sdk.init(
    dsn=os.environ["SENTRY_DSN"],
    send_default_pii=True,
    enable_logs=True,
    integrations=[
        OTLPIntegration(),
        # OTLPIntegration(collector_url="http://localhost:4318/v1/traces"),  # set if sending spans to an OTel Collector
    ],
    # Do NOT set traces_sample_rate — tracing is handled by OTel
    # Errors, Logs, Crons, and Metrics are still captured natively by Sentry
    # and automatically linked to the active OTel trace
)
```

Configure your OTel instrumentations separately (keep your existing OTel setup):

```python
from opentelemetry.instrumentation.auto_instrumentation import sitecustomize
# or configure manually:
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider

provider = TracerProvider()
trace.set_tracer_provider(provider)
# Sentry auto-adds its OTLP exporter and propagator —
# no manual SpanProcessor or Propagator wiring needed
```

#### How it works

- Sentry derives an OTLP endpoint from your DSN and registers a `SpanExporter` with the existing `TracerProvider` — your other exporters (Jaeger, Zipkin, Collector) continue working
- A propagator injects `sentry-trace` + `baggage` headers for distributed tracing with other Sentry-instrumented services
- Errors captured via `sentry_sdk.capture_exception()` are automatically linked to the active OTel trace/span via shared `trace_id`

#### OTLP configuration options

| Option | Default | Purpose |
|--------|---------|---------|
| `setup_otlp_traces_exporter` | `True` | Auto-configure exporter; set `False` if you send to your own Collector |
| `collector_url` | `None` | OTLP HTTP endpoint of an OTel Collector (e.g., `http://localhost:4318/v1/traces`); when set, spans are sent to the collector instead of directly to Sentry |
| `setup_propagator` | `True` | Auto-configure propagator; set `False` if you manage propagation yourself |
| `capture_exceptions` | `False` | Intercept exceptions recorded via OTel `Span.record_exception` |

#### Important

**Do not combine OTLP with native Sentry tracing.** When using `OTLPIntegration`:
- Do **not** set `traces_sample_rate` or `traces_sampler`
- Do **not** set `instrumenter="otel"` (that is the legacy SpanProcessor approach)
- Do **not** use `sentry_sdk.start_transaction()` or `sentry_sdk.start_span()` — use OTel's `tracer.start_as_current_span()` instead

### OpenTelemetry bridge (legacy)

> **Deprecated:** Prefer the OTLP integration above. The SpanProcessor bridge below is the older approach.

```python
# pip install "sentry-sdk[opentelemetry]"
import sentry_sdk
from opentelemetry import trace
from opentelemetry.propagate import set_global_textmap
from opentelemetry.sdk.trace import TracerProvider
from sentry_sdk.integrations.opentelemetry import SentrySpanProcessor, SentryPropagator

sentry_sdk.init(
    dsn="...",
    traces_sample_rate=1.0,
    instrumenter="otel",      # disables built-in Sentry auto-instrumentation
)

provider = TracerProvider()
provider.add_span_processor(SentrySpanProcessor())
trace.set_tracer_provider(provider)
set_global_textmap(SentryPropagator())

# Use OTel API as normal — spans appear in Sentry
tracer = trace.get_tracer(__name__)
with tracer.start_as_current_span("my-operation"):
    do_work()
```

## Best Practices

- Use `traces_sampler` instead of `traces_sample_rate` for production — it lets you drop health checks, adjust by route, and honour distributed trace decisions
- Always finish spans — unfinished spans are silently dropped
- Use `span.set_data()` not `span.set_tag()` for span-level data (tags are for scope-level filtering)
- Set `trace_propagation_targets` to avoid leaking `sentry-trace` headers to third-party services
- Add `sentry-trace` and `baggage` to your CORS allowlist when tracing browser-to-backend flows

## Troubleshooting

| Issue | Solution |
|-------|----------|
| No transactions appearing | Verify `traces_sample_rate > 0` or `traces_sampler` returns non-zero |
| Spans not linked to transaction | Ensure spans are created inside an active `start_transaction()` context |
| Distributed traces broken | Check that `sentry-trace` and `baggage` headers pass through proxies/gateways |
| `description=` deprecation warning | Replace `description=` with `name=` in `start_span()` calls |
| OTel spans not appearing (OTLP) | Verify `sentry-sdk[opentelemetry-otlp]` is installed; do **not** set `traces_sample_rate` when using `OTLPIntegration` |
| OTel spans not appearing (legacy bridge) | Ensure `SentrySpanProcessor` is added to `TracerProvider` before creating spans |
| `@sentry_sdk.trace` on static method fails | Put `@sentry_sdk.trace` AFTER `@staticmethod` / `@classmethod` |
