Skip to content
bucker

bucker-sdk (Python)

First-party Python SDK: errors with source context, breadcrumbs, request context, session (release-health) tracking, trace linkage and opt-in GenAI call records.

  • Install: pip install bucker-sdk · Import: import bucker_sdk · Python 3.10+
  • Zero runtime dependencies. Only urllib, threading and contextvars. The SDK runs inside your process, so every dependency it added would be one you inherit.

Distribution note. bucker-sdk is built and tested but not yet on PyPI. Its licence is decided (Apache-2.0); publishing is the remaining human step (see sdks/PUBLISHING.md). Until then pip install bucker-sdk will not resolve, and the supported way to send Python errors to Bucker today is an unmodified Sentry SDK pointed at your DSN (switch from Sentry) or an OpenTelemetry exporter (OTLP). This page says "install" in the present tense because the package exists; it does not mean the registry has it.

Init

import bucker_sdk

bucker_sdk.init(
    dsn="https://<publicKey>@us.ingest.bucker.io/<projectId>",
    environment="production",
    release="api@1.4.0",
)

Call it once, early, in the process that serves requests. With no DSN at all the SDK is a silent no-op, so init() is safe to leave in a test suite or a local entrypoint.

A DSN that cannot be used disables the SDK; it never raises. An observability library must never be the reason an application fails to start. The client logs one warning on the bucker_sdk logger and sends nothing. There are three distinct cases, kept apart because the fix differs:

Case The warning says Fix
No DSN (silent) Set dsn= or BUCKER_DSN
Does not parse the configured DSN could not be parsed (<reason>) Re-copy it whole; see DSNs and regions
Still the documentation placeholder the DSN still contains the documentation placeholder for its public key Paste your project's DSN from Settings → Projects

The placeholder case matters because https://<publicKey>@us.ingest.bucker.io/<projectId> parses: accepted, it would enable the SDK and send every event to a project id of <projectId>, where nothing resolves. This is the same guard @bucker/browser and @bucker/node have.

Options

Option Default Meaning
dsn $BUCKER_DSN The project DSN. A ?query or #hash on a copied URL is accepted and never stored
environment $BUCKER_ENVIRONMENT, then production
release $BUCKER_RELEASE Your build's version or git SHA. Without it, no sessions are sent and release health is blank
server_name $BUCKER_SERVER_NAME Defaults to the hostname unless attach_server_name=False
distinct_id none Stable per-install id for crash-free users. Never a real user identifier
auto_session_tracking True Emit release-health session items
sample_rate 1.0 Fraction of error events to send; the rest are counted as dropped
max_breadcrumbs SDK default Breadcrumb ring size
max_value_length 8192 Longest string kept in an event (bounded, not pruned)
before_send none fn(event, hint) -> event or None; None drops the event
ignore_errors () Drop events whose message or exception (Type: value) matches a substring or a compiled re.Pattern; before sampling, counted as event_processor
before_breadcrumb none Same for breadcrumbs; None is the PII escape hatch
in_app_include / in_app_exclude () Package prefixes that are, or are not, "your code"
include_source_context True ±lines_of_context lines around each in-app frame, read from disk
lines_of_context 5
include_local_variables False Opt in. Captured as repr() strings and masked server-side
attach_server_name True
shutdown_timeout 2.0 Seconds the exit flush may take
http_timeout 10.0 Seconds one request may take
install_excepthook / install_atexit True Process-wide hooks; turn off in tests
debug False Log the SDK's own internal failures at debug level
transport built-in Supply your own wire call (a proxy); see Transport

Capturing

Function Notes
capture_exception(error=None) None means the exception currently being handled. Accepts an exception or an exc_info triple. Returns the event id
capture_message(message, level="info")
capture_event(event) A fully formed event dict
add_breadcrumb(message, category=…, level=…, data=…)
set_user(dict), set_tag(k, v), set_extra(k, v), set_context(k, v) Write to the current scope
flush(timeout), close(timeout) flush returns whether everything drained
try:
    risky()
except Exception:
    bucker_sdk.capture_exception()

Scopes

The current scope lives in a ContextVar, and the module-level setters are copy-on-write: they fork the scope they inherited. Two concurrent asyncio tasks or threads cannot see each other's tags without any push_scope() at the call site. isolation_scope() starts empty; push_scope() inherits; get_global_scope() is process-wide and merges into every event from every thread (put process-wide configuration there).

One caveat: mutating a Scope object you took out of get_current_scope() directly bypasses the fork. Use the setters, or push_scope() when you want a handle.

Web frameworks

# WSGI: Flask, Django (sync), Pyramid, Bottle
from bucker_sdk.wsgi import BuckerWsgiMiddleware
app = BuckerWsgiMiddleware(app)

# ASGI: FastAPI, Starlette, Django (async), Quart
from bucker_sdk.asgi import BuckerAsgiMiddleware
app = BuckerAsgiMiddleware(app)

Both run each request in an empty scope, attach the request (URL, method, query string and an allowlist of headers) and a transaction name, adopt an inbound traceparent, capture unhandled exceptions and re-raise them untouched. The WSGI middleware also wraps the response iterable, so a generator view that raises mid-stream is still captured.

  • Headers are allowlisted, not denylisted. Cookie and Authorization never leave the process.
  • The client IP is opt-in: BuckerWsgiMiddleware(app, include_ip=True).
  • URLs are redacted. In breadcrumbs every query and fragment value is replaced; the request's query string keeps ordinary parameters (page=2) and blanks only credential-named ones.

The two middlewares are the standards-level layer. Each major framework also has a one-line integration on top of them (below) that adds route-template names, a server span per request and the framework's own error path. Every framework is imported lazily by its own module, so import bucker_sdk never pulls one in, and none is a dependency of the SDK.

Django

INSTALLED_APPS = [..., "bucker_sdk.django"]

BUCKER_DSN = "https://<publicKey>@us.ingest.bucker.io/<projectId>"   # or $BUCKER_DSN
BUCKER = {"release": "web@1.4.0", "environment": "production"}       # optional client options

That is the whole install. The app config calls bucker_sdk.init(...) from those settings (unless your project already called it) and puts BuckerMiddleware first in MIDDLEWARE; set BUCKER = {"middleware": False} to place it yourself. The middleware is sync and async capable. A view's exception arrives through Django's process_exception hook (Django turns it into a 500 response inside each layer, so a try/except never sees it) and is captured once; Http404, PermissionDenied and SuspiciousOperation are control flow and are not captured. Spans and the transaction are named after the URL pattern (GET /items/<int:item_id>/), not the concrete path.

Flask

from bucker_sdk.flask import BuckerFlask
BuckerFlask(app)                 # or BuckerFlask().init_app(app)

Wraps wsgi_app in the WSGI middleware, captures through Flask's got_request_exception signal (Flask answers an unhandled error with a 500 itself, so the signal is the only place the exception is visible), and opens a server span named after the URL rule. HTTPException (abort(404)) is not captured.

FastAPI and Starlette

from bucker_sdk.fastapi import install
install(app)

Adds BuckerAsgiMiddleware with trace_requests=True: an unhandled exception is captured and re-raised (Starlette still answers 500), HTTPException and validation errors are handled further in and never reach it, and the transaction is the route template (GET /items/{item_id}).

Celery

from bucker_sdk.celery import install
install(app, monitor_beat_tasks=True)      # in the module that creates the Celery app

task_prerun / task_postrun give every task run its own scope and a queue.task.celery span; task_failure captures the exception (a Retry is a breadcrumb, never an event); a traceparent header is stamped on publish and adopted by the worker, so the request that enqueued a task and the task are one trace. With monitor_beat_tasks=True, each beat_schedule entry's task is bracketed by cron check-ins (in_progress, then ok or error) and the monitor is created from the schedule on first sight, so a beat task that stops arriving is a missed check-in rather than silence. Crontab and interval (>= 1 minute) schedules are translated; anything else (solar, custom schedule classes) is left unmonitored rather than guessed.

Cron check-ins and feedback

from bucker_sdk import with_monitor, capture_feedback

@with_monitor("nightly-export", monitor_config={
    "schedule": {"type": "crontab", "value": "0 2 * * *"},
    "checkin_margin": 5, "max_runtime": 30, "timezone": "UTC",
})
def nightly_export(): ...

with with_monitor("one-off"):   # also a context manager
    run()

capture_feedback("the page hung", email="a@b.test", associated_event_id=event_id)

with_monitor sends in_progress, then ok (or error if the block raises; the exception still propagates), sharing one check_in_id and reporting the duration. capture_check_in(slug, status, ...) is the lower-level call. Both ride the Sentry-shaped check_in / feedback envelope items the server ingests, on the bulk lane, so flush() before a job exits straight after its last check-in.

Logging

import logging
from bucker_sdk.logging_integration import install_logging

install_logging(event_level=logging.ERROR, breadcrumb_level=logging.INFO)

Records at or above event_level become events; records at or above breadcrumb_level become breadcrumbs, so the INFO lines before an ERROR travel with it. Records carry the active trace's trace_id and span_id. logging.exception(...) produces a full exception event. The SDK's own bucker_sdk logger is never forwarded.

Tracing and the span API

The SDK propagates W3C trace context: get_trace_context() returns the active TraceContext, use_trace_context(ctx) activates one, and the middlewares adopt an inbound traceparent. Errors and log records raised inside a context link to its trace.

Spans for your own code:

import bucker_sdk

with bucker_sdk.start_span("db.query", op="db", attributes={"db.system": "postgresql"}) as span:
    rows = run_query()
    span.set_attribute("db.rows", len(rows))

@bucker_sdk.traced(op="task")          # sync functions and coroutines
def reindex(): ...

async with bucker_sdk.start_span("fetch"):
    ...

Spans nest through the same ContextVar as the trace context (so they survive await), and are sent as the span envelope item the Node SDK and the GenAI hook already use. An exception inside a span marks it error with the exception TYPE only (messages carry user data) and propagates. finish() is idempotent. A span whose parent is unsampled is non-recording but still carries real ids, so a downstream traceparent keeps saying sampled=0. If you already run OpenTelemetry, keep using it and export over OTLP (OTLP) instead; the two are not meant to be mixed in one process.

GenAI call records (opt-in)

from bucker_sdk import genai

with genai.llm_call(provider="anthropic", model="claude-opus-4") as call:
    response = client.messages.create(...)
    call.set_usage(input_tokens=response.usage.input_tokens,
                   output_tokens=response.usage.output_tokens)

or genai.record_llm_call(provider=…, input_tokens=…, …). It imports no provider SDK and patches nothing; it emits a span item with gen_ai.* attributes. See agentic observability for what Bucker does with them.

GenAI auto-instrumentation

from bucker_sdk.integrations import genai
genai.install()                              # everything importable
genai.install(["openai", "langchain"])       # or a chosen subset
Name Covers
openai chat.completions, responses, embeddings; sync, async and streaming
anthropic messages.create; sync, async and streaming
langchain LangChain and LangGraph (they share the callback system), installed globally with register_configure_hook; one invoke_agent turn per top-level run
litellm completion / acompletion / embedding, through LiteLLM's callback lists
openai_agents the OpenAI Agents SDK, through add_trace_processor
claude_agent the Claude Agent SDK query() stream

A library that is not installed reports False; an unknown name raises ValueError. Libraries are imported lazily and none is a dependency. Streams are observed in place (the SDK's own stream object is returned), the host's exceptions are recorded by type and re-raised untouched, and a recording failure is a debug line, never an exception in your request. Prompts and completions are not recorded; usage, model, finish reason and the first tool call are. Pydantic AI is not patched because it emits OpenTelemetry GenAI spans itself (Agent.instrument_all()): point its OTLP exporter at Bucker (OTLP).

Transport

Two priority lanes on a daemon worker thread: critical (errors, always drained first, including at shutdown) and bulk (spans, logs, client reports).

  • A 429 or X-Sentry-Rate-Limits drops the payload and counts it; it is never queued behind the limit. The counts come back as a client_report.
  • The retry buffer is bounded in bytes and in items; overflow evicts bulk first.
  • The exit flush is bounded by shutdown_timeout; a hung ingest endpoint costs a fixed delay, never a hung interpreter.
  • Bodies over 1 KiB are gzipped.
  • Fork-safe. gunicorn --preload, uWSGI and Celery prefork fork after init(); an os.register_at_fork hook starts a fresh worker thread in each child.
  • SIGTERM is handled so a Fly/Kubernetes shutdown ends the session as exited rather than leaving it ok.
from bucker_sdk.transport import HttpTransport
from bucker_sdk.clientreport import DropAccumulator

drops = DropAccumulator()
bucker_sdk.init(dsn=DSN, transport=HttpTransport(my_sender, drops), drops=drops)

Pass the same DropAccumulator to both so client-side and transport-side drops land in one client report.

What an event contains

exception.values[] with stacktrace.frames[] (oldest first, raising frame last, each with abs_path, filename, function, lineno, module, in_app), breadcrumbs.values[], tags, user, request, contexts, release, environment and platform: "python". Exception chains (raise … from, implicit __context__) become additional values, root cause first. in_app is decided from real sysconfig stdlib and site-packages paths first, with in_app_include winning over every exclusion. The ≤500-token essence is built server-side, so the SDK sends everything bounded in size and the projection improves without an SDK release.

Not in this SDK

  • No automatic span creation or database/HTTP client instrumentation. Use OpenTelemetry.
  • No source-map or symbol upload (not needed: Python frames carry their own source context).
  • Frameworks other than Django, Flask, FastAPI/Starlette and Celery (Pyramid, Tornado, RQ, Dramatiq, ...) use the WSGI/ASGI middleware, or capture_exception() where you catch a background failure.
  • Replay, profiling and attachments are not produced.

Compatibility and versioning

The Python SDK follows the same release policy as every Bucker SDK (sdks/PUBLISHING.md, "Release discipline"): a deprecation warns one minor version before removal, and no default widens data collection without a major version and an explicit opt-in. Its envelope serialization is byte-compatible with the TypeScript implementation, enforced by a fixture generated from the real serializer.