BugTape docs
Python
Server errors, logging and Streamlit, with messages redacted.
Report errors from Python servers, jobs and Streamlit apps. Frame locals are never read, and messages are redacted by pattern before they leave your process.
Install
pip install bugtapeThe package has no dependencies and supports Python 3.9 and later. If your network cannot reach PyPI, install the same reviewed wheel from BugTape: pip install https://app.bugtape.ai/sdk/python/bugtape-0.1.1-py3-none-any.whl.
Start
import bugtape
bugtape.init(service="reports", release="2026.09.28")The capture key is read from the BUGTAPE_KEY environment variable. Use a key from Setup → Python in the console (bt_live_… or bt_test_…). Without a key, init logs one warning and reporting stays off.
From then on BugTape reports:
- uncaught exceptions in the main thread and in threads,
- log records at
ERRORand above, from any logger, - anything you pass to
bugtape.capture_exception()orbugtape.capture_message(), - uncaught exceptions in Streamlit pages, when Streamlit is imported.
try:
run_export()
except Exception:
bugtape.capture_exception() # reports the exception being handled
raiseStreamlit
import streamlit as st
import bugtape
bugtape.init(service="analytics")
st.title("Revenue")Streamlit runs the page again on every interaction. Calling init at the top of the page is fine: a call with the same settings keeps the running client.
Streamlit catches page errors itself and shows them in the app, so they never reach Python's normal error hook. BugTape wraps the function Streamlit calls for those errors. The app still shows the error exactly as before, and BugTape files one report with the page name and the Streamlit session.
Streamlit also logs the same error. BugTape recognises it and does not report it twice.
If a future Streamlit release changes that function, init(streamlit=True) logs a warning. You can then wrap page code yourself:
from bugtape.integrations.streamlit import capture
with capture():
render_page()What leaves your server
| Sent | Never sent |
|---|---|
| Exception type and a redacted message | Frame locals |
| File, function and line of each frame | Source code lines |
Your service, release and environment | Arguments passed to a log call |
The log call's template, e.g. export failed for %s | Other environment variables (only the settings below, DD_SERVICE, DD_ENV, DD_VERSION and, to fill release, the commit variables under Release are read) |
Datadog trace and span ids, when ddtrace is running | Request bodies |
Redaction replaces:
- quoted values:
KeyError: 'Acme Pty Ltd'becomesKeyError: '[redacted]' - numbers, amounts and percentages:
margin 4.25% for acct 18233becomesmargin [num]% for acct [num] - SQL statements, email addresses, and URLs with credentials
- cloud storage paths, data file names, and ids
- values of
…_id=,name=,token=,password=and similar pairs
Python type names such as 'int' and 'DataFrame' are kept because they carry no data.
Values you pass in extra are redacted the same way. A value under a key such as client, customer, name, account or amount is replaced whole, and numbers become [num].
What redaction does not catch
Redaction works by pattern. A value written into a message without quotes or digits is not recognised: ValueError(f"no rate for client {name}") is sent as no rate for client Acme Pty Ltd. The same goes for a log call written as an f-string, because BugTape only sees the finished text. For code like that, pick one:
- pass values as log arguments (
log.error("no rate for client %s", name)), which are never sent, - add a pattern with
extra_redactions, - check or drop reports with
before_send, - or set
send_messages=False, which sends the exception type and location without the message.
Add your own patterns, or check every report before it is sent:
bugtape.init(
service="reports",
extra_redactions=[(r"\bACME-\d+\b", "[account]")],
before_send=lambda report: None if "healthcheck" in report["title"] else report,
)before_send receives the full JSON payload. Return it (changed or not) to send it, or None to drop it.
Grouping
Repeats of the same error group into one issue. The key is the exception type, the redacted message and the code location. The code location is the innermost frame in your own code, not in a library, and it is sent as a stable address such as python://reports/jobs/export/run_export. Redaction removes the changing values, so a timeout for account 18233 and one for account 20411 are the same issue.
Datadog
If your app already runs ddtrace, each report carries the active trace and span ids. Set your Datadog site in Settings → Integrations → Datadog, and the issue in BugTape links straight to the trace. BugTape never imports or starts Datadog itself. See Use BugTape with Datadog.
Options
| Option | Default | Meaning |
|---|---|---|
api_key | BUGTAPE_KEY | Capture key |
service | BUGTAPE_SERVICE, DD_SERVICE, app | Name of this app or job |
release | BUGTAPE_RELEASE, DD_VERSION, then your CI or host's commit | Version or commit (see Release) |
environment | BUGTAPE_ENVIRONMENT, DD_ENV | e.g. production |
endpoint | https://app.bugtape.ai/v1/ingest | Ingest URL |
redact | True | Set False only for data you know is safe |
capture_uncaught | True | Hook sys.excepthook and threading.excepthook |
capture_logging | True | Report ERROR log records |
logging_level | logging.ERROR | Lowest level reported |
streamlit | auto | True to require the hook, False to skip it |
send_messages | True | False sends the exception type without its message, and log records without their text unless they use arguments |
debug | False | Log BugTape's own delivery problems |
Delivery
Reports go out from a background thread with a 5 second timeout. BugTape retries once on a network error, a 5xx or a 429, using the same capture id so the server does not count it twice. At most 30 reports a minute are sent. BugTape never raises into your app.
Call bugtape.flush() at the end of a short script so queued reports are sent before the process exits. Uncaught exceptions flush on their own. It returns True when every report since the last flush was accepted, and False on timeout or when one was rejected or could not be sent (debug=True logs why).
Release
Every report should carry a release, so BugTape can tell a fixed issue from a regression. Without one it records release_missing. When you pass no release and set neither BUGTAPE_RELEASE nor DD_VERSION, the SDK uses the first of these that is set: GITHUB_SHA, VERCEL_GIT_COMMIT_SHA, CF_PAGES_COMMIT_SHA, COMMIT_REF, CI_COMMIT_SHA, CIRCLE_SHA1, GIT_COMMIT, TRAVIS_COMMIT, BUILD_VCS_NUMBER, RENDER_GIT_COMMIT, RAILWAY_GIT_COMMIT_SHA, HEROKU_SLUG_COMMIT. The same CI list is used by the browser SDK; the last three are hosts that set the commit at runtime. The list is bugtape.COMMIT_ENV_KEYS.
Check it
Raise a test error, then open Issues in the console:
BUGTAPE_KEY=bt_test_… python -c "import bugtape; bugtape.init(service='check'); bugtape.capture_message('BugTape Python check'); bugtape.flush()"