Skip to main content

devalerts

PyPI PyPI Downloads Python versions License: MIT Tests Ruff mypy

Русская версия

Send unhandled Python exceptions straight to a Telegram chat (or Slack) — the moment they happen, on your phone. No backend, no account, no database — just your own bot token or webhook URL.

import devalerts

devalerts.init(bot_token="123456:ABC-DEF...", chat_id=123456789)

That's the whole setup. Two minutes with @BotFather and every unhandled crash — including ones raised in threads — lands in your chat instead of a log file nobody's watching.

devalerts demo

Why devalerts

  • Zero infrastructure. No SaaS signup, no ingestion server, no API key to manage beyond your own Telegram bot token. State lives in a local SQLite file you already own.
  • One line to install, one line to wire up. init() installs the hook and gets out of the way.
  • Not spam. Errors are grouped by fingerprint and rate-limited per group, so a crash loop sends one message, not a thousand.
  • Framework-aware. Ships an ASGI middleware for FastAPI/Starlette apps, where the default excepthook would never even see a request error.
  • Small and typed. No dependencies, ships py.typed, ~450 lines total — short enough to read in one sitting before you trust it with your errors.

Install

uv add devalerts

(or pip install devalerts if you're not using uv)

Usage

  1. Create a bot with @BotFather and get its token.

  2. Message your bot once (or add it to a group) so it's allowed to message you back.

  3. Get your chat id — message @userinfobot, or call https://api.telegram.org/bot<TOKEN>/getUpdates after step 2 and read message.chat.id.

  4. Verify it's wired up correctly before touching any code:

    uv run devalerts test --bot-token 123456:ABC-DEF... --chat-id 123456789
    
  5. In your app, as early as possible:

import devalerts

devalerts.init(bot_token="123456:ABC-DEF...", chat_id=123456789)

That's it — any unhandled exception (including ones raised in threads) now also lands in your Telegram chat.

The traceback itself arrives folded into a collapsed quote — the exception type, message, and host are visible right away, tap to expand the full traceback. Keeps a big stack trace from taking over the chat.

Grouping, rate limiting, and the dashboard

Exceptions are grouped by fingerprint (exception type + file + line where it was raised) in a local SQLite file (~/.devalerts/state.db). Each group sends at most one Telegram message per rate_limit_seconds (default 300); repeats inside that window are counted but not sent, and the next message for that group says how many were skipped. Old groups (untouched for 7 days) are pruned automatically. Configure the window via init():

devalerts.init(bot_token="...", chat_id=123456789, rate_limit_seconds=60)

See what's grouped and what's currently rate-limited:

uv run devalerts dashboard

devalerts dashboard output

Pass --json for machine-readable output. Silence a noisy group without touching code — the ID column accepts any unique prefix:

uv run devalerts mute abc12345
uv run devalerts unmute abc12345
uv run devalerts clear abc12345   # or: devalerts clear --all

Unmuting resends the next occurrence with the accumulated skip count, same as a rate-limit window expiring.

Error groups that keep piling up while suppressed are chronic: each such resend doubles the effective rate_limit_seconds for that group (capped at 8x), so a crash loop backs itself off instead of paging you every window forever. A group that goes quiet and reappears once resets to the base rate immediately. The dashboard shows the active multiplier (● sending ×4).

Crash streak badge

devalerts dashboard also reports how long it's been since the last recorded incident (sent or suppressed by rate-limiting) — same idea as the classic "days since last workplace accident" sign, applied to your crashes:

🟢 14 days since the last incident.

Get the same number as a shareable SVG badge for your README:

uv run devalerts badge --out docs/streak.svg

devalerts crash streak badge

No hosting, no cron job built in — regenerate and commit it yourself, e.g. on a schedule with GitHub Actions:

- run: uv run devalerts badge --out docs/streak.svg
- run: git add docs/streak.svg && git commit -m "Update crash streak badge" || true

Pass --label to customize the left-hand text (default "crash streak").

Context: hostname and tags

Every alert automatically includes the sending host, so you can tell which process/server it came from when one bot serves several:

devalerts.init(bot_token="...", chat_id=123456789, tags={"env": "production"})
🔴 ValueError: boom
🖥️ prod-web-2 (env=production)

Add ad-hoc tags to a single call — they override init()'s tags (and each other) on a key collision:

devalerts.report(extra={"request_id": "abc123"})

Used as a decorator, capture() tags the alert with the wrapped function's name as job automatically — no extra needed:

@devalerts.capture()
def nightly_sync(): ...
🔴 ValueError: boom
🖥️ prod-web-2 (job=nightly_sync)

Pass extra={"job": "..."} explicitly to override the auto-detected name.

Manually reporting a caught exception

try:
    risky_call()
except Exception:
    devalerts.report()  # sends the currently-handled exception

or:

with devalerts.capture():
    risky_call()  # reports on exception, then re-raises

capture also works as a decorator, so you don't need to touch a function's body at all:

@devalerts.capture()
def risky_call():
    ...

FastAPI / Starlette / any ASGI app

init()'s excepthook won't see request errors — the framework already catches them internally to return a 500 response, so nothing "unhandled" ever reaches the process. Use the ASGI middleware instead:

app.add_middleware(devalerts.ASGIMiddleware)

Only exceptions that actually escape as server errors get reported — routing 404s and raised HTTPExceptions are already turned into responses by the framework before the middleware sees them.

Celery

Same problem as ASGI apps: init()'s excepthook never sees exceptions raised inside a task, because Celery catches them itself to record the task's FAILURE state. Call init_celery() in addition to init():

devalerts.init(bot_token="...", chat_id=123456789)
devalerts.init_celery()

This connects to Celery's task_failure signal (fired once a task has genuinely failed — retries exhausted or none configured, so retried tasks don't spam an alert per attempt) and reports through the same grouping/rate-limiting/redaction path as everything else, tagged with the task name and id automatically. Requires Celery to already be installed in the worker process — it's imported lazily, not a devalerts dependency.

Logged (not raised) exceptions

init()'s excepthook only ever sees exceptions that stay unhandled — but most real errors are caught and logged instead: logger.exception(...) inside a try/except never propagates anywhere. Attach LogHandler to catch those too:

import logging

logging.getLogger().addHandler(devalerts.LogHandler())

A record with exc_info (logger.exception(), or logger.error(..., exc_info=True)) is reported like a full exception alert — grouped by the same fingerprint an unhandled instance of it would use, so logging it and then re-raising sends one alert, not two. A plain logger.error("message") with no exception attached is reported as a short text alert, grouped by logger name + level + message. Defaults to level=logging.ERROR; pass extra={...} for tags added to every alert from this handler.

Blame

Curious who wrote the line that just blew up? blame=True runs git blame on it and adds the author, short commit hash, and date to the alert:

devalerts.init(bot_token="...", chat_id=123456789, blame=True)
🕵️ blame: sslinNn · a1b2c3d · 2026-07-15 (3d ago)

Best-effort and silent about it — no git installed, no repo (a container image without .git), or the line isn't committed yet all just skip the line instead of failing the alert. Off by default; opt in with blame=True.

Slack

Prefer Slack, or want both? Pass a Slack incoming webhook URL alongside or instead of bot_token/chat_id — every configured channel gets every alert:

devalerts.init(slack_webhook_url="https://hooks.slack.com/services/...")

Create one at api.slack.com/apps → your app → Incoming Webhooks. Same grouping/rate-limiting/redaction/blame path as Telegram, formatted as Slack mrkdwn instead of HTML. Verify it's wired up:

uv run devalerts test --slack-webhook-url https://hooks.slack.com/services/...

Why not Sentry?

If you already run Sentry/Rollbar/etc., keep using it — devalerts isn't a replacement. It's for the side project, internal tool, or small service that doesn't have (and doesn't want) that infrastructure: no account to create, no SDK to configure, no server to trust — just a bot token you already control.

devalerts Sentry-style tracker
Setup one bot token account + project + SDK config
Backend none — Telegram only hosted or self-hosted service
Where alerts land your Telegram chat a web dashboard
Grouping / rate limiting yes, local SQLite yes, server-side
Search, trends, releases no yes

FAQ

Works with FastAPI / Starlette? Yes — use ASGIMiddleware, since the default excepthook never sees request errors.

Works with Docker? Yes, nothing container-specific. Just make sure ~/.devalerts/ (the dedup state file) is either writable inside the container or a mounted volume if you want dedup state to survive restarts — it self-recreates otherwise.

Works with threads? Yes — init() installs both sys.excepthook and threading.excepthook.

Works with Celery / background workers? Yes — call init_celery() in addition to init() to catch exceptions raised inside tasks, which the excepthook alone won't see.

Catches exceptions I log but don't re-raise? Yes — add LogHandler to a logger; the excepthook alone never sees a caught-and-logged exception.

Works on Windows / Linux / macOS? Yes — stdlib only (urllib, sqlite3, threading), no OS-specific code paths.

Privacy & Security

  • The only network calls devalerts makes are to api.telegram.org and/or your configured Slack incoming webhook — no telemetry, no analytics, nothing else phones home.
  • No third-party server and no devalerts-run backend — messages go straight from your process to your own Telegram bot / Slack webhook.
  • No accounts, no signup, no API key beyond the bot token / webhook URL you create and control yourself.
  • Basic secret redaction only (a few common token/key patterns) — do not rely on this for sensitive production data; scrub what you can before it ever reaches an exception message.
  • If delivery fails after retrying, the alert (already redacted, if redact=True) is appended to ~/.devalerts/failed.log instead of being dropped — clean it up like any other local log file.

What this does NOT do (by design)

  • Grouping/rate limiting is local and in-process only (SQLite file, no server) — the dashboard is a CLI table, not a web UI.
  • No backend, no accounts — each user runs their own bot.

Roadmap

  • Web dashboard (hosted, optional — the local CLI dashboard stays either way)
  • Discord delivery
  • Email delivery

Development

uv sync --group dev
uv run pre-commit install

pre-commit runs ruff check, ruff format, and mypy before each commit. Run the full check manually with:

uv run pytest
uv run pre-commit run --all-files

License

MIT — see LICENSE.

Metadata

Release files for devalerts 0.4.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for devalerts 0.4.0
File Size Uploaded
devalerts-0.4.0.tar.gz 20.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for devalerts 0.4.0
File Interpreter ABI Platform
devalerts-0.4.0-py3-none-any.whl Python 3 none any Details

Total release size: 46.3 kB

Release files / devalerts-0.4.0.tar.gz

Download URL devalerts-0.4.0.tar.gz
Size 20.8 kB
Tags Source
SHA-256 checksum
How to use checksums
edc7a85930709a4d12788b299ac1ffafa2babb066b8260e1582e67fbfe2dd955
BLAKE2b-256 checksum
How to use checksums
f272a60addf328c623afceb3a50382e99e41aab83604c02111f8df1f3187aa03
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.11.29 {"installer":{"name":"uv","version":"0.11.29","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / devalerts-0.4.0-py3-none-any.whl

Download URL devalerts-0.4.0-py3-none-any.whl
Size 25.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
25e206d83a6100bbabece1dff42ff7d8ffe48e82b11e12329db10c3a6cba4603
BLAKE2b-256 checksum
How to use checksums
48914a9bf9e6914cd8052f3fcf8f477ef5f5268abb61377cc92812418b053347
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.11.29 {"installer":{"name":"uv","version":"0.11.29","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release history Release notifications | RSS feed

This release

0.4.0 This release

2 release files

0.3.0

2 release files

0.2.3

2 release files

0.2.2

2 release files

0.2.1

2 release files

0.2.0

2 release files

0.1.5

2 release files

0.1.4

2 release files

0.1.3

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page