foxray 0.1.2__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.
Files changed (72) hide show
  1. foxray-0.1.2/LICENSE +21 -0
  2. foxray-0.1.2/NOTICE +34 -0
  3. foxray-0.1.2/PKG-INFO +308 -0
  4. foxray-0.1.2/README.md +263 -0
  5. foxray-0.1.2/foxray/__init__.py +3 -0
  6. foxray-0.1.2/foxray/__main__.py +3 -0
  7. foxray-0.1.2/foxray/core/__init__.py +2 -0
  8. foxray-0.1.2/foxray/core/alerts.py +225 -0
  9. foxray-0.1.2/foxray/core/auth.py +54 -0
  10. foxray-0.1.2/foxray/core/cron.py +176 -0
  11. foxray-0.1.2/foxray/core/dsn.py +63 -0
  12. foxray-0.1.2/foxray/core/envelope.py +166 -0
  13. foxray-0.1.2/foxray/core/filters.py +173 -0
  14. foxray-0.1.2/foxray/core/fingerprint.py +82 -0
  15. foxray-0.1.2/foxray/core/ingest.py +255 -0
  16. foxray-0.1.2/foxray/core/maintenance.py +321 -0
  17. foxray-0.1.2/foxray/core/models.py +178 -0
  18. foxray-0.1.2/foxray/core/monitors.py +150 -0
  19. foxray-0.1.2/foxray/core/normalize.py +358 -0
  20. foxray-0.1.2/foxray/core/present.py +146 -0
  21. foxray-0.1.2/foxray/core/scrub.py +96 -0
  22. foxray-0.1.2/foxray/core/settings.py +96 -0
  23. foxray-0.1.2/foxray/core/sql.py +996 -0
  24. foxray-0.1.2/foxray/core/storage.py +139 -0
  25. foxray-0.1.2/foxray/core/util.py +101 -0
  26. foxray-0.1.2/foxray/django/__init__.py +4 -0
  27. foxray-0.1.2/foxray/django/admin.py +406 -0
  28. foxray-0.1.2/foxray/django/apps.py +21 -0
  29. foxray-0.1.2/foxray/django/conf.py +39 -0
  30. foxray-0.1.2/foxray/django/management/__init__.py +0 -0
  31. foxray-0.1.2/foxray/django/management/commands/__init__.py +0 -0
  32. foxray-0.1.2/foxray/django/management/commands/foxray_create_project.py +18 -0
  33. foxray-0.1.2/foxray/django/management/commands/foxray_prune.py +20 -0
  34. foxray-0.1.2/foxray/django/management/commands/foxray_tick.py +16 -0
  35. foxray-0.1.2/foxray/django/migrations/0001_initial.py +696 -0
  36. foxray-0.1.2/foxray/django/migrations/0002_default_project.py +18 -0
  37. foxray-0.1.2/foxray/django/migrations/__init__.py +0 -0
  38. foxray-0.1.2/foxray/django/models.py +275 -0
  39. foxray-0.1.2/foxray/django/runtime.py +162 -0
  40. foxray-0.1.2/foxray/django/sdk.py +231 -0
  41. foxray-0.1.2/foxray/django/store.py +48 -0
  42. foxray-0.1.2/foxray/django/templates/admin/foxray/_style.html +21 -0
  43. foxray-0.1.2/foxray/django/templates/admin/foxray/issue_detail.html +49 -0
  44. foxray-0.1.2/foxray/django/templates/admin/foxray/storage.html +22 -0
  45. foxray-0.1.2/foxray/django/urls.py +14 -0
  46. foxray-0.1.2/foxray/django/views.py +69 -0
  47. foxray-0.1.2/foxray/standalone/__init__.py +2 -0
  48. foxray-0.1.2/foxray/standalone/cli.py +158 -0
  49. foxray-0.1.2/foxray/standalone/config.py +66 -0
  50. foxray-0.1.2/foxray/standalone/mail.py +56 -0
  51. foxray-0.1.2/foxray/standalone/store.py +105 -0
  52. foxray-0.1.2/foxray/standalone/web.py +783 -0
  53. foxray-0.1.2/foxray.egg-info/PKG-INFO +308 -0
  54. foxray-0.1.2/foxray.egg-info/SOURCES.txt +70 -0
  55. foxray-0.1.2/foxray.egg-info/dependency_links.txt +1 -0
  56. foxray-0.1.2/foxray.egg-info/entry_points.txt +2 -0
  57. foxray-0.1.2/foxray.egg-info/requires.txt +15 -0
  58. foxray-0.1.2/foxray.egg-info/top_level.txt +1 -0
  59. foxray-0.1.2/pyproject.toml +55 -0
  60. foxray-0.1.2/setup.cfg +4 -0
  61. foxray-0.1.2/tests/test_boundaries.py +29 -0
  62. foxray-0.1.2/tests/test_django.py +357 -0
  63. foxray-0.1.2/tests/test_django_zero_config.py +108 -0
  64. foxray-0.1.2/tests/test_events.py +184 -0
  65. foxray-0.1.2/tests/test_ingest.py +246 -0
  66. foxray-0.1.2/tests/test_maintenance.py +201 -0
  67. foxray-0.1.2/tests/test_monitors.py +132 -0
  68. foxray-0.1.2/tests/test_protocol.py +136 -0
  69. foxray-0.1.2/tests/test_rules.py +121 -0
  70. foxray-0.1.2/tests/test_sdk_any_version.py +43 -0
  71. foxray-0.1.2/tests/test_sdk_standalone.py +158 -0
  72. foxray-0.1.2/tests/test_store_contract.py +338 -0
foxray-0.1.2/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dan Quellhorst
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
foxray-0.1.2/NOTICE ADDED
@@ -0,0 +1,34 @@
1
+ Foxray
2
+ Copyright (c) 2026 Dan Quellhorst. MIT License (see LICENSE).
3
+
4
+ Foxray works with the Sentry SDKs. It is not affiliated with, endorsed by, or
5
+ sponsored by Functional Software, Inc. ("Sentry"). "Sentry" is a trademark of
6
+ Functional Software, Inc.
7
+
8
+ How it was built
9
+ ----------------
10
+ Foxray is written from scratch (clean room) against public descriptions of
11
+ the protocol the Sentry SDKs speak:
12
+
13
+ - Sentry's SDK developer documentation (develop.sentry.dev): envelopes and
14
+ their item types, the event payload, the X-Sentry-Auth header and DSN
15
+ format, the X-Sentry-Rate-Limits header, and cron check-ins.
16
+ - The sentry-python SDK (MIT License, Copyright (c) 2018 Functional Software,
17
+ Inc.), read as a reference for what it actually sends (compression, auth
18
+ header, check-in and monitor_config fields, how it parses rate-limit
19
+ headers) and used, unmodified, in Foxray's tests to prove compatibility.
20
+ No SDK code is included in Foxray.
21
+
22
+ No code from Sentry's server (getsentry/sentry) or Relay (getsentry/relay),
23
+ which are under the Functional Source License, was read or used.
24
+
25
+ GlitchTip (MIT) was not used as a source: no GlitchTip code was read,
26
+ included or adapted. The comparison in the README is a short summary of the
27
+ project as publicly described, not derived from its code.
28
+
29
+ Optional third-party packages, not bundled
30
+ ------------------------------------------
31
+ - Django (BSD-3-Clause), for the Django app.
32
+ - psycopg (LGPL-3.0), for Postgres with the standalone server.
33
+ - Brotli (MIT), to decode brotli-compressed envelopes.
34
+ - sentry-sdk (MIT), for capturing a Django app's own errors in-process.
foxray-0.1.2/PKG-INFO ADDED
@@ -0,0 +1,308 @@
1
+ Metadata-Version: 2.4
2
+ Name: foxray
3
+ Version: 0.1.2
4
+ Summary: A light error tracker that accepts errors from the Sentry SDKs. Works as a Django app or a single small process.
5
+ Author: Dan Quellhorst
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/askrobots/foxray
8
+ Project-URL: Source, https://github.com/askrobots/foxray
9
+ Project-URL: Issues, https://github.com/askrobots/foxray/issues
10
+ Keywords: error-tracking,sentry,django,self-hosted,monitoring,cron-monitoring
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Environment :: Web Environment
13
+ Classifier: Framework :: Django
14
+ Classifier: Framework :: Django :: 4.2
15
+ Classifier: Framework :: Django :: 5.2
16
+ Classifier: Framework :: Django :: 6.0
17
+ Classifier: Intended Audience :: Developers
18
+ Classifier: Intended Audience :: System Administrators
19
+ Classifier: Operating System :: OS Independent
20
+ Classifier: Programming Language :: Python :: 3
21
+ Classifier: Programming Language :: Python :: 3 :: Only
22
+ Classifier: Programming Language :: Python :: 3.10
23
+ Classifier: Programming Language :: Python :: 3.11
24
+ Classifier: Programming Language :: Python :: 3.12
25
+ Classifier: Programming Language :: Python :: 3.13
26
+ Classifier: Programming Language :: Python :: 3.14
27
+ Classifier: Topic :: Software Development :: Bug Tracking
28
+ Classifier: Topic :: System :: Monitoring
29
+ Requires-Python: >=3.10
30
+ Description-Content-Type: text/markdown
31
+ License-File: LICENSE
32
+ License-File: NOTICE
33
+ Provides-Extra: django
34
+ Requires-Dist: Django>=4.2; extra == "django"
35
+ Provides-Extra: postgres
36
+ Requires-Dist: psycopg>=3.1; extra == "postgres"
37
+ Provides-Extra: brotli
38
+ Requires-Dist: Brotli; extra == "brotli"
39
+ Provides-Extra: dev
40
+ Requires-Dist: pytest; extra == "dev"
41
+ Requires-Dist: pytest-django; extra == "dev"
42
+ Requires-Dist: sentry-sdk>=2; extra == "dev"
43
+ Requires-Dist: Django<5,>=4.2; extra == "dev"
44
+ Dynamic: license-file
45
+
46
+ # Foxray
47
+
48
+ A light error tracker that accepts errors from the official Sentry SDKs. It
49
+ runs two ways from one codebase:
50
+
51
+ - **inside a Django app**, as `foxray.django`: errors from the app itself are
52
+ stored in-process (no DSN, no HTTP), and the Django admin is the UI;
53
+ - **on its own**, as one small stdlib-only process (`foxray serve`) with a
54
+ SQLite file or Postgres behind it.
55
+
56
+ **Works with the Sentry SDKs; not affiliated with Sentry.**
57
+
58
+ Install from PyPI: `pip install foxray` (add `[postgres]` or `[brotli]` for
59
+ those extras).
60
+
61
+ **Design rule: one process plus one database; no Redis, Celery, Kafka or
62
+ ClickHouse.** Everything other trackers push into side services lives in the
63
+ database, kept as simple as possible:
64
+
65
+ | Elsewhere it's in | In Foxray it's |
66
+ | --- | --- |
67
+ | Redis rate-limit counters | `foxray_rate`: one atomic upsert per project per minute |
68
+ | Redis/memcached dedupe, cooldowns | a unique index on event ids; `last_alerted_at` on the issue |
69
+ | Celery alert tasks | `foxray_alert`: an outbox written in the ingest transaction, sent after commit, retried |
70
+ | Celery beat (cron checks, cleanup) | `foxray_state` leases; run after ingest when due, or by `foxray tick` |
71
+ | Kafka/ClickHouse counts | `foxray_issue_day`, `foxray_counter`: daily counters |
72
+
73
+ So several processes (uvicorn workers, a cron `tick`) always agree, and
74
+ nothing else needs running.
75
+
76
+ ## Add it to a Django app
77
+
78
+ `pip install sentry-sdk` and Foxray, add `"foxray.django"` to `INSTALLED_APPS`,
79
+ add `path("foxray/", include("foxray.django.urls"))` to your URLs, and run
80
+ `python manage.py migrate`. That's all: the migration creates a "default"
81
+ project, `AppConfig.ready()` starts the Sentry SDK with an in-process transport
82
+ (DjangoIntegration, CeleryIntegration if Celery is installed,
83
+ `traces_sample_rate=0`, no PII), errors appear in the admin under **Foxray >
84
+ Issues**, and alert emails go to `settings.ADMINS` (or, if that is empty, the
85
+ superusers) through Django's email backend. Access is Django's own: superusers
86
+ see everything; give staff `foxray.view_issue` (and `foxray.change_issue` to
87
+ resolve or ignore) through groups. Postgres or SQLite; Django 4.2+; Python 3.10+.
88
+
89
+ ```python
90
+ # settings.py
91
+ INSTALLED_APPS = [..., "foxray.django"]
92
+
93
+ # Optional; every key has a default. FOXRAY_<KEY> env vars fill in what isn't set here.
94
+ FOXRAY = {
95
+ "BASE_URL": "https://askrobots.com", # for links in alerts and DSNs
96
+ "ALERT_EMAILS": ["ops@example.com"], # default: ADMINS, else superusers
97
+ "ALERT_WEBHOOK": "", # POSTs alert JSON here too
98
+ "MAX_DB_MB": 500, # Foxray's own tables
99
+ }
100
+
101
+ # urls.py
102
+ urlpatterns += [path("foxray/", include("foxray.django.urls"))]
103
+ ```
104
+
105
+ - If you already call `sentry_sdk.init(...)` before apps load, or `SENTRY_DSN`
106
+ is set, Foxray leaves the SDK alone. (A left-over `SENTRY_DSN` with no SDK
107
+ started: set `FOXRAY = {"AUTO_INIT": True}` and Foxray starts anyway.) To send the app's own errors to Foxray
108
+ explicitly, call `foxray.django.sdk.init(**options)` instead of
109
+ `sentry_sdk.init`. `FOXRAY = {"AUTO_INIT": False}` turns auto-start off. It
110
+ also stays off under `manage.py test` and pytest, so a host app's test errors
111
+ don't land in its test database.
112
+ - Storing never raises into the app: if it fails, the error is logged with
113
+ `logging` (logger `foxray.sdk`, which the SDK is told to ignore).
114
+ - **Other apps** send over HTTP: copy a DSN from **Foxray > Projects** (or run
115
+ `manage.py foxray_create_project webhive`). It looks like
116
+ `https://<key>@askrobots.com/foxray/<project_id>`. The ingest views are
117
+ CSRF-exempt and don't use sessions or auth; the key is the credential.
118
+ - Cleanup runs by itself (see below). `manage.py foxray_tick` (monitors,
119
+ cleanup when due, alerts) and `manage.py foxray_prune` (cleanup now) are there
120
+ if you'd rather run them from cron or your scheduler.
121
+
122
+ **The trade-off:** an app that reports to itself can't report while it's
123
+ down, and an error storm adds load to the same app and database (rate limits
124
+ and per-issue sampling bound it). The standalone server on another box avoids
125
+ both.
126
+
127
+ ## Run it on its own
128
+
129
+ ```sh
130
+ pip install foxray # SQLite: standard library only
131
+ pip install "foxray[postgres]" # optional: Postgres via psycopg
132
+
133
+ export FOXRAY_DATABASE_URL=/var/lib/foxray/foxray.db # or postgres://user@host/foxray
134
+ export FOXRAY_BASE_URL=https://foxray.example.com
135
+ foxray create-project shop # prints the DSN
136
+ foxray create-user you@example.com # a UI login (asks for a password)
137
+ foxray serve --host 127.0.0.1 --port 8765
138
+ ```
139
+
140
+ `serve` is a threaded HTTP server: SDK ingest at `/api/<id>/envelope/` and
141
+ `/api/<id>/store/`, and an HTML UI behind a login (issues, issue pages with the
142
+ stack trace, monitors, projects with their filters and limits, alerts,
143
+ storage). It runs its own tick every minute. Put it behind a TLS proxy (Caddy,
144
+ nginx) and set `FOXRAY_TRUST_PROXY=1`. Other commands: `foxray projects`,
145
+ `foxray tick`, `foxray prune`, `foxray storage`.
146
+
147
+ Configuration comes from environment variables or an INI file
148
+ (`--config foxray.ini`, section `[foxray]`, same names in lower case; a `.toml`
149
+ file works on Python 3.11+). Environment variables win.
150
+
151
+ | Variable | Default | |
152
+ | --- | --- | --- |
153
+ | `FOXRAY_DATABASE_URL` / `DATABASE_URL` | `foxray.db` | SQLite path or `postgres://...` |
154
+ | `FOXRAY_HOST`, `FOXRAY_PORT` | `127.0.0.1`, `8765` | |
155
+ | `FOXRAY_BASE_URL` | `http://host:port` | public URL, for DSNs and alert links |
156
+ | `FOXRAY_SMTP_HOST`, `_PORT`, `_USER`, `_PASSWORD`, `_FROM` | off, 587 | email alerts (STARTTLS; `FOXRAY_SMTP_SSL=1` for 465) |
157
+ | `FOXRAY_ALERT_EMAILS` | | comma-separated; per project too |
158
+ | `FOXRAY_ALERT_WEBHOOK` | | POST alert JSON; per project too |
159
+ | `FOXRAY_RATE_LIMIT_PER_MINUTE` | 600 | events per project; 0 = off; per project too |
160
+ | `FOXRAY_SAMPLE_AFTER` | 1000 | event bodies per issue per day, then counts only |
161
+ | `FOXRAY_MAX_DB_MB` | 500 | hard size cap |
162
+ | `FOXRAY_EVENT_RETENTION_DAYS` | 30 | also `EVENTS_PER_ISSUE` (100), `ISSUE_RETENTION_DAYS` (90) |
163
+ | `FOXRAY_ALERT_COOLDOWN_SECONDS` | 3600 | |
164
+ | `FOXRAY_SECRET_KEY` | generated, kept in the DB | signs UI sessions |
165
+
166
+ Measured on an M-series Mac, Python 3.10, `bench/measure.py` (1,000 SDK-shaped
167
+ 4 KB events, gzip, one keep-alive connection, everything stored):
168
+
169
+ | | idle RSS | 1,000 events |
170
+ | --- | --- | --- |
171
+ | SQLite | 24.5 MB | 1.9 s (about 520/s) |
172
+ | Postgres 14 (local) | 34.9 MB | 5.7 s (about 175/s) |
173
+
174
+ ## Point an SDK at it
175
+
176
+ ```python
177
+ import sentry_sdk
178
+ from sentry_sdk.integrations.django import DjangoIntegration
179
+ from sentry_sdk.integrations.celery import CeleryIntegration
180
+
181
+ sentry_sdk.init(
182
+ dsn="https://<key>@foxray.example.com/<project_id>",
183
+ integrations=[DjangoIntegration(), CeleryIntegration()],
184
+ traces_sample_rate=0,
185
+ send_default_pii=False,
186
+ )
187
+ ```
188
+
189
+ - Recent sentry-sdk releases (2.73 at the time of writing) deprecate
190
+ `send_default_pii` in favour of `data_collection`. Both default to off, so
191
+ you can leave it out.
192
+ - Cron monitors: `@sentry_sdk.crons.monitor(monitor_slug=..., monitor_config=...)`
193
+ or `capture_checkin(...)`. Crontab and interval schedules, timezones,
194
+ `checkin_margin` and `max_runtime` are honoured.
195
+ - **Brotli:** the Python SDK compresses with brotli whenever the `Brotli`
196
+ package is installed in the *sending* app (whitenoise setups often have it).
197
+ Foxray decodes brotli only when `Brotli` is installed where Foxray runs
198
+ (`pip install "foxray[brotli]"`). Otherwise it answers 415 and those events
199
+ are lost, so either install it or set
200
+ `_experiments={"transport_compression_algo": "gzip"}` on the senders. gzip
201
+ and deflate always work.
202
+
203
+ ## What it keeps, and what it drops
204
+
205
+ | Item | |
206
+ | --- | --- |
207
+ | `event` (and the legacy `/store/` JSON) | stored: exception chain, frames with source context, `in_app`, message, level, platform, release, environment, server_name, transaction, tags, contexts, breadcrumbs (last 50), extra |
208
+ | `check_in` | stored; monitors created and scheduled from `monitor_config` |
209
+ | `transaction`, `session(s)`, `client_report`, `attachment`, `profile`, `replay*`, `statsd`, anything unknown | accepted (200), dropped, counted per project per day |
210
+
211
+ - User data is cut to `id` and request data to `url` (query string removed)
212
+ and `method` by default; frame local variables aren't kept unless
213
+ `KEEP_FRAME_VARS` is on. Everything kept is scrubbed before storage:
214
+ passwords, tokens, cookies, Authorization headers, anything under a
215
+ secret-looking key, card numbers (Luhn-checked), private keys, JWTs, and
216
+ Stripe/AWS/GitHub/Slack-style keys.
217
+ - **Grouping:** an SDK `fingerprint` wins (`{{ default }}` works). Otherwise
218
+ each exception's type plus its in-app frames as `module:function` (no line
219
+ numbers, so edits elsewhere don't split an issue). Messages group with
220
+ numbers, hex and ids blanked, using the logging template when there is one.
221
+ - **Answers:** `200 {"id": ...}`; 401/403 for a missing or wrong key; 413 for
222
+ too large (10 MB sent, 20 MB decompressed, 1 MB per event); 415 for an
223
+ unknown encoding; 429 with `Retry-After` and `X-Sentry-Rate-Limits` over the
224
+ rate limit. CORS is open, so browser SDKs work.
225
+
226
+ ## Filters, alerts, cleanup
227
+
228
+ - **Inbound filters** (per project, in the DB, edited in the UI or the admin)
229
+ are applied before anything is stored. A dropped event still gets a 200 and
230
+ is counted by reason per day. You can drop by exception type or message
231
+ (substring or regex, 200-character limit, nested repeats refused, optionally
232
+ only for one environment or release), by environment, or by release. Built-in
233
+ switches drop crawlers by user agent and health-check transactions, and
234
+ (opt-in) browser-extension frames and localhost URLs on JavaScript events.
235
+ After N events per issue per day (1,000) only the count is kept. Issues can
236
+ be ignored for good, until N more events, or until a date.
237
+ - **Alerts** fire on a new issue or a regression (a resolved issue seen again,
238
+ or an ignored one past its "until"). Repeats never alert, and regressions
239
+ respect a per-issue cooldown. Per project they can be limited to new issues
240
+ and/or regressions, a minimum level, and environments. Monitors alert when they
241
+ go from healthy to missed, failed or timed out. Delivery is email and/or a
242
+ webhook, through the outbox.
243
+ - **Cleanup runs by itself.** At most every 10 minutes, the ingest request
244
+ that notices it's due runs one small batch on a short thread after
245
+ answering. A lease row means two never overlap. Defaults:
246
+ - event bodies older than 30 days go;
247
+ - each issue keeps its newest 100 bodies;
248
+ - resolved/ignored issues not seen for 90 days go, with their daily counts;
249
+ - daily counts go after 400 days;
250
+ - check-ins: 30 days or the newest 500 per monitor;
251
+ - sent alerts go after 30 days, failed ones after 7 days of retries;
252
+ - rate windows and drop counters go after 2 days.
253
+
254
+ Issue counts and first/last seen survive all of it.
255
+ - **Size cap** (`MAX_DB_MB`, 500). Over 90%, the oldest bodies go until the
256
+ store is under 80%. Still over 100%, Foxray counts only: it keeps grouping,
257
+ counting and alerting but stores no new bodies, shows a banner, and sends
258
+ one "storage full" alert. Ingest never fails because the store is full. On
259
+ SQLite the file is created with incremental auto-vacuum, so space goes back
260
+ to the disk after each prune (plus a WAL checkpoint). Postgres keeps freed
261
+ space for reuse: Foxray deletes in batches, lets autovacuum work, never runs
262
+ `VACUUM FULL`, and only prunes again once the tables grow. The Storage page
263
+ shows the size, cap, mode, rows per table and the last cleanup.
264
+
265
+ ## Compared with GlitchTip and self-hosted Sentry
266
+
267
+ - **Self-hosted Sentry** is the full product: performance tracing, replays,
268
+ profiling, source maps and symbolication, release health, integrations,
269
+ teams. It is also a couple of dozen services (Kafka, ClickHouse, Redis,
270
+ Postgres, Snuba, Relay, ...) and its docs ask for about 16 GB of RAM. Its
271
+ server is under the Functional Source License.
272
+ - **GlitchTip** is the closest relative: open source (MIT), Sentry-SDK
273
+ compatible, a Django app with a proper multi-user UI, organizations and
274
+ teams, performance and uptime monitoring. A standard install runs PostgreSQL
275
+ plus a Redis-compatible store and a worker.
276
+ - **Foxray** is smaller on purpose: one process and one database (even just a
277
+ SQLite file), about 25 MB of RAM, or no extra process at all inside an
278
+ existing Django app. It does errors, grouping, alerts, cron monitors,
279
+ filters and its own cleanup. It does not do tracing, replays, profiles,
280
+ attachments, source maps (minified JavaScript stack traces stay minified),
281
+ release health, organizations/teams, or integrations beyond email and a
282
+ webhook. If you need those, use GlitchTip or Sentry.
283
+
284
+ ## The name
285
+
286
+ Foxray: add up the letters of "sentry" as you go (a=1…z=26, wrapping past z)
287
+ and you get s x l f x w; in the phonetic alphabet that's Sierra X-ray Lima
288
+ Foxtrot X-ray Whiskey, and Foxtrot X-ray gives the name. It looks inside a
289
+ crash.
290
+
291
+ ## Development
292
+
293
+ ```sh
294
+ python3.10 -m venv .venv && .venv/bin/pip install -e ".[dev,postgres,brotli]"
295
+ .venv/bin/pytest # SQLite everywhere
296
+ FOXRAY_TEST_PG_URL=postgres://you@localhost/postgres .venv/bin/pytest
297
+ # + Postgres (creates and drops its own databases)
298
+ FOXRAY_TEST_DJANGO_PG=postgres://you@localhost/postgres .venv/bin/pytest tests/test_django.py tests/test_store_contract.py
299
+ python bench/measure.py [--db postgres://...] # idle RSS and 1,000-event ingest time
300
+ ```
301
+
302
+ Layout: `foxray/core` (stdlib only: protocol, normalising, grouping, scrubbing,
303
+ filters, cron, alerts, the SQL store, cleanup), `foxray/standalone` (server,
304
+ UI, CLI), `foxray/django` (models, migrations, admin, ingest views, in-process
305
+ transport). The storage contract in `tests/test_store_contract.py` runs
306
+ against SQLite, Postgres and the Django store alike.
307
+
308
+ MIT licensed; see `LICENSE` and `NOTICE`.
foxray-0.1.2/README.md ADDED
@@ -0,0 +1,263 @@
1
+ # Foxray
2
+
3
+ A light error tracker that accepts errors from the official Sentry SDKs. It
4
+ runs two ways from one codebase:
5
+
6
+ - **inside a Django app**, as `foxray.django`: errors from the app itself are
7
+ stored in-process (no DSN, no HTTP), and the Django admin is the UI;
8
+ - **on its own**, as one small stdlib-only process (`foxray serve`) with a
9
+ SQLite file or Postgres behind it.
10
+
11
+ **Works with the Sentry SDKs; not affiliated with Sentry.**
12
+
13
+ Install from PyPI: `pip install foxray` (add `[postgres]` or `[brotli]` for
14
+ those extras).
15
+
16
+ **Design rule: one process plus one database; no Redis, Celery, Kafka or
17
+ ClickHouse.** Everything other trackers push into side services lives in the
18
+ database, kept as simple as possible:
19
+
20
+ | Elsewhere it's in | In Foxray it's |
21
+ | --- | --- |
22
+ | Redis rate-limit counters | `foxray_rate`: one atomic upsert per project per minute |
23
+ | Redis/memcached dedupe, cooldowns | a unique index on event ids; `last_alerted_at` on the issue |
24
+ | Celery alert tasks | `foxray_alert`: an outbox written in the ingest transaction, sent after commit, retried |
25
+ | Celery beat (cron checks, cleanup) | `foxray_state` leases; run after ingest when due, or by `foxray tick` |
26
+ | Kafka/ClickHouse counts | `foxray_issue_day`, `foxray_counter`: daily counters |
27
+
28
+ So several processes (uvicorn workers, a cron `tick`) always agree, and
29
+ nothing else needs running.
30
+
31
+ ## Add it to a Django app
32
+
33
+ `pip install sentry-sdk` and Foxray, add `"foxray.django"` to `INSTALLED_APPS`,
34
+ add `path("foxray/", include("foxray.django.urls"))` to your URLs, and run
35
+ `python manage.py migrate`. That's all: the migration creates a "default"
36
+ project, `AppConfig.ready()` starts the Sentry SDK with an in-process transport
37
+ (DjangoIntegration, CeleryIntegration if Celery is installed,
38
+ `traces_sample_rate=0`, no PII), errors appear in the admin under **Foxray >
39
+ Issues**, and alert emails go to `settings.ADMINS` (or, if that is empty, the
40
+ superusers) through Django's email backend. Access is Django's own: superusers
41
+ see everything; give staff `foxray.view_issue` (and `foxray.change_issue` to
42
+ resolve or ignore) through groups. Postgres or SQLite; Django 4.2+; Python 3.10+.
43
+
44
+ ```python
45
+ # settings.py
46
+ INSTALLED_APPS = [..., "foxray.django"]
47
+
48
+ # Optional; every key has a default. FOXRAY_<KEY> env vars fill in what isn't set here.
49
+ FOXRAY = {
50
+ "BASE_URL": "https://askrobots.com", # for links in alerts and DSNs
51
+ "ALERT_EMAILS": ["ops@example.com"], # default: ADMINS, else superusers
52
+ "ALERT_WEBHOOK": "", # POSTs alert JSON here too
53
+ "MAX_DB_MB": 500, # Foxray's own tables
54
+ }
55
+
56
+ # urls.py
57
+ urlpatterns += [path("foxray/", include("foxray.django.urls"))]
58
+ ```
59
+
60
+ - If you already call `sentry_sdk.init(...)` before apps load, or `SENTRY_DSN`
61
+ is set, Foxray leaves the SDK alone. (A left-over `SENTRY_DSN` with no SDK
62
+ started: set `FOXRAY = {"AUTO_INIT": True}` and Foxray starts anyway.) To send the app's own errors to Foxray
63
+ explicitly, call `foxray.django.sdk.init(**options)` instead of
64
+ `sentry_sdk.init`. `FOXRAY = {"AUTO_INIT": False}` turns auto-start off. It
65
+ also stays off under `manage.py test` and pytest, so a host app's test errors
66
+ don't land in its test database.
67
+ - Storing never raises into the app: if it fails, the error is logged with
68
+ `logging` (logger `foxray.sdk`, which the SDK is told to ignore).
69
+ - **Other apps** send over HTTP: copy a DSN from **Foxray > Projects** (or run
70
+ `manage.py foxray_create_project webhive`). It looks like
71
+ `https://<key>@askrobots.com/foxray/<project_id>`. The ingest views are
72
+ CSRF-exempt and don't use sessions or auth; the key is the credential.
73
+ - Cleanup runs by itself (see below). `manage.py foxray_tick` (monitors,
74
+ cleanup when due, alerts) and `manage.py foxray_prune` (cleanup now) are there
75
+ if you'd rather run them from cron or your scheduler.
76
+
77
+ **The trade-off:** an app that reports to itself can't report while it's
78
+ down, and an error storm adds load to the same app and database (rate limits
79
+ and per-issue sampling bound it). The standalone server on another box avoids
80
+ both.
81
+
82
+ ## Run it on its own
83
+
84
+ ```sh
85
+ pip install foxray # SQLite: standard library only
86
+ pip install "foxray[postgres]" # optional: Postgres via psycopg
87
+
88
+ export FOXRAY_DATABASE_URL=/var/lib/foxray/foxray.db # or postgres://user@host/foxray
89
+ export FOXRAY_BASE_URL=https://foxray.example.com
90
+ foxray create-project shop # prints the DSN
91
+ foxray create-user you@example.com # a UI login (asks for a password)
92
+ foxray serve --host 127.0.0.1 --port 8765
93
+ ```
94
+
95
+ `serve` is a threaded HTTP server: SDK ingest at `/api/<id>/envelope/` and
96
+ `/api/<id>/store/`, and an HTML UI behind a login (issues, issue pages with the
97
+ stack trace, monitors, projects with their filters and limits, alerts,
98
+ storage). It runs its own tick every minute. Put it behind a TLS proxy (Caddy,
99
+ nginx) and set `FOXRAY_TRUST_PROXY=1`. Other commands: `foxray projects`,
100
+ `foxray tick`, `foxray prune`, `foxray storage`.
101
+
102
+ Configuration comes from environment variables or an INI file
103
+ (`--config foxray.ini`, section `[foxray]`, same names in lower case; a `.toml`
104
+ file works on Python 3.11+). Environment variables win.
105
+
106
+ | Variable | Default | |
107
+ | --- | --- | --- |
108
+ | `FOXRAY_DATABASE_URL` / `DATABASE_URL` | `foxray.db` | SQLite path or `postgres://...` |
109
+ | `FOXRAY_HOST`, `FOXRAY_PORT` | `127.0.0.1`, `8765` | |
110
+ | `FOXRAY_BASE_URL` | `http://host:port` | public URL, for DSNs and alert links |
111
+ | `FOXRAY_SMTP_HOST`, `_PORT`, `_USER`, `_PASSWORD`, `_FROM` | off, 587 | email alerts (STARTTLS; `FOXRAY_SMTP_SSL=1` for 465) |
112
+ | `FOXRAY_ALERT_EMAILS` | | comma-separated; per project too |
113
+ | `FOXRAY_ALERT_WEBHOOK` | | POST alert JSON; per project too |
114
+ | `FOXRAY_RATE_LIMIT_PER_MINUTE` | 600 | events per project; 0 = off; per project too |
115
+ | `FOXRAY_SAMPLE_AFTER` | 1000 | event bodies per issue per day, then counts only |
116
+ | `FOXRAY_MAX_DB_MB` | 500 | hard size cap |
117
+ | `FOXRAY_EVENT_RETENTION_DAYS` | 30 | also `EVENTS_PER_ISSUE` (100), `ISSUE_RETENTION_DAYS` (90) |
118
+ | `FOXRAY_ALERT_COOLDOWN_SECONDS` | 3600 | |
119
+ | `FOXRAY_SECRET_KEY` | generated, kept in the DB | signs UI sessions |
120
+
121
+ Measured on an M-series Mac, Python 3.10, `bench/measure.py` (1,000 SDK-shaped
122
+ 4 KB events, gzip, one keep-alive connection, everything stored):
123
+
124
+ | | idle RSS | 1,000 events |
125
+ | --- | --- | --- |
126
+ | SQLite | 24.5 MB | 1.9 s (about 520/s) |
127
+ | Postgres 14 (local) | 34.9 MB | 5.7 s (about 175/s) |
128
+
129
+ ## Point an SDK at it
130
+
131
+ ```python
132
+ import sentry_sdk
133
+ from sentry_sdk.integrations.django import DjangoIntegration
134
+ from sentry_sdk.integrations.celery import CeleryIntegration
135
+
136
+ sentry_sdk.init(
137
+ dsn="https://<key>@foxray.example.com/<project_id>",
138
+ integrations=[DjangoIntegration(), CeleryIntegration()],
139
+ traces_sample_rate=0,
140
+ send_default_pii=False,
141
+ )
142
+ ```
143
+
144
+ - Recent sentry-sdk releases (2.73 at the time of writing) deprecate
145
+ `send_default_pii` in favour of `data_collection`. Both default to off, so
146
+ you can leave it out.
147
+ - Cron monitors: `@sentry_sdk.crons.monitor(monitor_slug=..., monitor_config=...)`
148
+ or `capture_checkin(...)`. Crontab and interval schedules, timezones,
149
+ `checkin_margin` and `max_runtime` are honoured.
150
+ - **Brotli:** the Python SDK compresses with brotli whenever the `Brotli`
151
+ package is installed in the *sending* app (whitenoise setups often have it).
152
+ Foxray decodes brotli only when `Brotli` is installed where Foxray runs
153
+ (`pip install "foxray[brotli]"`). Otherwise it answers 415 and those events
154
+ are lost, so either install it or set
155
+ `_experiments={"transport_compression_algo": "gzip"}` on the senders. gzip
156
+ and deflate always work.
157
+
158
+ ## What it keeps, and what it drops
159
+
160
+ | Item | |
161
+ | --- | --- |
162
+ | `event` (and the legacy `/store/` JSON) | stored: exception chain, frames with source context, `in_app`, message, level, platform, release, environment, server_name, transaction, tags, contexts, breadcrumbs (last 50), extra |
163
+ | `check_in` | stored; monitors created and scheduled from `monitor_config` |
164
+ | `transaction`, `session(s)`, `client_report`, `attachment`, `profile`, `replay*`, `statsd`, anything unknown | accepted (200), dropped, counted per project per day |
165
+
166
+ - User data is cut to `id` and request data to `url` (query string removed)
167
+ and `method` by default; frame local variables aren't kept unless
168
+ `KEEP_FRAME_VARS` is on. Everything kept is scrubbed before storage:
169
+ passwords, tokens, cookies, Authorization headers, anything under a
170
+ secret-looking key, card numbers (Luhn-checked), private keys, JWTs, and
171
+ Stripe/AWS/GitHub/Slack-style keys.
172
+ - **Grouping:** an SDK `fingerprint` wins (`{{ default }}` works). Otherwise
173
+ each exception's type plus its in-app frames as `module:function` (no line
174
+ numbers, so edits elsewhere don't split an issue). Messages group with
175
+ numbers, hex and ids blanked, using the logging template when there is one.
176
+ - **Answers:** `200 {"id": ...}`; 401/403 for a missing or wrong key; 413 for
177
+ too large (10 MB sent, 20 MB decompressed, 1 MB per event); 415 for an
178
+ unknown encoding; 429 with `Retry-After` and `X-Sentry-Rate-Limits` over the
179
+ rate limit. CORS is open, so browser SDKs work.
180
+
181
+ ## Filters, alerts, cleanup
182
+
183
+ - **Inbound filters** (per project, in the DB, edited in the UI or the admin)
184
+ are applied before anything is stored. A dropped event still gets a 200 and
185
+ is counted by reason per day. You can drop by exception type or message
186
+ (substring or regex, 200-character limit, nested repeats refused, optionally
187
+ only for one environment or release), by environment, or by release. Built-in
188
+ switches drop crawlers by user agent and health-check transactions, and
189
+ (opt-in) browser-extension frames and localhost URLs on JavaScript events.
190
+ After N events per issue per day (1,000) only the count is kept. Issues can
191
+ be ignored for good, until N more events, or until a date.
192
+ - **Alerts** fire on a new issue or a regression (a resolved issue seen again,
193
+ or an ignored one past its "until"). Repeats never alert, and regressions
194
+ respect a per-issue cooldown. Per project they can be limited to new issues
195
+ and/or regressions, a minimum level, and environments. Monitors alert when they
196
+ go from healthy to missed, failed or timed out. Delivery is email and/or a
197
+ webhook, through the outbox.
198
+ - **Cleanup runs by itself.** At most every 10 minutes, the ingest request
199
+ that notices it's due runs one small batch on a short thread after
200
+ answering. A lease row means two never overlap. Defaults:
201
+ - event bodies older than 30 days go;
202
+ - each issue keeps its newest 100 bodies;
203
+ - resolved/ignored issues not seen for 90 days go, with their daily counts;
204
+ - daily counts go after 400 days;
205
+ - check-ins: 30 days or the newest 500 per monitor;
206
+ - sent alerts go after 30 days, failed ones after 7 days of retries;
207
+ - rate windows and drop counters go after 2 days.
208
+
209
+ Issue counts and first/last seen survive all of it.
210
+ - **Size cap** (`MAX_DB_MB`, 500). Over 90%, the oldest bodies go until the
211
+ store is under 80%. Still over 100%, Foxray counts only: it keeps grouping,
212
+ counting and alerting but stores no new bodies, shows a banner, and sends
213
+ one "storage full" alert. Ingest never fails because the store is full. On
214
+ SQLite the file is created with incremental auto-vacuum, so space goes back
215
+ to the disk after each prune (plus a WAL checkpoint). Postgres keeps freed
216
+ space for reuse: Foxray deletes in batches, lets autovacuum work, never runs
217
+ `VACUUM FULL`, and only prunes again once the tables grow. The Storage page
218
+ shows the size, cap, mode, rows per table and the last cleanup.
219
+
220
+ ## Compared with GlitchTip and self-hosted Sentry
221
+
222
+ - **Self-hosted Sentry** is the full product: performance tracing, replays,
223
+ profiling, source maps and symbolication, release health, integrations,
224
+ teams. It is also a couple of dozen services (Kafka, ClickHouse, Redis,
225
+ Postgres, Snuba, Relay, ...) and its docs ask for about 16 GB of RAM. Its
226
+ server is under the Functional Source License.
227
+ - **GlitchTip** is the closest relative: open source (MIT), Sentry-SDK
228
+ compatible, a Django app with a proper multi-user UI, organizations and
229
+ teams, performance and uptime monitoring. A standard install runs PostgreSQL
230
+ plus a Redis-compatible store and a worker.
231
+ - **Foxray** is smaller on purpose: one process and one database (even just a
232
+ SQLite file), about 25 MB of RAM, or no extra process at all inside an
233
+ existing Django app. It does errors, grouping, alerts, cron monitors,
234
+ filters and its own cleanup. It does not do tracing, replays, profiles,
235
+ attachments, source maps (minified JavaScript stack traces stay minified),
236
+ release health, organizations/teams, or integrations beyond email and a
237
+ webhook. If you need those, use GlitchTip or Sentry.
238
+
239
+ ## The name
240
+
241
+ Foxray: add up the letters of "sentry" as you go (a=1…z=26, wrapping past z)
242
+ and you get s x l f x w; in the phonetic alphabet that's Sierra X-ray Lima
243
+ Foxtrot X-ray Whiskey, and Foxtrot X-ray gives the name. It looks inside a
244
+ crash.
245
+
246
+ ## Development
247
+
248
+ ```sh
249
+ python3.10 -m venv .venv && .venv/bin/pip install -e ".[dev,postgres,brotli]"
250
+ .venv/bin/pytest # SQLite everywhere
251
+ FOXRAY_TEST_PG_URL=postgres://you@localhost/postgres .venv/bin/pytest
252
+ # + Postgres (creates and drops its own databases)
253
+ FOXRAY_TEST_DJANGO_PG=postgres://you@localhost/postgres .venv/bin/pytest tests/test_django.py tests/test_store_contract.py
254
+ python bench/measure.py [--db postgres://...] # idle RSS and 1,000-event ingest time
255
+ ```
256
+
257
+ Layout: `foxray/core` (stdlib only: protocol, normalising, grouping, scrubbing,
258
+ filters, cron, alerts, the SQL store, cleanup), `foxray/standalone` (server,
259
+ UI, CLI), `foxray/django` (models, migrations, admin, ingest views, in-process
260
+ transport). The storage contract in `tests/test_store_contract.py` runs
261
+ against SQLite, Postgres and the Django store alike.
262
+
263
+ MIT licensed; see `LICENSE` and `NOTICE`.
@@ -0,0 +1,3 @@
1
+ """Foxray: a light error tracker that speaks the Sentry SDK protocol."""
2
+
3
+ __version__ = "0.1.2"
@@ -0,0 +1,3 @@
1
+ from foxray.standalone.cli import main
2
+
3
+ raise SystemExit(main())
@@ -0,0 +1,2 @@
1
+ """Foxray core: protocol parsing, normalising, grouping, scrubbing, rules and
2
+ the SQL store. Pure standard library; never imports Django."""