Files
Evan Purkhiser 64dfeaa1f1 ref(skills): move superseded skills to skills-legacy (#265)
These per-SDK skills, plus a handful of setup/workflow skills, are superseded
by the sentry-instrument skill and the references/sdks/ trees. Move them under
skills-legacy/ so they are excluded from the plugin build and skipped by the
drift automation, while skills.sentry.dev can still serve them as a fallback
for existing URL consumers.

Moves 26 skills: the 19 per-SDK skills plus sentry-sdk-skill-creator,
sentry-fix-issues, the three sentry-span-streaming-* skills,
sentry-instrument-logging, and sentry-instrumentation-guide. Regenerates
SKILL_TREE.md and repaths the moved entries in CODEOWNERS.

sentry-sdk is intentionally left in place; its removal is handled in a
separate PR.
2026-07-10 11:51:09 -04:00

12 KiB
Raw Permalink Blame History

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

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

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

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

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

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

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

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
Pyramid ✅ Requests, route matching
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 / httpx2 ✅ Outbound HTTP calls with trace propagation

Database auto-instrumentation

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:

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

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
)

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

pip install "sentry-sdk[opentelemetry-otlp]"
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):

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.

# 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