Skip to content

[UPSTREAM CHANGES] latest changes as of Mon Jul 06 2026 00:59:06 GMT+0000 (Coordinated Universal Time) - #236

Open
github-actions[bot] wants to merge 5710 commits into
masterfrom
upstream-changes-2026-07-06
Open

[UPSTREAM CHANGES] latest changes as of Mon Jul 06 2026 00:59:06 GMT+0000 (Coordinated Universal Time)#236
github-actions[bot] wants to merge 5710 commits into
masterfrom
upstream-changes-2026-07-06

Conversation

@github-actions

@github-actions github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown

This PR is auto-generated by
actions/github-script.

LUARM and others added 30 commits April 29, 2026 20:34
…seline

Python 3.12 is the project's minimum (`requires-python = ">=3.12"`), so
PEP 604 (`X | Y`) and PEP 585 (`list[X]`) syntax is native and the
`from __future__ import annotations` import added in
`glitchtip/oidc_discovery.py` was a no-op. Note this in AGENTS.md so
future AI-drafted contributions don't reintroduce it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
GlitchTip is async-first; the `a` prefix is only meaningful in
libraries that expose parallel sync and async APIs in the same
namespace (Django ORM, allauth, etc.). Rename `aget_openid_config` and
`aget_authorize_url` to drop the prefix and document the convention in
AGENTS.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
expose is_public on StripePrice for canonical-price selection

See merge request glitchtip/glitchtip-backend!2342
feat: filter Stripe products by hosted product_type metadata

See merge request glitchtip/glitchtip-backend!2307
perf(api): cache OIDC discovery doc instead of fetching per request

See merge request glitchtip/glitchtip-backend!2344
Refactor JavaScript sourcemap processing to remap stacktraces per
exception instead of flattening all frames first.

This keeps `raw_stacktrace` unset when sourcemap resolution is a no-op
for an exception, while still preserving the original stacktrace before
the first successful frame remap.
fix: persist raw JS stacktrace before sourcemap remap

See merge request glitchtip/glitchtip-backend!2340
The hosted product_type filter merged in !2307 was added at the same
time !2306 introduced test_sync_product_round_trips_price_is_public.
The filter MR updated test_sync_product to set product_type=hosted in
metadata, but the round-trip test (developed in parallel) was missed.
After both merged the new test's products were filtered out, no
StripePrice rows were created, and aget(stripe_id="price_pub") raised
DoesNotExist on master CI.

Generated with assistance from Claude Code (Opus 4.7).
fix(stripe): add product_type=hosted to round-trip test products

See merge request glitchtip/glitchtip-backend!2345
add test and test data for ios event context;
Fix ios event context v2

See merge request glitchtip/glitchtip-backend!2346
GlitchTip's hot ingest paths (event store, log ingest, transaction ingest,
auth) now use ``async_connections`` directly instead of wrapping sync
cursors in ``sync_to_async``. The default ``ENGINE`` swaps to
``django_async_backend.db.backends.postgresql`` so async cursors are
always available; psycopg3 still does the actual I/O.

Why
---
Threadpool-wrapped sync queries serialise on the GIL during the await
chain. With granian + asyncio, native async cursors release the GIL
during PG round-trips, letting concurrent ingest waves overlap I/O.

What
----
- ``apps.shared.async_db`` exposes ``fetchall`` / ``execute`` /
  ``fetchall_mogrified_values`` / ``execute_mogrified_values`` on top of
  ``async_connections``.
- ``get_project_auth_info_row`` is async-only; auth path uses it directly.
- Default DB ENGINE: ``django_async_backend.db.backends.postgresql``.
  ``close_async_connections`` middleware is always in the chain so async
  cursors return to the pool per request.
- ``apps.event_ingest.process_event`` and ``apps.logs.process_logs`` use
  the async helpers throughout the hot path.
- Synthetic ``/api/_probe/async/`` endpoint for concurrency benchmarks.
- Concurrency benchmark scaffolding in ``benchmarks/``.

Tests
-----
``async_connections`` opens its own PG sessions, so ``TestCase``-style
rollback can't roll back uncommitted fixture data — the async session
doesn't see it.

Hot-path test classes (event_ingest, log ingest, transaction ingest,
sourcecode workflow, alert regression) now subclass ``TransactionTestCase``
(via a new ``GlitchTipTransactionTestCase``). Fixtures get committed and
truncated between tests instead of rolled back, which the async session
sees correctly. The remaining ~870 tests stay on fast ``TestCase``.

Other test-runner adjustments:
- ``TimedTestRunner.teardown_databases`` terminates lingering async-pool
  sessions before ``DROP DATABASE`` so test teardown doesn't fail on
  task-local connections.
- ``compose.yml`` raises Postgres ``max_connections`` to 300 to absorb
  the connection accumulation across ``TransactionTestCase`` classes
  before runner teardown.
- ``EventIngestTestCase.tearDown`` closes async connections per test.

1024 tests pass, 0 fail.

Disclosure: this MR was prepared with assistance from Claude.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
``psycopg.AsyncConnection.close()`` is async, so connections opened in
a task that has gone out of scope keep their socket open until process
exit. Over a 1024-test suite that piles up enough leaked sessions to
trip Postgres' default ``max_connections``.

Two-part fix:

1. ``run_async_closing`` helper (apps/event_ingest/tests/utils.py)
   wraps ``async_to_sync`` for direct ingest invocations, closing each
   alias's task-local async connection in the same task that opened it
   — the same lifecycle ``close_async_connections`` middleware
   provides for HTTP requests.

   Used by tests that drive ``process_issue_events`` /
   ``process_log_events`` / ``process_transaction_events`` directly.

2. ``TimedTestRunner.teardown_databases`` runs
   ``pg_terminate_backend`` on each test DB before stock teardown.
   Catches the residual leaks from paths we can't reach (Django's test
   client async-view dispatch, ``django_vtasks`` immediate batch
   flushes) so the final ``DROP DATABASE`` succeeds.

3. ``compose.yml`` raises ``max_connections`` to 300 — pure
   test-infrastructure headroom while we run with leaks. Production
   doesn't have this pattern (each request goes through middleware
   which closes cleanly).

Disclosure: this MR was prepared with assistance from Claude.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Left over after switching ingest test invocations to ``run_async_closing``.

Disclosure: this MR was prepared with assistance from Claude.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The lock file kept the workspace member entry after pyproject.toml lost
it; CI failed with "Distribution not found at .../gt_rust". Regenerated
with ``uv lock``.

Disclosure: this MR was prepared with assistance from Claude.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The ingest hot path was a mix of native async cursors (raw SQL via
async-backend) and Django ORM convenience methods (abulk_create,
aupdate, aexists, async-iter on a sync QuerySet) that thread-pool
through the sync connection. The latter group made the hot path
double-counted across two pools and opaque to async-aware test
assertions.

Convert every remaining sync_to_async-wrapped DB call in
process_issue_events / process_transaction_events to either raw async
SQL via async_db.fetchall/execute helpers or async-backend's
AsyncQuerySet (debug bundles, where ORM model construction with
select_related is the right tool). Issue + IssueHash creation now
runs inside async_atomic with raw INSERTs; the IntegrityError
race-recovery path reads issue_id back via a SELECT.

Replace assertNumQueries on the affected hot-path tests with a new
AsyncQueryCounter (glitchtip/test_utils/async_query_counter.py) that
patches AsyncCursorWrapper at the class level, so it observes queries
across whatever task the sync test client dispatches into. Sync
queries from setUp fixtures are now ignored by design.

test_store_api.setUp gains cache.clear() so TransactionTestCase's
RESTART IDENTITY truncation can't race the auth/throttle cache between
tests in the class.
Project baseline is Python 3.12+; PEP 604 / PEP 585 are native and
PEP 563 deferred evaluation isn't relied on. Use a forward-reference
string for the self-referential return annotation instead.
Vulture flagged exc_type/exc_value/traceback as unused. Replace with
*_exc_info — the leading underscore tells vulture the variable is
intentionally unused.

Also expand the module docstring to spell out why AsyncQueryCounter
exists and what would have to change before AsyncCaptureQueriesContext
from django-async-backend can replace it (every middleware in
MIDDLEWARE has to be async_capable, and a recalibration pass to absorb
the BEGIN/COMMIT delta in queries_log).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add a prominent docstring banner and an import-time DeprecationWarning to
glitchtip/wsgi.py. GlitchTip is async-first now: WSGI bridges every async
view (including the entire ingest hot path) through async_to_sync, leaks
a task-local async-backend DB connection per request, and bypasses the
glitchtip.ingest_asgi fast-path dispatcher entirely.

Custom deployments that bypass our scripts and import glitchtip.wsgi
directly will now see the warning in their logs and have a clear pointer
to glitchtip.asgi:application under Granian.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
feat(ingest): native async DB cursors via django-async-backend

See merge request glitchtip/glitchtip-backend!2343
The release Docker build previously ran on a single amd64 runner and
emulated arm64 layers via QEMU (`multiarch/qemu-user-static`). Emulated
ARM builds are 5-10x slower than native, dominating the wall time of
every release tag.

GitLab now exposes native aarch64 SaaS runners (tag
`saas-linux-medium-arm64`), so split the multi-arch build into two
parallel native jobs (one per arch) that push arch-suffixed tags, plus a
small `release_manifest_merge` job that uses
`docker buildx imagetools create` to publish the user-facing tags
(`latest`, `X.Y.Z`, `X.Y`, `X`) as a manifest list referencing both
arches. A `release_prepare_assets` job downloads the frontend
`dist/` artifact once and shares it with both arch builds.

Also add a `validate_build_{amd64,arm64}` matrix that runs on MRs into
the default branch and on master pushes. It builds the production
Dockerfile natively for each arch (no push) and runs a cheap
`python -c "import django, glitchtip"` smoke test against the resulting
image. This catches "build succeeds but image won't run on this arch"
regressions before they reach a release tag, where the QEMU build was
the first time we discovered them.

Triggers for the release jobs are unchanged: protected vX.Y.Z tags
only, never on schedules or MRs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drop verbose intros and the QEMU/legacy-job references; keep only the
non-obvious "why" (smoke test purpose, no-DinD note for imagetools).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ci: native multi-arch builds + per-MR build validation

See merge request glitchtip/glitchtip-backend!2349
Bulk INSERT/UPDATE in event ingest, log ingest, and the transaction
pipeline previously expanded one ``(%s, %s, ...)`` tuple per row into a
``VALUES`` list via ``execute_mogrified_values``. That has three costs
that grow with batch size: a per-row mogrify round-trip in Python, a
distinct statement shape per batch length (poor plan-cache reuse), and
the 65535 bind-parameter ceiling Postgres enforces, which clamps batches
on wide schemas (the 13-column ``issue_events_issueevent`` insert tops
out around 5000 rows).

Pass per-column arrays to ``unnest(%s::T[], %s::T[], ...)`` instead. One
parameter per column regardless of batch size; one statement shape; no
bind-param ceiling.

Synthetic benchmark (Postgres 18, unlogged tables, 30 rounds, local docker):

  issue_events_issueevent  13 cols  500 rows: 26% faster (45ms -> 33ms median)
  issue_events_issuetag    6  cols  2000 rows: 52% faster (70ms -> 34ms median)

Converted call sites:
- apps/event_ingest/process_event.py: issueevent INSERT, issuetag INSERT,
  update_issues UPDATE, update_statistics, update_org_statistics,
  transactiongroup INSERT, spanstaging INSERT,
  _update_transaction_group_stats Phase 2, and the two IN(VALUES)
  lookups (_fetch_issue_hashes_raw, _fetch_transaction_groups), now
  rewritten as JOIN unnest().
- apps/logs/process_logs.py: logevent INSERT, log_statistics upsert,
  resource_lookup upsert.

Left as VALUES (not worth converting): the small release/environment
fanout (tiny batches), and ``_update_transaction_group_stats`` Phase 1
which carries an int[] histogram per row that doesn't trivially unnest.

Adds two thin helpers in apps/shared/async_db.py — ``execute_unnest``
and ``fetchall_unnest`` — that transpose row-major data to per-column
lists before executing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
perf(ingest): switch hot-path bulk writes from VALUES to UNNEST

See merge request glitchtip/glitchtip-backend!2350
Same conversion as the prior commit on this branch but for the uptime
hot path: the upsert previously mogrified one ``(%s,%s,%s)`` tuple per
organization in a Python loop, joined them into a ``VALUES`` list, and
ran the whole thing through ``sync_to_async``. Switch to async-native
``execute_unnest`` with ``async_atomic`` so we drop both the per-row
Python work and the sync_to_async thread hop on every check flush
(every 1 s or 100 checks, whichever comes first).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
perf(uptime): switch update_uptime_statistics from VALUES to UNNEST

See merge request glitchtip/glitchtip-backend!2351
The Stripe webhook handler was hitting unrecoverable failures on transient
429 lock_timeout responses from the Stripe API, which Stripe itself flags
as retryable via the Stripe-Should-Retry header.

Two related issues:

1. stripe_get/stripe_post had no retry logic — any non-200 response
   immediately raised. Now they share a retry helper that backs off
   exponentially on 429/5xx, honoring the Stripe-Should-Retry header
   when present.

2. The webhook view marked event IDs as processed via cache.aadd before
   actually processing them. If processing raised, Stripe's webhook retry
   would arrive, hit the dedup cache, and be silently dropped. The mark
   is now released on failure so retries can reprocess.

🤖 Generated with Claude Code
Woodsii and others added 30 commits June 29, 2026 15:35
chore(deps): update dependency symbolic to ~=13.6.1

See merge request glitchtip/glitchtip-backend!2426
Chore/implement async organization methods

See merge request glitchtip/glitchtip-backend!2429
pyproject.toml pins `symbolic~=13.6.1`, but uv.lock was still resolved at
13.5.0 — a stale lock that does not satisfy the constraint (~=13.6.1 means
>=13.6.1,<13.7), so `uv lock --locked` failed on master. The renovate bump
that updated pyproject never regenerated the lock. Regenerate so symbolic
resolves to 13.6.1; lock and pyproject are now consistent.

🤖 Generated with AI (Claude Code) — human review required.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fix(ingest): bucket transaction usage by received time, not client time

See merge request glitchtip/glitchtip-backend!2404
chore(deps): relock symbolic to 13.6.1 to match pyproject

See merge request glitchtip/glitchtip-backend!2431
Replace regex-based 33-char Breakpad appendix stripping with
symbolic's normalize_debug_id, which handles all format variants
including non-zero appendix values that the regex approach missed.

💘 Generated with Crush

Assisted-by: Deepseek V4 Pro via Crush <crush@charm.land>
💘 Generated with Crush

Assisted-by: Deepseek V4 Pro via Crush <crush@charm.land>
FileBlobs become orphaned when multi-chunk uploads are concatenated
into a combined blob, when Files are replaced during source-map
re-uploads, or when uploads are abandoned before assembly.

Add cleanup_orphaned_file_blobs() to the daily maintenance task.
It deletes FileBlobs with zero File references that are older than
24 hours (grace period to protect in-flight uploads).
chore(deps): update dependency symbolic to ~=13.7.0

See merge request glitchtip/glitchtip-backend!2433
apps/organizations_ext/permissions.py defined three DRF-style
ScopedPermission subclasses left over from before the migration to
django-ninja. It imported `from glitchtip.permissions import
ScopedPermission`, but that module no longer exists — importing this file
would raise ImportError. Nothing references the module or its classes
anywhere in the codebase, so it is dead code.

Authorization is now enforced via glitchtip/api/permissions.py
(has_permission) plus per-endpoint role checks; this file played no part.

AI-assisted change (Claude Code).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pulls in glitchtip-rust 0.3.1, whose gt_rust.django_backend now honors
libpq/psycopg verify-ca semantics (CA chain checked, hostname not), so the
Rust Postgres driver can connect through the staging pgbouncer pooler whose
serving cert is issued for the CNPG cluster service names, not the pooler.

uv lock also corrects a latent lockfile inconsistency: pyproject already
pins symbolic~=13.7.0 but the lock still had 13.6.1; relocking updates it to
13.7.0 to satisfy the existing constraint. No other packages change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…_debug_id

💘 Generated with Crush

Assisted-by: Deepseek V4 Pro via Crush <crush@charm.land>
chore(deps): bump glitchtip-rust to >=0.3.1 (verify-ca hostname fix)

See merge request glitchtip/glitchtip-backend!2436
- Merge orphan cleanup into cleanup_old_files instead of a separate
  maintenance step
- Use aiterator() instead of sync_to_async(list) for real async
- Delete redundant cleanup_orphaned_fileblobs management command
fix: fix support for mattermost webhooks glitchtip-backend#485

See merge request glitchtip/glitchtip-backend!2434
Accept Breakpad-format debug_id in NativeDebugImage

See merge request glitchtip/glitchtip-backend!2420
feat(files): periodic cleanup of orphaned FileBlobs

See merge request glitchtip/glitchtip-backend!2432
Follow-ups to the orphaned-FileBlob cleanup:

- Delete DB rows before storage, filtering through the original
  queryset so the pass conditions are re-evaluated inside the delete.
  Uploads dedupe on checksum via get_or_create, so a blob fetched as
  orphaned can gain a File reference before the batch delete runs;
  previously that File would be cascaded away. Row-first ordering
  means a skipped blob keeps both its row and its storage bytes — a
  crash mid-batch can only leak an unreferenced storage object, never
  leave a File whose backing bytes are gone.
- Pass save=False to blob.delete() so it no longer issues an UPDATE
  blanking the blob field on an already-deleted row, halving
  sync-connection writes per blob.
- Count only FileBlob rows against MAX_DELETIONS_PER_RUN instead of
  the cascade total, so the cap is a blob budget.
- Log storage-delete failures with exc_info so a missing file is
  distinguishable from a storage misconfiguration.
- Assert storage-file existence in the cleanup tests.

The ORM stays on the async API throughout; only the storage delete
hops threads, since django-storages has no async API.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
perf(files): streamline FileBlob cleanup deletes

See merge request glitchtip/glitchtip-backend!2439
chore(deps): update dependency symbolic to ~=13.8.0

See merge request glitchtip/glitchtip-backend!2437
django-organizations 2.7.0 ships async versions of the core membership
utilities. Bump to ~=2.7 and drop GlitchTip's local reimplementation of
the create + user_added signal mechanics:

- Organization.add_user/aadd_user now delegate org-user creation and the
  user_added signal to upstream via super(), keeping only the role-based
  first-user-becomes-owner logic (upstream's abstract variant is
  is_admin-based, which GlitchTip does not use).
- The create-organization API calls aadd_user instead of hand-rolling
  the org user and owner records and sending the sync user_added signal
  from an async view.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Assert the creating user gets the OWNER role and an OrganizationOwner
record, since that behavior now lives in aadd_user's first-user branch
rather than inline in the endpoint. Also document that user_added fires
before the owner record exists.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
refactor(organizations): use django-organizations 2.7 async core utils

See merge request glitchtip/glitchtip-backend!2440
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants