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,threadingandcontextvars. The SDK runs inside your process, so every dependency it added would be one you inherit.
Distribution note.
bucker-sdkis built and tested but not yet on PyPI. Its licence is decided (Apache-2.0); publishing is the remaining human step (seesdks/PUBLISHING.md). Until thenpip install bucker-sdkwill 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.
CookieandAuthorizationnever 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
429orX-Sentry-Rate-Limitsdrops the payload and counts it; it is never queued behind the limit. The counts come back as aclient_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 afterinit(); anos.register_at_forkhook starts a fresh worker thread in each child. SIGTERMis handled so a Fly/Kubernetes shutdown ends the session asexitedrather than leaving itok.
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.