Send unhandled Python exceptions straight to a Telegram chat. No backend, no account, no database — just your own bot token.
Project description
devalerts
Send unhandled Python exceptions straight to a Telegram chat — the moment they happen, on your phone. No backend, no account, no database — just your own bot token.
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.
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
-
Create a bot with @BotFather and get its token.
-
Message your bot once (or add it to a group) so it's allowed to message you back.
-
Get your chat id — message @userinfobot, or call
https://api.telegram.org/bot<TOKEN>/getUpdatesafter step 2 and readmessage.chat.id. -
Verify it's wired up correctly before touching any code:
uv run devalerts test --bot-token 123456:ABC-DEF... --chat-id 123456789 -
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.
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
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).
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 on a key
collision:
devalerts.report(extra={"request_id": "abc123"})
@devalerts.capture(extra={"job": "nightly-sync"})
def run_job(): ...
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.
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.
Works on Windows / Linux / macOS?
Yes — stdlib only (urllib, sqlite3, threading), no OS-specific code
paths.
Privacy & Security
- The only network call devalerts makes is to
api.telegram.org— 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.
- No accounts, no signup, no API key beyond the bot token 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 Telegram delivery fails after retrying, the alert (already redacted, if
redact=True) is appended to~/.devalerts/failed.loginstead 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)
- Slack delivery
- 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.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file devalerts-0.2.0.tar.gz.
File metadata
- Download URL: devalerts-0.2.0.tar.gz
- Upload date:
- Size: 14.9 kB
- Tags: Source
- Uploaded using 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}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4883d45c623220d0d602db49202e02beb8fdcb10991ae949e49c1d349871ffff
|
|
| MD5 |
fd0a48b60a43d6f662a4bd8694787b21
|
|
| BLAKE2b-256 |
8e0d31b7188c524133980b7a7ad15a30faf6dfb22b9461cd9f056e8185cc0c70
|
File details
Details for the file devalerts-0.2.0-py3-none-any.whl.
File metadata
- Download URL: devalerts-0.2.0-py3-none-any.whl
- Upload date:
- Size: 18.3 kB
- Tags: Python 3
- Uploaded using 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}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
160cf1cc68f272d739e5754401938d95f3c9d84ce9633a792c6ea3cd68a31300
|
|
| MD5 |
1cf76a51ea669784075bd959c39fafcc
|
|
| BLAKE2b-256 |
4c24418ce1954b8a51c4dd9561455798c26a7c1a4c6d02640d8fe4e92062348a
|