Skip to content

[UPSTREAM CHANGES] latest changes as of Mon Jun 15 2026 01:23:01 GMT+0000 (Coordinated Universal Time) - #233

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

[UPSTREAM CHANGES] latest changes as of Mon Jun 15 2026 01:23:01 GMT+0000 (Coordinated Universal Time)#233
github-actions[bot] wants to merge 5598 commits into
masterfrom
upstream-changes-2026-06-15

Conversation

@github-actions

Copy link
Copy Markdown

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

bufke and others added 30 commits April 9, 2026 06:06
Update dependency boto3 to v1.42.86

See merge request glitchtip/glitchtip-backend!2287
Update dependency memray to v1.19.3

See merge request glitchtip/glitchtip-backend!2288
Update dependency symbolic to v12.17.4

See merge request glitchtip/glitchtip-backend!2289
feat: return multiple prices and marketing features from Stripe products API

See merge request glitchtip/glitchtip-backend!2290
Update dependency boto3 to v1.42.87

See merge request glitchtip/glitchtip-backend!2291
Update dependency ruff to v0.15.10

See merge request glitchtip/glitchtip-backend!2292
Update dependency boto3 to v1.42.88

See merge request glitchtip/glitchtip-backend!2294
The heartbeat existence check in fetch() filtered uptime_monitorcheck
only by monitor_id and start_check. Because the table is nested-partitioned
RANGE (UUIDv7 id) -> HASH (organization_id), this forced Postgres to lock
every range partition and every hash sub-partition on every heartbeat
check, exhausting max_locks_per_transaction under concurrency and raising
"out of shared memory".

Add organization_id (hash key) and an id__gte bound derived from the
interval window (range key) so the planner can prune to the relevant
partitions.

AI-assisted: diagnosis and fix drafted with Claude Code.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
transform_parameterized_message crashed the entire ingest batch with
TypeError when a client shipped a log entry whose %d/%f format
specifiers were paired with string params (e.g. '%d' with '0').
The schema-level validator catches this for the positional case but
leaves formatted empty, after which utils re-ran the % operator
without protection.

Wrap both the % and .format() calls in a best-effort try/except and
fall back to the raw template on failure, so one bad payload no longer
takes down a batch of events.

AI-assisted: diagnosis and fix drafted with Claude Code.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
fix: prune partitions in uptime heartbeat existence check

See merge request glitchtip/glitchtip-backend!2295
fix: tolerate malformed parameterized log messages in ingest

See merge request glitchtip/glitchtip-backend!2296
fix: accept ISO-8601 timestamps in log envelope items

See merge request glitchtip/glitchtip-backend!2279
promote_spans was declared async def and then called the sync _promote
via asyncio.to_thread(). That bypasses django-vtasks' sync-task wrapper
(_run_sync_with_db_cleanup in django_vtasks/worker.py), which calls
close_old_connections() before and after execution. The thread-local
Django connection therefore survived across scheduled task runs, and
eventually hit "OperationalError: the connection is closed" when
PgBouncer reaped the idle backend between invocations (INTERNAL-6P,
73 events in 10 days).

Flatten both wrappers to plain sync def. django-vtasks already
dispatches sync tasks via asyncio.to_thread internally, so behavior is
identical minus the stale-connection bug. The re-enqueue on truncation
also becomes the sync .enqueue() variant.

Add regression tests asserting both task functions stay sync, since
reintroducing async def would silently re-break the cleanup path.

AI-assisted: diagnosis and fix drafted with Claude Code.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Make the regression rationale self-contained for open source readers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The prior fix flattened both task wrappers to sync def to route them
through django-vtasks' _run_sync_with_db_cleanup path. That shipped
safely but is inconsistent with the async-first codebase and blocks
further async work here (e.g. native aenqueue).

Flip the refactor: keep the task wrappers async, use async ORM
(async for, .adelete()) for all Django DB work, and wrap only the
genuinely sync bits (arro3 Parquet writes, Django storage, DuckDB)
in sync_to_async with default thread_sensitive=True. Those hops
route to Django's shared sync executor thread — the same thread
django-vtasks cleans via _async_close_old_connections — so the
original "connection is closed" failure mode is structurally
impossible.

Changes:
- promotion.py::promote_spans is now async def. ORM calls use
  async for / .adelete(). _write_chunk_parquet and storage.delete
  are wrapped in sync_to_async at call site. Drops the raw
  connection.cursor() DELETE in favor of ORM .adelete() — same
  generated SQL, partition pruning preserved.
- tasks.py task wrappers are async again. Imports hoisted with
  underscore prefixes to avoid the name shadowing footgun.
  compact_span_chunks (100% storage/DuckDB, no ORM) is wrapped in
  one top-level sync_to_async hop rather than ping-ponging per call.
- test_cold_storage.py: task-invoking tests in PromoteSpansTestCase
  converted to async def + await with abulk_create/acount, baker.make
  wrapped where called from async context. CompactSpansTestCase
  tests stay sync (zero ORM). The sync-task regression class added
  in the prior approach is removed — it asserted the wrong invariant
  for the async-first target shape.

Verified:
- 89 apps/performance tests pass.
- iscoroutinefunction(promote_spans.func) and (compact_span_chunks.func)
  both True.
- ruff check + format clean.

AI-assisted: planned and implemented with Claude Code.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The .adelete() path inlines every UUID as a SQL literal (21KB of
query text for 500 UUIDs; proportionally worse at batch limit 100k)
and wraps the statement in BEGIN/COMMIT. Raw id = ANY(%s) passes
the UUID list as a single bound array parameter and skips the
transaction wrap, which is what the original code was deliberately
doing.

Extract the cursor.execute call into a named sync helper and call
it via sync_to_async so the surrounding task stays async. Doc comment
on the helper explains why the ORM path is wrong to keep anyone
from "fixing" it back.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Push sync_to_async down to the leaf calls (arro3 write, storage
save/delete, os.makedirs) instead of wrapping the whole function at
the call site. Keeps the async-first rule consistent: sync_to_async
is a bandaid applied at the leaves of the call tree, not at interior
nodes. Caller in promote_spans becomes a plain await.

Test call sites use async_to_sync since TestCase.setUp is sync.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
refactor: promote_spans / compact_span_chunks async-native

See merge request glitchtip/glitchtip-backend!2297
Update dependency boto3 to v1.42.89

See merge request glitchtip/glitchtip-backend!2298
Update dependency duckdb to v1.5.2

See merge request glitchtip/glitchtip-backend!2299
bufke and others added 30 commits June 10, 2026 15:18
fix(performance): derive SpanStaging id from server time

See merge request glitchtip/glitchtip-backend!2380
Accept OpenTelemetry logs from any OTLP exporter (language SDKs, the
Collector, auto-instrumentation) at the OTLP-standard /v1/logs path,
with no sentry SDK involved. Both encodings the OTLP/HTTP spec defines
are supported, multiplexed on Content-Type: application/x-protobuf
(the default for OTel SDKs and the Collector) and application/json.

The project is resolved from the DSN public key carried in an auth
header -- Authorization: Bearer <key>, X-Sentry-Auth, or a sentry_key
query param -- because native OTLP exporters point at a base endpoint
and append /v1/logs, leaving no project id in the path. public_key is
globally unique, so the key alone identifies the project.

Records are normalized to the existing OTel log-record shape and reuse
otel_log_to_log_item and the ingest_logs pipeline; resource attributes
(service.name, deployment.environment.name, host.name) are hoisted onto
each record. The /v1/ paths are added to the lean ingest middleware
fast-path. Gated by the existing GLITCHTIP_ENABLE_LOGS flag.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Map an unset protobuf LogRecord body to "" instead of the literal
  string "None" (str(None) downstream); aligns protobuf with the JSON path.
- Treat all-zero trace_id/span_id bytes as absent rather than storing a
  zero-UUID / span_id 0 (8 zero bytes is truthy, so the prior guard let it
  through).
- Honor partial throttle rates (1-99%) on the OTLP path, matching the
  envelope hot path, so a throttled org/project isn't fully exempt.
- Tolerate an optional trailing slash on /v1/logs in both the route and the
  ingest fast-path regex, avoiding a confusing 301-drops-body on misconfig.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Fold `Authorization: Bearer <key>` into the shared auth_from_request so
  the envelope and OTLP paths use one key extractor; delete the duplicate
  otlp_key_from_request. Bearer is harmless on the envelope path (SDKs don't
  send it). get_project_by_key now reuses auth_from_request.
- Hoist the inline `from apps.projects.models import ProjectKey` (no circular
  import) and the opentelemetry-proto / protobuf imports to module top.
- Tests: cover OTLP auth via X-Sentry-Auth and ?sentry_key (now shared), and
  drain the enqueued task in the trailing-slash test so it can't bleed into a
  later test's flush.

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

Two review fixes for the native OTLP/HTTP logs endpoint.

Cheap rejection: `get_project_by_key` resolved the project with a DB lookup
on every request, with no equivalent of the block cache the envelope hot
path keeps. A flood of unpaid traffic — an invalid DSN key, or a real key
whose org is over quota — therefore cost an indexed Postgres lookup per
request (and the lookup precedes the body read, but nothing stopped the
repeat). Add the same block cache, keyed on the DSN public key since there
is no project id on this path: a known-bad key caches "v", an over-quota or
throttled org caches its throttle, and repeat requests are bounced from
Valkey in one round trip before any DB hit or body read. Also fire the
periodic `check_organization_throttle` out of band like the envelope path so
an over-quota OTLP-only org gets throttled promptly rather than waiting on
the 4-hour sweep.

Observed-time fallback: `otel_log_to_log_item` read only time_unix_nano, so
a record carrying only observed time (valid per the OTel log data model when
the original timestamp is unknown) collapsed to the 1970 epoch on the JSON
transport. Fall back to observed_time_unix_nano, matching what the protobuf
decoder already does, so both transports agree.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reverses the precedence from the prior commit. GLITCHTIP_LICENSE_KEY is the
recommended self-hosted setup and represents the deployment's declared config,
so it should win over a hand-edited admin DB row. Checking it first also lets
resolved() return without a DB query on the common path.

This restores the original env-wins design (e2617cd) that a later
admin-only refactor had dropped.
fix(stripe): fall back to GLITCHTIP_LICENSE_KEY env var in resolved()

See merge request glitchtip/glitchtip-backend!2389
fix(billing): report uptime/log usage consistently across periods

See merge request glitchtip/glitchtip-backend!2387
The Monitor.interval field was a PositiveSmallIntegerField (Postgres
smallint, max 32767) while its own MaxValueValidator allows up to 86400
(one day). The uptime monitor API also did not bound interval at all.

As a result, creating or updating a monitor with an interval between
32768 and 86400 (e.g. 86400 for a daily check) passed validation and
then crashed on save with `DataError: smallint out of range`, returning
an HTTP 500 instead of accepting the value or returning a clean error.

The validator's 86400 ceiling shows the intent is to allow up to one
day, so widen the column to PositiveIntegerField rather than lowering
the limit. Also add an explicit `interval: Annotated[int, Ge(1),
Le(86400)]` bound to the MonitorIn schema so an out-of-range value is
rejected at the API layer (clean 4xx) instead of reaching the database.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
difs_create_file_from_chunks iterated FileBlob.objects.filter(
checksum__in=chunks) in arbitrary DB order and stored only blobs[0] as
the file's content. A debug file uploaded as more than one chunk would
therefore fail the checksum verification or, worse, be stored as just
its first chunk.

Order the blobs by the client-supplied chunk list, concatenate them in
order while verifying the whole-file checksum, and persist the result as
a single combined FileBlob (GlitchTip's File model points at one blob).
Single-chunk uploads reuse the uploaded blob directly, so the common
case stores no duplicate data and is unchanged.

Also simplify difs_get_file_from_chunks: File.checksum is the whole-file
SHA1, so a matching checksum already identifies identical content
regardless of how it was chunked.

Tests: assert the assembled blob content matches the whole file, that
reassembly follows the request's chunk order (not DB order), single-
chunk reuse, and missing-chunk handling. Verified end-to-end against
both glitchtip-cli and the open-source sentry-cli uploading a 1.5MB
debug file split into 6 chunks.

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

Add tests for a file whose chunks reference the same blob twice (content
must be duplicated per chunk-list entry) and for assembling the same
multi-chunk file twice (no second combined blob). Replace the
iter(lambda ...) blob reader with a walrus while-loop to match
files/models.py.

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

The sentry-compatible envelope endpoint validates each item's header against
a `type` that must be one of three buckets:

- supported: types we process (transaction, event, user_report, feedback,
  log, otel_log)
- ignored: known types we deliberately accept-and-drop silently (session,
  client_report, attachment, replay_*, span, profile_chunk, ...)
- unknown: anything in neither list, which raises a pydantic ValidationError
  and is reported for visibility in case the SDK spec evolved.

Previously every unknown type produced a structurally identical error, so an
error tracker folded ALL unknown types into one issue — masking the arrival
of any genuinely new item type. Now an unknown-type failure (a literal_error
on the `type` field) is captured under a per-type fingerprint
`["envelope-unsupported-item-type", <type>]`, so each distinct new type
surfaces as its own issue. Other header schema failures (e.g. a malformed
`length`) keep the generic grouping so they aren't merged under a type.

Also adds `trace_metric` to the ignored list. It is a metrics item emitted by
newer sentry JS SDKs that GlitchTip does not support (too heavy/high-volume
for its lightweight goal), so it is now accepted-and-dropped silently instead
of perpetually flagging as unknown.

AI-assisted change; human review required before merge.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(ingest): fingerprint unknown envelope item types per type; ignore trace_metric

See merge request glitchtip/glitchtip-backend!2393
Reassemble multi-chunk debug files in order

See merge request glitchtip/glitchtip-backend!2392
fix(uptime): widen monitor interval column to fit its 86400 max

See merge request glitchtip/glitchtip-backend!2391
feat(ingest): native OTLP/HTTP logs endpoint (POST /v1/logs)

See merge request glitchtip/glitchtip-backend!2374
…dleware

Decompression of ingest request bodies (gzip/deflate/br/zstd) moves from the
hand-rolled Python DecompressBodyMiddleware into the gt_rust extension. There
is no longer a toggle or a Python fallback: gt_rust is now a hard dependency of
the ingest path.

- gt_rust gains a generic decompress(body, encoding, max) primitive
  (glitchtip-rust 0.2.0). The envelope hot path uses parse_envelope to
  decompress + frame in a single Rust pass.
- /envelope/ and /minidump/ (plain Django views) call gt_rust directly at the
  body-read seam. /store/ and /security/ (django-ninja) decompress in the
  ORJSON parser before schema validation, so their routing and status codes
  are unchanged.
- glitchtip/middleware.py (the streaming Python decoders) is deleted and the
  middleware is removed from MIDDLEWARE and the ingest ASGI chain.
- Brotli (a Python dependency only the middleware used) is dropped; zstandard
  stays for the test suite, which still compresses fixtures to exercise ingest.

The win is bounded memory per request: gt_rust decompresses with the GIL
released and a hard size cap (GLITCHTIP_MAX_UNZIPPED_PAYLOAD_SIZE), instead of
materializing the decompressed body on the Python heap before the view runs.

Requires glitchtip-rust>=0.2.0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Now that glitchtip-rust 0.2.0 (with the generic decompress() primitive)
is published, resolve it from the registry to unblock CI.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drop the migration framing ("old middleware", "no longer", "prior loop")
from the Rust-decompression code and comments so they read correctly for
someone arriving fresh, explaining why the work is in Rust (bounded memory,
DOS safety) as steady-state fact rather than as a change from a prior state.

Document the Rust-decompression seam as a gotcha in AGENTS.md and extend the
clean-room terminology guidance (lowercase sentry for SDKs/wire formats;
capital-S Sentry only when distinguishing GlitchTip from the company).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
0.2.1 bounds the attacker-declared envelope item length that could abort
the worker process; pin to it so the vulnerable 0.2.0 can't satisfy the
ingest dependency. Re-locked from the PyPI registry.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The native OTLP/HTTP logs endpoint (POST /v1/logs) landed on master after this
branch was cut. It is a plain Django view served by the minimal ingest ASGI
chain, which previously included DecompressBodyMiddleware — so a gzipped OTLP
body was decompressed upstream before the view read it.

This branch deletes that middleware, so the OTLP view now has to decompress at
its own body-read seam, exactly like the /envelope/ and /minidump/ plain views.
gzip on the wire is not exotic here: the OTLP/HTTP spec requires servers to
support gzip, and the OpenTelemetry Collector's OTLP/HTTP exporter gzips by
default — so real exporter traffic arrives Content-Encoding: gzip.

Decompress via gt_rust (request_content_encoding + decompress_body) before
decode_otlp_logs, mapping the oversized-payload cap to 413 and a garbled stream
to 400. Adds an endpoint test that posts a real gzip-compressed protobuf batch.

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

Moves issue full-text search off the Issue table into a dedicated,
hash-partitioned issue_events_issuesearchindex table and removes the old
Issue.search_vector column + its GIN index entirely.

The Issue.search_vector GIN was maintained on nearly every event but queried
rarely. Worse, an issue's search document saturates fast (title, transaction,
URL, stack filenames repeat across an issue's events), so the per-event
append_and_limit_tsvector + GIN churn was largely redundant work on the hot
ingest path, and it kept the Issue row wide and un-cacheable for the common
issue-list reads.

This is a hard cutover (no dual-write, no read-time OR fallback): few users
search, and active issues repopulate the new index on their next event, so a
brief post-deploy gap self-heals. A bounded backfill covers recently-active
issues so search works immediately after deploy.

- New IssueSearchIndex model: composite PK (issue_id, organization_id),
  hash-partitioned by organization_id, GIN on fts_document. No DB-level FKs
  (a partitioned table can't be an FK target), matching the existing
  partitioned-table convention.
- Ingest writes ONLY the new index: a new issue inserts its row in
  _create_issue_and_hash; subsequent events upsert via a single unnest CTE
  (UPDATE existing rows, INSERT the rest). Raw search text is passed to
  append_and_limit_tsvector so the new table gets the same lexeme/size
  limiting, and re-tokenizing an already-serialized tsvector is avoided. The
  legacy Issue.search_vector UPDATE/INSERT is gone.
- Search reads ONLY the index, scoped by organization_id so Postgres prunes
  hash partitions. list_issues / list_project_issues resolve organization_id
  on the text-search path so the pruning actually triggers (without the old
  column there is no fallback for a full-partition scan).
- delete_issues_in_batches deletes IssueSearchIndex rows (no DB cascade on a
  no-FK partitioned table); regression-tested.
- Migrations: 0019 creates the partitioned table; 0020 backfills recently
  active (GLITCHTIP_EVENT_HOT_DAYS) issues, keyset-paginated over the
  last_seen index (atomic=False, ON CONFLICT DO NOTHING) so cost scales with
  hot-issue count, not table size, and a timed-out deploy resumes on re-run;
  0021 drops the search_vector GIN CONCURRENTLY (atomic=False, never blocks
  ingest); 0022 drops the column under a bounded lock_timeout (metadata-only,
  O(1)). Drops run after the backfill.
- make_sample_issues populates IssueSearchIndex so seeded issues stay
  searchable.

Issue is a non-partitioned table (too large to hash-partition today); dropping
search_vector shrinks it, easing both issue-list cache pressure now and a
future partitioning effort.

DEPLOY ORDER: 0022 drops a column the old release reads/writes, so drain the
old release before applying these migrations.

AI-assisted (Claude Code); human review required before merge.

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

Combines the search-index teardown from four migrations to three. The old
GIN drop (RemoveIndexConcurrently) and the backfill both require
atomic=False / autocommit and are both idempotent (DROP INDEX CONCURRENTLY
IF EXISTS; INSERT ... ON CONFLICT DO NOTHING), so they share one
re-runnable migration:

  0019 create table + partitions + GIN   (atomic, expand)
  0020 backfill hot issues + drop old GIN (atomic=False, both idempotent)
  0021 drop Issue.search_vector column    (atomic, contract)

The destructive column drop stays in its own atomic migration so it keeps
the transactional SET LOCAL lock_timeout guard and remains an isolated,
deferrable contract step. Verified: applies clean on an empty DB,
makemigrations --check reports no drift, and a 200k-issue/140k-hot seed
produces the same end state as the four-migration version.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move the per-event-updated columns (count, last_seen, last_release) plus
status/level and the full-text vector off the wide Issue row onto a new
hash-partitioned-by-organization IssueIndex table. Issue becomes small and
near-static (no per-event rewrite), and these columns gain org-partition
pruning on the issue list and search paths.

- IssueIndex model (table issue_events_issueindex), one-to-one with Issue via
  the `index` relation. Issue exposes count/last_seen/status/level/last_release
  through read-only proxy properties, so the read API is unchanged; queries and
  sorts resolve through index__* and a post_save signal gives ORM-created
  Issues their leaf row (ingest writes the leaf via raw SQL).
- Ingest writes a single leaf upsert per event; the Issue table is no longer
  touched on ingest.
- Migrations: 0019 creates the leaf (alignment-optimal columns, DB defaults for
  rolling-deploy safety, org-scoped list indexes); 0020 two-phase backfill
  (mandatory fixed-width columns for all issues, then abandonable fts for hot
  issues, time-budget guarded); 0021 drops Issue.search_vector; 0022 drops the
  five hot columns from Issue. 0022 is deploy-order sensitive -- drain the old
  release before applying it.
- Org-scoped reads constrain the leaf partition key so Postgres prunes the hash
  partitions (verified: 4 partitions scanned -> 1). All leaf status/count writes
  carry organization_id to prune likewise.

Prepared with AI assistance (Claude Code). Human review required.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Runtime zstd decompression is entirely in gt_rust, so the only Python-side
consumer of a zstd library was test_compression's fixture builder, which
*produces* a zstd-encoded body to POST. Stdlib zstd (PEP 784) covers that on
Python 3.14+, so the third-party zstandard dependency only existed to build the
fixture on the 3.12 CI job.

Drop the dependency outright (nothing else pulls it transitively; uv.lock loses
it on every platform) and skip test_zstd_compression when stdlib compression.zstd
is unavailable. The zstd decode path is the same gt_rust wheel on both CI jobs,
so the 3.12 skip loses no real coverage — the path is still exercised on 3.14.
When requires-python rises to >=3.14 the skip becomes dead and can go too.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(ingest): move all request-body decompression into Rust; drop middleware

See merge request glitchtip/glitchtip-backend!2388
feat: hot-split Issue into a partitioned IssueIndex (move hot columns off Issue)

See merge request glitchtip/glitchtip-backend!2191
The issue list sorts by count/last_seen, which the feat/issue-search-index
hot-split moved onto the one-to-one IssueIndex leaf (the ORM order keys are
now index__count / index__last_seen). AsyncLinkHeaderPagination inherited
the upstream _get_position_from_instance, which reads the ordering value with
a flat getattr(instance, "index__last_seen") to build the next-page cursor.
That raises AttributeError, so any issue list spanning more than one page
500s (the org issues endpoint, breaking the frontend list + its e2e specs).

Override _get_position_from_instance to walk the __ relation path. The leaf
is already select_related, so this never issues an extra query, and flat
fields (first_seen, priority annotation, dict rows) are unaffected.

Add a regression test that paginates issues sorted by the joined index
fields; it reproduces the AttributeError without the fix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fix(api): resolve cursor position through relation paths in pagination

See merge request glitchtip/glitchtip-backend!2395
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