devalerts 0.3.0__tar.gz → 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {devalerts-0.3.0 → devalerts-0.4.0}/PKG-INFO +29 -1
- {devalerts-0.3.0 → devalerts-0.4.0}/README.md +28 -0
- {devalerts-0.3.0 → devalerts-0.4.0}/pyproject.toml +1 -1
- devalerts-0.4.0/src/devalerts/_badge.py +55 -0
- {devalerts-0.3.0 → devalerts-0.4.0}/src/devalerts/_store.py +12 -0
- {devalerts-0.3.0 → devalerts-0.4.0}/src/devalerts/cli.py +50 -1
- {devalerts-0.3.0 → devalerts-0.4.0}/LICENSE +0 -0
- {devalerts-0.3.0 → devalerts-0.4.0}/src/devalerts/__init__.py +0 -0
- {devalerts-0.3.0 → devalerts-0.4.0}/src/devalerts/_alert.py +0 -0
- {devalerts-0.3.0 → devalerts-0.4.0}/src/devalerts/_blame.py +0 -0
- {devalerts-0.3.0 → devalerts-0.4.0}/src/devalerts/_celery.py +0 -0
- {devalerts-0.3.0 → devalerts-0.4.0}/src/devalerts/_slack.py +0 -0
- {devalerts-0.3.0 → devalerts-0.4.0}/src/devalerts/_telegram.py +0 -0
- {devalerts-0.3.0 → devalerts-0.4.0}/src/devalerts/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devalerts
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Send unhandled Python exceptions straight to a Telegram chat (or Slack). No backend, no account, no database — just your own bot token or webhook URL.
|
|
5
5
|
Keywords: telegram,error-tracking,exception-monitoring,alerts,logging,monitoring,asgi
|
|
6
6
|
Author: sslinNn
|
|
@@ -139,6 +139,34 @@ resend doubles the effective `rate_limit_seconds` for that group (capped at
|
|
|
139
139
|
forever. A group that goes quiet and reappears once resets to the base rate
|
|
140
140
|
immediately. The dashboard shows the active multiplier (`● sending ×4`).
|
|
141
141
|
|
|
142
|
+
## Crash streak badge
|
|
143
|
+
|
|
144
|
+
`devalerts dashboard` also reports how long it's been since the last
|
|
145
|
+
recorded incident (sent or suppressed by rate-limiting) — same idea as the
|
|
146
|
+
classic "days since last workplace accident" sign, applied to your crashes:
|
|
147
|
+
|
|
148
|
+
```
|
|
149
|
+
🟢 14 days since the last incident.
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Get the same number as a shareable SVG badge for your README:
|
|
153
|
+
|
|
154
|
+
```
|
|
155
|
+
uv run devalerts badge --out docs/streak.svg
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+

|
|
159
|
+
|
|
160
|
+
No hosting, no cron job built in — regenerate and commit it yourself,
|
|
161
|
+
e.g. on a schedule with GitHub Actions:
|
|
162
|
+
|
|
163
|
+
```yaml
|
|
164
|
+
- run: uv run devalerts badge --out docs/streak.svg
|
|
165
|
+
- run: git add docs/streak.svg && git commit -m "Update crash streak badge" || true
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Pass `--label` to customize the left-hand text (default `"crash streak"`).
|
|
169
|
+
|
|
142
170
|
## Context: hostname and tags
|
|
143
171
|
|
|
144
172
|
Every alert automatically includes the sending host, so you can tell which
|
|
@@ -112,6 +112,34 @@ resend doubles the effective `rate_limit_seconds` for that group (capped at
|
|
|
112
112
|
forever. A group that goes quiet and reappears once resets to the base rate
|
|
113
113
|
immediately. The dashboard shows the active multiplier (`● sending ×4`).
|
|
114
114
|
|
|
115
|
+
## Crash streak badge
|
|
116
|
+
|
|
117
|
+
`devalerts dashboard` also reports how long it's been since the last
|
|
118
|
+
recorded incident (sent or suppressed by rate-limiting) — same idea as the
|
|
119
|
+
classic "days since last workplace accident" sign, applied to your crashes:
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
🟢 14 days since the last incident.
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Get the same number as a shareable SVG badge for your README:
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
uv run devalerts badge --out docs/streak.svg
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+

|
|
132
|
+
|
|
133
|
+
No hosting, no cron job built in — regenerate and commit it yourself,
|
|
134
|
+
e.g. on a schedule with GitHub Actions:
|
|
135
|
+
|
|
136
|
+
```yaml
|
|
137
|
+
- run: uv run devalerts badge --out docs/streak.svg
|
|
138
|
+
- run: git add docs/streak.svg && git commit -m "Update crash streak badge" || true
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Pass `--label` to customize the left-hand text (default `"crash streak"`).
|
|
142
|
+
|
|
115
143
|
## Context: hostname and tags
|
|
116
144
|
|
|
117
145
|
Every alert automatically includes the sending host, so you can tell which
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "devalerts"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.4.0"
|
|
4
4
|
description = "Send unhandled Python exceptions straight to a Telegram chat (or Slack). No backend, no account, no database — just your own bot token or webhook URL."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Crash-streak SVG badge rendering for `devalerts badge` -- no network call,
|
|
2
|
+
no shields.io dependency, just a hand-rolled flat badge."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from xml.sax.saxutils import escape
|
|
7
|
+
|
|
8
|
+
_BAND_COLORS = {
|
|
9
|
+
"grey": "#9f9f9f",
|
|
10
|
+
"red": "#e05d44",
|
|
11
|
+
"yellow": "#dfb317",
|
|
12
|
+
"green": "#4c1",
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _streak_band(days: int | None) -> str:
|
|
17
|
+
if days is None:
|
|
18
|
+
return "grey"
|
|
19
|
+
if days < 1:
|
|
20
|
+
return "red"
|
|
21
|
+
if days < 7:
|
|
22
|
+
return "yellow"
|
|
23
|
+
return "green"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _streak_text(days: int | None) -> str:
|
|
27
|
+
if days is None:
|
|
28
|
+
return "no incidents yet"
|
|
29
|
+
if days < 1:
|
|
30
|
+
return "today"
|
|
31
|
+
return f"{days} day{'' if days == 1 else 's'}"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _render_badge(label: str, days: int | None) -> str:
|
|
35
|
+
"""Flat shields.io-style badge. Character width is a fixed-per-glyph
|
|
36
|
+
approximation, not real font metrics -- fine for a README image, not
|
|
37
|
+
meant to be pixel-perfect."""
|
|
38
|
+
value = _streak_text(days)
|
|
39
|
+
color = _BAND_COLORS[_streak_band(days)]
|
|
40
|
+
label_width = 10 + 7 * len(label)
|
|
41
|
+
value_width = 10 + 7 * len(value)
|
|
42
|
+
width = label_width + value_width
|
|
43
|
+
height = 20
|
|
44
|
+
label_esc = escape(label)
|
|
45
|
+
value_esc = escape(value)
|
|
46
|
+
return (
|
|
47
|
+
f'<svg xmlns="http://www.w3.org/2000/svg" width="{width}" height="{height}">'
|
|
48
|
+
f'<rect width="{label_width}" height="{height}" fill="#555"/>'
|
|
49
|
+
f'<rect x="{label_width}" width="{value_width}" height="{height}" fill="{color}"/>'
|
|
50
|
+
f'<g fill="#fff" font-family="Verdana,Geneva,sans-serif" font-size="11">'
|
|
51
|
+
f'<text x="{label_width / 2}" y="14" text-anchor="middle">{label_esc}</text>'
|
|
52
|
+
f'<text x="{label_width + value_width / 2}" y="14" text-anchor="middle">'
|
|
53
|
+
f"{value_esc}</text>"
|
|
54
|
+
f"</g></svg>"
|
|
55
|
+
)
|
|
@@ -152,6 +152,18 @@ def _should_send(
|
|
|
152
152
|
return True, 0, True
|
|
153
153
|
|
|
154
154
|
|
|
155
|
+
def _last_incident() -> float | None:
|
|
156
|
+
"""Unix timestamp of the most recent occurrence across all groups (sent or
|
|
157
|
+
suppressed -- last_seen is updated either way), or None if the state DB
|
|
158
|
+
has no groups recorded (fresh install, or everything cleared)."""
|
|
159
|
+
conn = _get_connection()
|
|
160
|
+
try:
|
|
161
|
+
row = conn.execute("SELECT MAX(last_seen) FROM error_groups").fetchone()
|
|
162
|
+
finally:
|
|
163
|
+
conn.close()
|
|
164
|
+
return row[0]
|
|
165
|
+
|
|
166
|
+
|
|
155
167
|
def _match_fingerprints(prefix: str) -> list[str]:
|
|
156
168
|
"""Fingerprints starting with prefix, for CLI mute/unmute/clear commands."""
|
|
157
169
|
conn = _get_connection()
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""CLI: `devalerts dashboard` reports grouped/rate-limited errors from the local
|
|
2
2
|
state DB; `devalerts test` sends a one-off message to verify bot_token/chat_id
|
|
3
|
-
and/or slack_webhook_url
|
|
3
|
+
and/or slack_webhook_url; `devalerts badge` renders a 'days since last
|
|
4
|
+
incident' SVG badge."""
|
|
4
5
|
|
|
5
6
|
from __future__ import annotations
|
|
6
7
|
|
|
@@ -11,13 +12,16 @@ import sqlite3
|
|
|
11
12
|
import sys
|
|
12
13
|
import time
|
|
13
14
|
from importlib.metadata import version as _pkg_version
|
|
15
|
+
from pathlib import Path
|
|
14
16
|
|
|
17
|
+
from ._badge import _render_badge, _streak_band
|
|
15
18
|
from ._slack import _send_slack_message
|
|
16
19
|
from ._store import (
|
|
17
20
|
_DB_PATH,
|
|
18
21
|
_DEFAULT_RATE_LIMIT_SECONDS,
|
|
19
22
|
_clear,
|
|
20
23
|
_clear_all,
|
|
24
|
+
_last_incident,
|
|
21
25
|
_match_fingerprints,
|
|
22
26
|
_set_muted,
|
|
23
27
|
)
|
|
@@ -54,6 +58,23 @@ def _relative_time(ts: float, now: float) -> str:
|
|
|
54
58
|
return f"{int(delta // 86400)}d ago"
|
|
55
59
|
|
|
56
60
|
|
|
61
|
+
def _streak_line(last_incident: float, now: float, unicode_ok: bool) -> str:
|
|
62
|
+
days = int((now - last_incident) // 86400)
|
|
63
|
+
band = _streak_band(days)
|
|
64
|
+
icons = (
|
|
65
|
+
{"red": "🔴", "yellow": "🟡", "green": "🟢"}
|
|
66
|
+
if unicode_ok
|
|
67
|
+
else {"red": "[RESET]", "yellow": "[WARN]", "green": "[OK]"}
|
|
68
|
+
)
|
|
69
|
+
if band == "red":
|
|
70
|
+
return (
|
|
71
|
+
f"{icons['red']} New incident {_relative_time(last_incident, now)}"
|
|
72
|
+
" — streak reset to 0 days."
|
|
73
|
+
)
|
|
74
|
+
plural = "" if days == 1 else "s"
|
|
75
|
+
return f"{icons[band]} {days} day{plural} since the last incident."
|
|
76
|
+
|
|
77
|
+
|
|
57
78
|
def _color_enabled() -> bool:
|
|
58
79
|
if os.environ.get("NO_COLOR") is not None or not sys.stdout.isatty():
|
|
59
80
|
return False
|
|
@@ -157,6 +178,9 @@ def _dashboard(as_json: bool = False) -> int:
|
|
|
157
178
|
ellipsis = "…" if unicode_ok else "..."
|
|
158
179
|
times_char = "×" if unicode_ok else "x"
|
|
159
180
|
|
|
181
|
+
print(_streak_line(rows[0][3], now, unicode_ok))
|
|
182
|
+
print()
|
|
183
|
+
|
|
160
184
|
style = _Style(_color_enabled())
|
|
161
185
|
type_width = max(len("TYPE"), *(len(r[1]) for r in rows))
|
|
162
186
|
|
|
@@ -255,6 +279,18 @@ def _clear_command(prefix: str | None, clear_all: bool) -> int:
|
|
|
255
279
|
return 0
|
|
256
280
|
|
|
257
281
|
|
|
282
|
+
def _badge_command(out: str | None, label: str) -> int:
|
|
283
|
+
incident = _last_incident()
|
|
284
|
+
days = int((time.time() - incident) // 86400) if incident is not None else None
|
|
285
|
+
svg = _render_badge(label, days)
|
|
286
|
+
if out:
|
|
287
|
+
Path(out).write_text(svg, encoding="utf-8")
|
|
288
|
+
print(f"Wrote badge to {out}.")
|
|
289
|
+
else:
|
|
290
|
+
print(svg)
|
|
291
|
+
return 0
|
|
292
|
+
|
|
293
|
+
|
|
258
294
|
def _test(
|
|
259
295
|
bot_token: str | None, chat_id: str | None, slack_webhook_url: str | None
|
|
260
296
|
) -> int:
|
|
@@ -330,6 +366,17 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
330
366
|
clear_target.add_argument(
|
|
331
367
|
"--all", action="store_true", help="Delete all error groups"
|
|
332
368
|
)
|
|
369
|
+
badge_parser = subparsers.add_parser(
|
|
370
|
+
"badge", help="Print or write an SVG 'days since last incident' badge"
|
|
371
|
+
)
|
|
372
|
+
badge_parser.add_argument(
|
|
373
|
+
"--out", help="Write the SVG to this path instead of stdout"
|
|
374
|
+
)
|
|
375
|
+
badge_parser.add_argument(
|
|
376
|
+
"--label",
|
|
377
|
+
default="crash streak",
|
|
378
|
+
help="Badge label text (default: 'crash streak')",
|
|
379
|
+
)
|
|
333
380
|
args = parser.parse_args(argv)
|
|
334
381
|
|
|
335
382
|
if args.command == "dashboard":
|
|
@@ -342,6 +389,8 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
342
389
|
return _mute_command(args.fingerprint, muted=False)
|
|
343
390
|
if args.command == "clear":
|
|
344
391
|
return _clear_command(args.fingerprint, args.all)
|
|
392
|
+
if args.command == "badge":
|
|
393
|
+
return _badge_command(args.out, args.label)
|
|
345
394
|
return 1
|
|
346
395
|
|
|
347
396
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|