gitlab glitchtip/glitchtip-backend v7.0.0

2 hours ago
  • Change: GlitchTip runs as a single process. The web/worker split is gone: one granian ASGI server hosts HTTP, the task worker and the scheduler, and scaling means running more replicas of that same process. Action required for installs that ran separate web and worker deployments: collapse them into one deployment of bin/start.sh, which needs no arguments; every replica consumes the task queue. A leftover worker is refused rather than quietly turned into a second web server that also runs migrations: bin/start.sh exits with an error when SERVER_ROLE names anything but web or all_in_one, or on a Heroku worker* dyno, and bin/run-worker.sh now only prints that message and exits. bin/run-web.sh and bin/run-granian.sh remain as shims that run bin/start.sh and will be removed in a later release; the bin/run-celery.sh, bin/run-celery-with-beat.sh and bin/run-beat.sh stubs are gone. IS_WORKER, GLITCHTIP_EMBED_WORKER and SERVER_ROLE no longer do anything and log a warning at startup when set — except GLITCHTIP_EMBED_WORKER=true and SERVER_ROLE=all_in_one, which describe what now happens anyway, so the published Helm chart and sample compose file work unchanged. Database initialization on start is safe with any number of replicas starting at once (init_database takes an advisory lock); set SKIP_INIT=true if migrations run as a separate pre-deploy step (bin/run-migrate.sh). SENTRY_SERVICE_NAME defaults to glitchtip rather than glitchtip-web or glitchtip-worker.
  • Change: WSGI serving is removed. glitchtip/wsgi.py (deprecated since 6.1.7), bin/run-uwsgi.sh, the uWSGI and uwsgi-chunked dependencies, the WSGI_APPLICATION setting and the USE_ASYNC_SERVER switch are gone, and libxml2 leaves the runtime image with them. A deployment that points its server at glitchtip.wsgi:application must switch to glitchtip.asgi:application under granian, which is what the Docker image already runs. USE_ASYNC_SERVER logs a warning at startup if still set. DATABASE_CONN_MAX_AGE is removed with it: persistent connections are a WSGI-era setting, and a non-zero value was actively harmful here — it skipped the pool configuration, discarding every DATABASE_POOL_* setting and leaving the driver on its own built-in pool size. Connections are now always pooled, and setting the variable to anything but 0 logs a warning. Tracing now drops transactions for static-file paths as intended; the rule that suppressed them read a WSGI-only key and never matched under ASGI.
  • Change: PostgreSQL 15 is the minimum supported version, and this release enforces it. Django 6.1 dropped PostgreSQL 14, so a connection to an older server now fails instead of working unsupported. This replaces the 6.2.5 note that PostgreSQL 14 remained supported. Upgrade PostgreSQL first — GlitchTip 6.x runs on 15, and CI's minimum lane has tested 15 since 6.2.5 — then upgrade GlitchTip.
  • Change: Upgrading to 7.0 requires a 6.x install. Migration history is squashed at the state that shipped in 6.0.3, and migrations whose only effect was on data written by 5.x or early 6.x releases are removed, so a database whose history stops at 5.x or earlier has no path forward. Upgrade to the latest 6.x release, let it migrate, then upgrade to 7.0. The jump straight from 5.x is not blocked up front and can fail partway through, leaving migrations to finish by hand, so do not skip the intermediate release. Fresh installs are unaffected.
  • Change: Legacy environment variable aliases are retired. A retired name is now ignored, with a warning at startup naming its replacement; nothing refuses to start, so check your configuration before upgrading. For the retention variables the fallback deletes data: an ignored value means retention drops to its default and the next maintenance run removes anything older. Renamed: GLITCHTIP_MAX_EVENT_LIFE_DAYS → GLITCHTIP_RETENTION_DAYS, GLITCHTIP_MAX_TRANSACTION_EVENT_LIFE_DAYS → GLITCHTIP_TRANSACTION_RETENTION_DAYS, GLITCHTIP_MAX_UPTIME_CHECK_LIFE_DAYS → GLITCHTIP_UPTIME_RETENTION_DAYS, GLITCHTIP_MAX_FILE_LIFE_DAYS → GLITCHTIP_FILE_RETENTION_DAYS, GLITCHTIP_LOGS_COLD_DAYS → GLITCHTIP_LOG_RETENTION_DAYS, GLITCHTIP_EVENTS_HOT_DAYS → GLITCHTIP_EVENT_HOT_DAYS, GLITCHTIP_LOGS_HOT_DAYS → GLITCHTIP_LOG_HOT_DAYS, and ENABLE_OPEN_USER_REGISTRATION → ENABLE_ORGANIZATION_CREATION. The old registration name used to override the new one, so an install that set only the old name must set ENABLE_ORGANIZATION_CREATION to keep its current behavior. Still accepted and unchanged: GLITCHTIP_DOMAIN (alongside GLITCHTIP_URL and APP_URL), FORCE_SCRIPT_NAME (alongside BASE_PATH), and the REDIS_* aliases for VALKEY_*.
  • Change: An organization slug can no longer be only digits, so that an all-digit segment in an API path can be read as an organization id without ever naming a second organization. An organization whose name is only digits now gets a -1 suffix on its slug, the same as a reserved word, and the database rejects an all-digit slug however it is set. The migration renames every existing all-digit slug, deleted organizations included, to the first free -N suffix (2024 becomes 2024-1) and prints each organization id with its old and new slug. Breaking: links, bookmarks, CLI or CI configuration and API scripts that use a renamed slug must be updated to the new one; nothing redirects from the old slug.
  • Perf: Org-scoped API routes check membership and role in the same query that fetches their data, instead of looking the organization up first. This covers every route with an organization in its path except the three public ones. Those routes also accept the organization's numeric id in place of its slug (/api/0/organizations/42/...). Slug URLs work unchanged, and the all-digit slug rule above keeps the two from ever naming different organizations.
  • Change: Role and scope checks are now consistent across org-scoped routes, which tightens several. Breaking for members and API tokens that relied on the old behavior. Deleting an organization needs the owner role; managers could before. Renaming a team, updating a project, creating a client key and hiding an environment need admin or above, as do creating, updating and testing a project alert — members on one of the project's teams could create an alert, and update and test had no role check. Creating a source code repository needs manager or above. Uptime routes had no role or scope check: reads now need org:read, and creating, editing or deleting a monitor or a status page needs project:write (admin or above), so members can no longer do it. Billing routes, which had no scope check either, take org:read or org:admin, and issue stats take event:read. An API token now authorizes a request only through scopes its user's role also holds, so a member's token carrying only a write scope the member role lacks (member:admin, or team:write without team:read) can no longer remove its own user or join an open team.
  • Change: Permission failures on org-scoped routes say what is missing. A member without the role a route needs gets a 403 naming the required role, where most routes answered 404. A non-member still gets a 404, now with the body Organization not found. rather than Not Found. Soft-deleted issues, and the issues and alerts of a soft-deleted project, are 404 on these routes.
  • Change: The debug file and chunk upload routes match organization slugs exactly instead of lowercasing them first, like every other route. A sentry-cli configuration whose org differs from the real slug only in case now gets a 404 and must use the slug as shown in GlitchTip. Project slugs on these routes are still matched case-insensitively.
  • Change: X-Forwarded-For is only read from trusted proxies. TRUSTED_PROXIES used to default to *, which took the first address in the header, so any client could choose the address that login, signup and password-reset rate limits are keyed on. The default is now private: proxies on private, loopback, link-local and carrier-grade NAT addresses are trusted, and the header is read from the right, skipping trusted proxies, so the address a trusted proxy recorded is used rather than one the client wrote. Setting TRUSTED_PROXIES replaces the default (loopback stays trusted) and accepts addresses, networks and the keywords private, loopback and cloudflare. X-Forwarded-Proto is still honoured from any proxy, so https detection is unchanged. Action recommended behind Cloudflare: set TRUSTED_PROXIES=cloudflare,private; behind another proxy with public addresses, list its ranges plus private. Until then users behind the same proxy address share one rate limit, and GlitchTip logs a warning. The startup banner shows the setting, and the log lists the proxies seen. TRUSTED_PROXIES=* keeps the old behaviour.
  • Change: The API no longer adapts events stored in the pre-5.2.0 shape, where exception and breadcrumbs were a bare list rather than {"values": [...]}. Such an event can only survive on an install that ingested it under 5.1 or earlier and kept it through all of 6.x; it now renders without its exception or breadcrumbs section, and logs a warning, rather than failing.
  • Change: The Rust ingest pipeline now serves every POST /api/<project_id>/envelope/ unconditionally; the GLITCHTIP_RUST_INGEST environment variable is removed and ignored if set. The flag shipped as an opt-in while the Rust path was validated against the Python view — an interleaved A/B benchmark and a golden parity suite covered CPU, memory, and observable behavior — and keeping both arms meant every envelope behavior existed twice. The Django view remains only as the pipeline's documented fallback: envelopes carrying an SDK minidump attachment are handed back to it before any side effect, because turning a minidump into an event is implemented in Python (the minidump package plus the Python ingest schema) and has not been ported. Minidump processing is still basic (see the minidump entry below); the handback keeps it on the Python side until it is ported. One behavior change for installs that never enabled the flag: an envelope whose only credential is the envelope-header dsn field is now authenticated from that header and ingested, where the Python view read only the query string and auth headers and answered 403. Requires Valkey/Redis (the default) or, on VALKEY_URL="" installs, the database cache and broker the Rust path already supports — the same stacks as before, since gt_rust was already a hard dependency. Setting the variable now logs a warning at startup so an operator who had deliberately turned it off learns the setting is inert rather than discovering the change in traffic. One unrelated response-shape tidy-up rides along: POST /api/<project_id>/store/ no longer returns the always-null task_id field, which was a debug placeholder nothing ever populated.
  • Change: Source the debug-symbol toolchain (native symbolication, JS sourcemap remapping, ProGuard deobfuscation, C++ demangling) from gt_rust.symbolic instead of the symbolic and cxxfilt PyPI packages, which are removed as dependencies. The functionality is the same Rust crates either way — the symbolic wheel wraps them through cffi, while the already-required glitchtip-rust wheel now exposes the same API surface through its single PyO3 extension — so the swap removes a second ~19 MB native runtime, a cffi layer, and two wheels from the dependency tree with almost no behavior change. It also puts the memory-heavy operations (building a SourceMapCache or symcache parses tens of MB of transient native memory per bundle) behind an interface GlitchTip controls, where they can later be bounded or streamed without touching the call sites. The visible deltas are all display improvements: demangling now covers MSVC and Rust v0 symbols and strips the hash suffix from Rust legacy ones (cxxfilt passed all three through mangled), and ProGuard remaps infer a frame's file name from its class where the mapping lacks file info. One incidental fix: Archive.from_bytes in the cffi package borrows a buffer it does not keep alive (the reason GlitchTip only ever used Archive.open); the replacement copies its input.
  • Change: Symbolication no longer re-reads and re-parses uploaded sourcemaps, native debug files and ProGuard mappings for every event, which is where most of the memory spikes behind out-of-memory restarts of busy installs came from, and memory use during ingest drops accordingly. Each upload is now processed once, when the first event needs it, into a compact lookup cache kept next to the uploads in file storage (under uploads/symbolcache/); uploads with the same content share one cache, and later events reuse it. Nothing needs configuring, and caches are removed along with the uploads they were built from. Two things to expect after upgrading: while a newly uploaded file's cache is being built, which takes a few seconds, the first events that need it may be stored unsymbolicated, and such an event can open an issue of its own, separate from the one its symbolicated successors group into. And some existing issues regroup once, because their events are now symbolicated where they were not, or differently (see the fixes below) — notably Android issues from apps with uploaded ProGuard mappings, which have not been applied since 6.0.8. The wait for a cache is also shared by each batch of incoming events, so a batch that needs many newly uploaded files at once waits a few seconds in all, not a few seconds per file: past that, its events are stored unsymbolicated while the caches finish building. An event with no native frames no longer reads any native debug file (#512), and native events without debug_meta try only a project's newest few debug files instead of all of them. An Android event that names its ProGuard mapping is deobfuscated with that mapping only: if it was never uploaded, the frames stay obfuscated rather than being tried against other builds' mappings. Very large uploads (a ProGuard mapping over 50 MB, for one; less in a container with a small memory limit, so that processing one cannot exhaust it) are skipped, with a log line, and their events stored unsymbolicated.
  • Change: Objects read from S3 or Google Cloud Storage that are larger than 4 MB are spooled to a temporary file instead of being held in memory whole, which bounds memory when reading large uploads. This needs a writable temporary directory; on a read-only root filesystem without one, objects are held in memory as before, and a warning is logged at startup.
  • Change: Static files are served compressed again, and by the ASGI app rather than granian's file mount. GlitchTip shipped WhiteNoise as a dependency for years without it ever handling a request in the released image: the entrypoint set granian's --static-path-mount whenever a collected static/ directory existed, which is always the case there, and setting it removed WhiteNoise from the middleware list. Granian has no compression support, so every asset went out uncompressed -- the main bundle at 67 KB where 16 KB was available -- and one expiry covered the whole tree, so a content-hashed bundle that could be cached forever was treated like a file that changes on every release. /static/ is now answered by ServeStatic, an ASGI application above Django, so a static hit never builds a Django request, and the file is handed to the server by path rather than read in Python, so it costs no filesystem call and no thread; collectstatic writes a brotli and a gzip copy beside each file and ServeStatic hands out whichever the client accepts. Content-hashed bundles, and the source map beside each, are marked immutable; everything that keeps its name across a release keeps a one-hour TTL, so an upgrade cannot pin a client to a stale copy. Django's own responses -- the single-page app's index.html and every API reply -- are now compressed too, which covers the half ServeStatic does not. Those are built per request, so they are compressed per request: brotli for the browsers that accept it, gzip for anything that does not. Brotli at the quality used here produced a smaller body than Django's gzip default for a little under half the processor time, so this costs less CPU per response than sending them uncompressed did to serialize. A cold first load drops from 126 KB to 31 KB. None of this needs configuring, and a CDN or compressing proxy in front still works as before. Two notes for operators: STATICFILES_STORAGE pinned to a whitenoise.storage.* backend is translated to the ServeStatic equivalent with a deprecation warning rather than refusing to start, but should be updated because the translation goes away on the next major; and GRANIAN_STATIC_PATH_MOUNT still hands /static/ back to granian for anyone who wants that instead.
  • Change: The all-in-one image no longer recycles a web worker when its resident memory grows. It previously set granian's GRANIAN_WORKERS_MAX_RSS to half the container's memory limit (floor 1024 MiB, or 2048 MiB with no limit set), on the reasoning that a long-lived worker accumulates fragmentation and a restart is cheap. Both halves of that turned out to be wrong. A memory-triggered respawn fires at peak RSS by definition — granian starts the replacement while the old worker is still serving and still holding its peak — so the overlap needs the threshold plus a whole second worker to fit inside the limit, which at a threshold of half the limit it cannot: the container OOMs at exactly the moment the mechanism decides to protect it, and the killed worker takes its in-flight task batch with it. The floor made this worse on the smallest installs, which are the ones a memory cap is for: at a 2 GiB limit the threshold was 1 GiB and the overlap wanted ~2 GiB, while at a 1 GiB limit the threshold exceeded the limit and could never fire at all, so the setting was harmful at one common size and inert at another and behaved as intended only above about 4 GiB. Meanwhile the accumulation it was built for is no longer there to find. Resident memory on a busy install is dominated by bursts that scale with event volume and worker concurrency, not by a ratchet: measured over 17 hours with recycling off, the hourly minimum stayed flat at roughly 500 MB from the first hour to the last while peaks came and went, and granian's resource monitor defaults to respawning after a single 30-second sample over the threshold, so what it actually detected was a busy worker. Two consequences for operators. Reported memory will look higher than on previous releases, because the old default truncated the measurement rather than the usage — on the install this was measured on, p95 resident memory went from about 1.1 GB to about 2.7 GB with a peak of 3.4 GB, all of it burst, none of it retained. Size the container for the burst, and note that the burst scales with the knobs that bound concurrent work (VTASKS_CONCURRENCY, VTASKS_INGEST_CONCURRENCY, VTASKS_INGEST_BATCH_COUNT) rather than with uptime — an install ingesting a handful of events an hour never reaches these levels regardless of how long it runs. Operators who want a recycling backstop anyway should prefer age to memory: GRANIAN_WORKERS_LIFETIME (seconds) respawns on a timer, which lands at the worker's average footprint instead of its peak and so needs roughly one extra worker of headroom rather than twice a threshold. Setting GRANIAN_WORKERS_MAX_RSS still works and restores the old shape; pair it with GRANIAN_RSS_SAMPLES above 1 so a single burst is not a trigger. The kill timeout the image sets (60s) is unchanged and still bounds the overlap whenever a respawn does happen.
  • Change: Once an organization has opted into overage billing, its usage window — the period events are counted in, throttled against and billed for — is the metered subscription item's own Stripe billing period rather than the base plan's period or, on annual plans, the virtual month GlitchTip computes from it. Stripe files a meter event under the period of the item that contains the event's timestamp, so with a last meter the only window a cycle-to-date total is correct in is that item's period; counted over any other window, a total that outlives the item's period lands in the next one as if it were that period's own and bills those events twice. In Stripe test mode an item attached part-way through a month to an annual subscription was given the anchor-aligned month as its first period, so the two windows were observed to coincide and nothing changes where they do; the point is that GlitchTip no longer assumes it, and takes the period Stripe reports for the item wherever that turns out to be. The period is stored from every path that sees the item (the subscription webhook, the nightly sync, and the enable request itself, so the first re-check bills against it), and when it has run out before the renewal webhook lands, the throttle check re-reads the subscription from Stripe rather than advancing the period locally, at most once every ten minutes per subscription; a failed refresh leaves the window stale, which reports nothing, as before, and an item whose period has never been recorded is throttled but not reported until it is. The nightly sync refreshes any metered period more than two days stale as a backstop. The item's period applies whether or not overage is currently switched on: the item stays attached across a disable, so toggling never moves the window or hands out a fresh quota. The usage endpoints, the billing page's cycle dates and the overage status card all read the same window, and earlier periods on those endpoints step back a month at a time from it. Organizations without a metered item are unaffected.
  • Change: Overage usage is now reported to Stripe as the cycle-to-date total against a Billing Meter that aggregates by last, replacing the per-subscription counter that sent increments to a sum meter. The counter was the source of every overcharge path in the design: any state it lost — a replacement subscription, a rolled-back write, a renewal racing a sweep — meant re-sending units Stripe already held, and the guards around it (write-ahead, conditional rollback, identifiers keyed on the counter) each closed one route and left the next. Keying the counter to the organization instead of the subscription would not have closed it either: the cycle anchor still came from the primary subscription, so a replacement whose period started even seconds later — the checkout-race case — read as a rollover, reset the counter, and re-billed the whole cycle under a fresh identifier. With last there is nothing to re-bill: a retry, a concurrent sweep, or a replacement subscription all restate the same figure, and every failure leaves Stripe holding at most what was measured. Stripe files each event under the period containing its timestamp and, within a period, keeps the latest (verified, not assumed), so the measurement time is sent rather than the send time and a slow sweep can never overtake a fresher one. Every report is recomputed from current data and nothing previously sent is an input to it, so a wrong figure, whatever put it there, is replaced by the next sweep restating the right one, downwards included. Two inputs are held for the cycle rather than the figure itself. The spend cap is floored at what the cycle has already billed, so lowering it stops further growth without retracting billed usage, while a lower true total still goes out. And overage is measured against the largest quota the plan has held during the cycle, so a mid-cycle downgrade cannot turn usage that was within quota when it happened into billable overage; an upgrade likewise covers the whole cycle and forgives overage accrued under the smaller plan. The remaining state is a record on the organization of the last figure sent, used only to skip unchanged, same-second, or out-of-order reports, and of that cycle quota. Two guards ride along. Because these totals into a sum meter would compound on every sweep, the sweep reads the meter's formula from Stripe (cached) and refuses to report to anything but last, and both provision_overage_billing and its --verify fail on a sum meter; a fresh meter is provisioned under the event name glitchtip_overage_events_total so nothing can land on the old one. And because a Stripe meter aggregates per customer, two subscriptions each carrying the metered price both bill the same usage (also verified against Stripe); enabling overage is now refused when another subscription on the account already has the metered item. A measurement outside the local billing period is dropped in both directions: the local period dates lag Stripe's by a webhook, and a check that ran just after a boundary while still counting the old cycle would otherwise file the old cycle's whole total under the new period. This release assumes no customer is yet being invoiced for overage under the previous meter, which holds for the hosted service. An install that was must do a cutover, since a price's meter is fixed: create a metered price on the overage product bound to the new last meter (provision_overage_billing does this in test mode), archive the old price, and for each subscription remove the old metered item with clear_usage and attach the new one before the next period boundary — leaving both attached bills the cutover cycle twice.
  • Change: Breaking for managers: they can no longer grant the owner role, invite an owner, or change or remove an owner. If an organization's only owner is gone, its managers can no longer remove them or become owner; an instance admin has to change the roles in the Django admin. Web imports are capped the same way: imported members get at most the importing user's role, and an import no longer changes existing members or pending invites.
  • Change: Breaking for web imports: projects and teams are matched by slug within the organization being imported into, and existing ones are left unchanged. Imported projects get new ids and keys, so their DSNs change; update your SDKs' DSNs after a web import. To move an organization to a server you administer and keep its DSNs, use the import management command, which keeps ids and keys.
  • Change: Breaking: only a comment's author, a manager or an owner can edit or delete it; other members and admins could before.
  • Fix: Link crash-report embed dialog submissions to their issue. submit_embed_error_page looked up the associated event by IssueEvent.id, the server-generated UUIDv7 primary key, when the eventId query param it receives is always the SDK's client-generated event_id (the same value Sentry.lastEventId() returns and the ingest endpoint echoes back as {"id": ...}). The two are unrelated UUIDs, so the lookup essentially never matched and UserReport.issue_id stayed NULL for effectively every report submitted through the embed dialog. The lookup now filters on event_id, scoped to the requesting organization and the last week of events so it stays cheap on a large, partitioned table, matching the bounded query the SDK-feedback ingestion path (tasks.ingest_user_report) already uses.
  • Change: On billing-enabled installs, enabling overage no longer converts a classic-mode Stripe subscription to flexible billing mode first; the metered item is attached directly, so every active subscription must already be in flexible mode.
  • Change: The S3-compatible server used for local development and CI is Garage instead of MinIO, whose open-source server is archived upstream and no longer published. compose.minio.yml is replaced by compose.s3.yml. Production object storage configuration is unaffected.
  • Security: Bound minidump parsing. The minidump package trusts every count a file claims, so a few crafted bytes sent to POST /api/<project_id>/minidump/, or attached to an envelope as event.minidump, could make it allocate gigabytes and get the process OOM-killed; since 7.0 runs the web server, task worker and scheduler in one process, that took all three down. The header, stream directory, thread list and module list are now checked against the file size before parsing, and the streams GlitchTip never reads are skipped. A malformed minidump is answered with 400 on the minidump endpoint and dropped from an envelope. Well-formed minidumps ingest as before.
  • Security: Cap uptime monitors per organization on billing-enabled installs. Monitor creation was unbounded and Monitor.interval accepts one second, so a single account could stand up an arbitrary number of monitors aimed at one third-party URL and have the check dispatcher send them traffic on a schedule — traffic the abuser pays for once, at signup, rather than per request as event ingestion charges. The billing throttle that eventually stops an over-quota organization is no defense here: uptime checks are weighted at a tenth of an event, nothing on the check path triggers the fast per-organization throttle re-check (only event ingestion and Stripe webhooks do), and the periodic sweep that would catch an organization sending no events at all runs every four hours. A hundred monitors at a one-second interval exhaust the free tier's monthly quota in under two minutes and then keep running for the rest of that window. The ceiling is now enforced when a monitor is created, where it costs one COUNT. Plans set their own through an uptime_monitors Stripe product metadata value, the way events already carries the event quota; where a plan sets none, a built-in default of 100 applies. Self-hosted installs stay uncapped: billing being off means the operator chose who holds an account and can remove one, which is the control a hosted install does not have over an anonymous signup.
  • Security: Update brotli to 1.2.1 for CVE-2026-111497. The 1.2.0 C decoder it replaces is reachable from outbound fetches that decode a Brotli-compressed response.
  • Feat: Look up issues by short ID. GET /api/0/organizations/{org}/shortids/{short_id}/ resolves an ID such as PROJECT-1A, a short ID typed into the issue search jumps straight to that issue (falling back to text search when none matches), and the MCP issue tools accept short IDs (#515).
  • Feat: PUT /api/0/projects/{org}/{project}/issues/ bulk-updates a project's issues, which is what sentry-cli issues resolve/mute/unresolve and glitchtip-cli call. Issues are chosen by repeated id parameters; without id only a status change is allowed, applied to the issues matching the status query parameter (unresolved by default). sentry-cli's muted and resolvedInNextRelease statuses are accepted.
  • Feat: Authenticate to AWS RDS with IAM tokens instead of a database password. Set DATABASE_IAM_AUTH=true; a short-lived token is minted for each new physical connection, so pool growth and connection recycling keep working past the token's 15-minute lifetime, and any password in DATABASE_URL is ignored for the covered aliases. DATABASE_IAM_AUTH_ALIASES (default default) selects which database aliases use it — PgBouncer cannot validate an IAM token, so leave out any alias that connects through it; RDS Proxy works. IAM aliases require a verified TLS connection (sslmode=verify-full by default) unless DATABASE_IAM_ALLOW_UNVERIFIED_TLS=true; DATABASE_SSLMODE and DATABASE_SSLROOTCERT configure TLS with or without IAM auth. The region is taken from the RDS endpoint hostname.
  • Feat: DATABASE_PREPARE_THRESHOLD controls server-side prepared statements. The default, 5, keeps the existing behavior (prepare a statement after five uses), which saves database CPU on the ingest path. Set it to off behind a transaction-mode connection pooler that cannot track prepared statements (PgBouncer needs 1.21+ with max_prepared_statements), or where migrations run while the previous release is still serving, which can otherwise fail requests with cached plan must not change result type until connections recycle. Any other integer prepares after that many uses; 0 prepares on first use.
  • Feat: The MCP server is ported to MCP SDK v2 and reports the GlitchTip version in its initialize handshake. get_event and get_latest_event now return a lean, size-bounded payload: stack-frame local variables and request bodies are left out by default, and an event over the budget sheds system frames, breadcrumb data, older breadcrumbs and long strings in that order, always keeping the exception and crash-site frames. A _meta block says what was omitted, and the new get_event_detail tool fetches an omitted section on request.
  • Feat: Filter alerts by environment. A project alert can name the environments it applies to; events and uptime monitors from other environments do not trigger it. Leaving the list empty matches every environment, as before.
  • Feat: Add a Slack alert recipient that sends Block Kit formatted messages to a Slack incoming webhook, for both issue and uptime alerts.
  • Feat: Sort uptime monitors by name, creation date, project or status, ascending or descending.
  • Fix: Listing an organization's uptime status pages returns only that organization's pages. It returned the status pages of every organization the user belongs to.
  • Fix: Apply an organization's or project's event throttle once per request rather than twice, which is what a partial rate was doing and why one never dropped the share it names. The rate is sampled when the request arrives, against the rates held in the block cache, and the request that got past that sampling then read the same rates back from the database and was sampled against them again. Two samplings of one rate compound: a rate set to drop half its events dropped about three quarters, one set to drop a tenth dropped about a fifth, and each extra drop cost a full project lookup before it happened. Only the second sampling is removed, and only where the database reports the rates the request was already sampled against; where the two disagree both are applied to the one request that observes the change, so a rate that has just been raised takes effect immediately rather than up to thirty seconds later. A rate cleared back to zero now also drops its cache entry, which was previously left to expire and went on dropping events until a request happened to get past it. Both ingest authentication paths had it, so this covers the sentry-protocol endpoints and the native OTLP ones alike. A rate of 100 was never affected, since it rejects outright rather than sampling.
  • Fix: Reject events from a switched-off project out of the ingest block cache, as was intended, instead of re-querying the database for every one. An organization with is_accepting_events unset, or an organization or project at a 100% throttle, had its block cached under a one-letter marker that the reader turned back into a throttle rate of zero percent — that is, into no throttle at all — so the cache entry was written on every request and acted on by none. The rejection itself was never in doubt, since the database row that follows says the same thing; what was lost is the cheap rejection the cache exists for, which is the whole defense when the traffic being rejected is a flood. The OTLP and Rust ingest paths were written later and already cache the blocked case as the 100% throttle it is; the endpoints still authenticated in Python — store, security, minidump, and the envelope view the Rust pipeline hands minidumps back to — now do the same. Two details ride along: a 100% throttle is stated to reject rather than left to a sampling bound that happens never to exceed it, and a blocked project now quotes one Retry-After (600 seconds) however the rejection was reached, where the curve would otherwise have returned 797 for a rejection served from cache and 600 for the same condition read from the database. A project id now keys that cache by its value rather than as the request spelled it. The store and security routes capture it as text, so 7, 007 and 7 arrived as written while naming one project, and each spelling took its own cache entry — enough to walk past a block indefinitely, one fresh project lookup per spelling, which is the cost the cache is there to avoid. The same normalization answers 400 rather than 500 for an id that is not a number at all, including a digit string long enough to trip CPython's integer conversion limit. Unchanged for anyone not blocked, and a block still clears within the same 30 seconds as before.
  • Fix: Finish making the importer follow pagination, and repair the import management command. Following a Link header meant deciding what to do about the absolute URL inside it, and the previous release refused any that did not match the origin the operator typed — sound in intent, but a server builds that URL from the public address it is configured with rather than the address the request arrived on, so a reverse proxy, a port-forward, or plain http in front of an https-configured install is enough to make the two differ and abort the import. Verified against a sentry 9.1.2 instance, whose pagination links name its configured public URL: reached by that name the import completed, reached by its address and port the same import failed. Worse, it failed only once a collection crossed a page boundary, so an install small enough to fit in one page imported cleanly and a real one did not. The link's path and query are now re-anchored onto the origin already being read, which is strictly the stronger guarantee — the auth token cannot leave the operator's origin even if a remote asks it to, rather than being compared against a host the remote chose. A remote that advertises a next link on every page, marking the end only by pointing back at a page already fetched, now ends the walk there instead of being crawled to the page cap; a page that is not the list of results it should be is reported instead of silently discarding everything collected before it; and a failed import answers 400 naming the reason rather than 500. Separately, the import management command could not import a single member: it is the only caller that creates users, and resolving one issued a query the wrong way for an async caller, so it raised SynchronousOnlyOperation partway through and left the organization written but empty. It is also the only path that brings user accounts across, the web importer recording members without them, so nothing that used it worked. The query now runs once for the whole set rather than once per member. Its existing test missed this by answering the members endpoint with an empty list, which returns before the loop.
  • Fix: Don't let one log event with a malformed timestamp fail every log batched with it. Log ingestion parsed client-supplied timestamps unguarded, so a numeric value outside datetime's representable range — observed live as a client sending epoch milliseconds where seconds belong, which parses as year 58575 and raises ValueError: year must be in 1..9999 — escaped the worker task and failed the whole ingest batch, sending up to 25 unrelated projects' log submissions to the dead-letter queue with no retry, repeatedly for as long as the client kept logging. Obvious unit mistakes (milli/micro/nanoseconds — anything past roughly year 5138 as seconds) are now rescaled to seconds and ingested at their intended time, since the existing one-day drift window proves intent unambiguously; anything still unparseable rejects that single log through the same counter the drift check uses, and the rest of the batch processes normally. Review of the fix found and closed the remaining ways one hostile log could still fail its batch: a naive ISO timestamp string parsed successfully but raised in the timezone-aware drift comparison (now read as UTC); an OTel severity_number outside the smallint column's range failed the batch at insert (now stored as NULL); and a NUL character anywhere in a log's text fields or attributes failed the batch's COPY, since PostgreSQL rejects NUL in text and jsonb outright (now stripped).
  • Fix: Detect the container CPU quota on cgroup v1 hosts when sizing MALLOC_ARENA_MAX. The startup tuning read only cgroup v2's cpu.max and otherwise fell back to nproc, which reports the host's CPU count rather than the container's share — the exact failure the setting exists to prevent, so on a v1 host (still the default on some older distributions) a small container on a large machine got an arena cap of twice the host's cores and could hold far more per-thread arena memory than intended. It now reads cpu.cfs_quota_us and cpu.cfs_period_us before falling back.
  • Fix: Bulk issue merges and updates keep the selection they were given (#507). Background batches rebuilt their filters without the id, project and date restrictions, so a merge of selected issues could absorb unrelated issues from elsewhere in the organization, and the merge task could loop indefinitely. Filters are now preserved, batches advance, events left behind by the immediate batch are moved, and a merge whose selection spans more than one project is rejected with 400.
  • Fix: Validate ProGuard mapping uploads with a cheap structural check instead of building the full mapping index. The old check parsed the whole file into memory (about 190 MB for a large Android mapping file, enough to get a small container OOM-killed on upload) while accepting almost any text as a valid mapping.
  • Fix: ProGuard mappings uploaded with sentry-cli upload-proguard 3.x (which uploads in chunks) are stored. They were rejected as unsupported debug files while the CLI reported success, so they never took effect. A file the server cannot use now makes the upload report an error.
  • Fix: ProGuard mappings are used again. The debug file query that skips source bundles dropped every row without a kind in its data, which is every uploaded mapping, so Android stack traces have stayed obfuscated since 6.0.8.
  • Fix: Deobfuscated ProGuard frames get their real line number, rather than keeping the obfuscated one (which also showed the wrong source context). Methods R8 inlined come back as all their frames instead of only the innermost, and abs_path follows the remapped file name.
  • Fix: Windows (PE/PDB) crashes with debug_meta are symbolicated. The event's debug id lost the PDB age, so it never matched the uploaded PDB.
  • Fix: Native frames from Windows builds get source context from an uploaded source bundle. A bundle stores a file recorded as C:\src\app.cpp as files/C/src/app.cpp, and the lookup never asked for that name.
  • Fix: Every architecture of a universal (fat) Mach-O or dSYM upload is used. Only the first slice, usually x86_64, was recorded, so crashes on the others, arm64 among them, stayed unsymbolicated. A universal file uploaded before this release keeps only its first slice until it is deleted and uploaded again.
  • Fix: Native caller frames show the line of the call rather than the line after it. Their return addresses are now looked up one byte back, as symbolizers do.
  • Fix: Estimate a native image base (for frames with neither debug_meta nor image_addr, as Flutter on Android sends) on a page boundary, with the first frames all tried as anchors. The old estimate was usually off by the first frame's offset within its page. It also applied its best guess even when that matched almost nothing, writing wrong symbols such as _init at line 0. Now a base must fit most frames, and no other base may fit as many; otherwise (as in dense Dart AOT symbol tables, where every nearby base fits) the frames stay unresolved rather than guessed.
  • Fix: Symbolicate every exception of a chained native or ProGuard exception, not only the first.
  • Fix: A JavaScript frame whose URL starts with webpack: no longer fails its whole ingest batch with a TypeError.
  • Fix: Sourcemaps: a bundle matched by debug id wins over one matched only by file name, and one event's debug_meta can no longer make another event in the same ingest batch be symbolicated against a bundle it never named. Context lines from CRLF sources no longer keep a trailing \r.
  • Fix: Use the image base address the SDK reports in debug_meta.images (matched by debug id) for native symbolication instead of estimating it, which fixes wrong frames on Flutter Android crashes.
  • Fix: Bound cold-storage memory in the span rollup. The hourly rollup computed percentiles with approx_quantile, whose per-group state DuckDB does not count against its memory limit; with near-unique span descriptions this reached several GiB. It now uses exact quantile_disc, which is bounded, faster and more accurate.
  • Fix: Cold-storage reads and writes over S3 sign requests with an explicit region (boto3's, falling back to us-east-1). DuckDB could otherwise sign with an empty region, which permissive S3-compatible servers accept and stricter ones reject as AuthorizationHeaderMalformed.
  • Fix: Deliver uptime alerts to Feishu (Lark) recipients, with cards for both the down and recovery states. Test notifications worked, but uptime state changes never reached the channel.
  • Fix: On billing-enabled installs, stop sending a form-encoded content type to Stripe's v2 API, which accepts only JSON. The meter-error webhook's event fetch and provision_overage_billing --verify's event-destination check both failed with 415.
  • Fix: On billing-enabled installs, an organization that cancels and then resubscribes no longer keeps overage state from the canceled subscription. The canceled subscription's webhook could arrive after the new one was already primary, so neither branch ran and the metered item and opt-in were left pointing at a dead subscription.
  • Fix: On billing-enabled installs, the nightly Stripe sync logs an error naming any customer whose non-canceled subscriptions carry the metered overage price more than once. A Stripe meter aggregates per customer, so each such subscription bills the whole usage. Enabling overage already refuses to create this state; the sync is the backstop for one created elsewhere.
  • Fix: On billing-enabled installs, Stripe Checkout now refuses an organization that already has an active paid subscription. Checkout creates a new subscription rather than changing the existing one, so a paid organization upgrading through it ended up with two active subscriptions and was billed both base plans; plan changes for paid organizations go through the billing portal, which changes the plan in place.
  • Fix: On billing-enabled installs, the overage status endpoint now returns reportedCostCents, the cost of the usage already sent to Stripe this period. Switching overage off stops further reporting but not the invoice for the figure already filed, and the only cost field until now was gated on overage being active, so the subscription page showed nothing where an invoice was still coming. The field is zero when nothing was sent this period or the metered item is no longer attached.
  • Deps: Django 6.1, with django-async-backend 6.1 alongside it (the async ORM tracks Django's minor version), glitchtip-rust 0.11 and django-vpg 0.6. Two effects an operator can see: PostgreSQL 15 is now required, as above, and psycopg-binary leaves the dependency tree. psycopg is used only as a library (SQL composition and JSON parameter wrappers), with no database I/O through it, so the pure-Python build against the system libpq is enough; the Docker image already ships libpq, but a checkout run outside it now needs libpq installed.
  • Deps: django-vtasks 3.2.0. The scheduler no longer stops for the life of the process on a transient broker error (which silently stopped all issue alerts), the periodic rescue sweep no longer dead-letters healthy in-flight tasks on the Valkey backend, and retries back off exponentially instead of logging a traceback every second during a broker outage.
  • Deps: Refresh the lockfile to the latest compatible versions, including django-ipware 8 (the project already asked for ~=8.0, but the lock still resolved 7.x), sentry-sdk 2.70, django-ninja 1.7 and mcp 2.2. Client IP resolution is unchanged: django-ipware 8's new default algorithm trusts CDN client-IP headers and skips private leading X-Forwarded-For hops, so IPWARE_ALGORITHM is pinned to legacy, the rules the Rust envelope path also follows.

Don't miss a new glitchtip-backend release

NewReleases is sending notifications on new releases.