Home

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 bugtape

The 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 ERROR and above, from any logger,
  • anything you pass to bugtape.capture_exception() or bugtape.capture_message(),
  • uncaught exceptions in Streamlit pages, when Streamlit is imported.
try:
    run_export()
except Exception:
    bugtape.capture_exception()  # reports the exception being handled
    raise

Streamlit

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

SentNever sent
Exception type and a redacted messageFrame locals
File, function and line of each frameSource code lines
Your service, release and environmentArguments passed to a log call
The log call's template, e.g. export failed for %sOther 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 runningRequest bodies

Redaction replaces:

  • quoted values: KeyError: 'Acme Pty Ltd' becomes KeyError: '[redacted]'
  • numbers, amounts and percentages: margin 4.25% for acct 18233 becomes margin [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

OptionDefaultMeaning
api_keyBUGTAPE_KEYCapture key
serviceBUGTAPE_SERVICE, DD_SERVICE, appName of this app or job
releaseBUGTAPE_RELEASE, DD_VERSION, then your CI or host's commitVersion or commit (see Release)
environmentBUGTAPE_ENVIRONMENT, DD_ENVe.g. production
endpointhttps://app.bugtape.ai/v1/ingestIngest URL
redactTrueSet False only for data you know is safe
capture_uncaughtTrueHook sys.excepthook and threading.excepthook
capture_loggingTrueReport ERROR log records
logging_levellogging.ERRORLowest level reported
streamlitautoTrue to require the hook, False to skip it
send_messagesTrueFalse sends the exception type without its message, and log records without their text unless they use arguments
debugFalseLog 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()"