Skip to content

[UPSTREAM CHANGES] latest changes as of Mon Jul 27 2026 03:48:49 GMT+0000 (Coordinated Universal Time) - #239

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

[UPSTREAM CHANGES] latest changes as of Mon Jul 27 2026 03:48:49 GMT+0000 (Coordinated Universal Time)#239
github-actions[bot] wants to merge 5814 commits into
masterfrom
upstream-changes-2026-07-27

Conversation

@github-actions

Copy link
Copy Markdown

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

bufke and others added 30 commits June 9, 2026 21:01
feat(quota): weight uptime checks at 0.1, matching logs

See merge request glitchtip/glitchtip-backend!2385
The events_count/period/ endpoint reported uptime checks and logs as raw
counts for past periods (periods_ago > 0) while dividing them by 10 for
the current period. Both weigh 0.1, so the per-category figures were
inconsistent between the two branches (the weighted `total` was always
correct). Divide by 10 in the past-period branch too, so every usage
endpoint reports each category's billed contribution consistently.

This change was made with AI assistance (Claude Code).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Setting the GLITCHTIP_LICENSE_KEY env var (the recommended self-hosted
setup) flips I_PAID_FOR_GLITCHTIP, so the frontend shows "Open Support
Chat" — but SupportLicense.resolved() only read the admin DB row, so the
support-link deep link came back bare (no #sub=<key>). Fall back to the
env var when the DB row is empty; the admin row still takes precedence.
test_retrieve, test_relative_event_ordering, and test_multi_page_list
created events rapid-fire and assumed creation order == id order. But
ordering (prev/next navigation and the list endpoint) sorts by id (UUIDv7),
and UUID7Helper.from_datetime fills the sub-millisecond bits randomly — so
events created in the same millisecond (~99% of the time here) sort
arbitrarily, making the ordering assertions fail almost every run. Pin
explicit, increasing ids so ordering is deterministic (matches the existing
test_cold_storage.py pattern).

No production change: across real milliseconds id order tracks time order;
only same-millisecond ties are arbitrary, which the id-based (partition-
pruning) ordering accepts by design.
test(issue-events): pin UUIDv7 ids in flaky ordering tests

See merge request glitchtip/glitchtip-backend!2390
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.
Per MR review: drop the SupportLicenseWelcome model + migration 0017 and keep
welcome-sent state on the Stripe subscription (metadata.welcome_sent) instead
of our Postgres. Stripe is already the system of record for support purchases
and the SaaS app gates nothing on the license, so persisting a row is redundant
and keeps billing PII off our disk.

- Idempotency reads metadata.welcome_sent off the webhook payload (no extra GET)
  and short-circuits before the product fetch.
- Set the flag via client.mark_welcome_sent (stripe_post bracket-merge).
- Send-then-mark (at-least-once): a dropped mark re-sends on Stripe's retry,
  replacing the record-then-send that could silently lose the only delivery.
- Failure taxonomy: transient -> re-raise (warning, Stripe retries); terminal
  no-email -> 200 + logger.error with the sub id only; residual -> Stripe's
  failing-endpoint alerts.
- Tests assert the welcome_sent mark and the alert path instead of DB rows.
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>
bufke and others added 30 commits July 16, 2026 15:14
fix: default GRANIAN_WORKERS_KILL_TIMEOUT so respawns can't pin 2x RSS

See merge request glitchtip/glitchtip-backend!2465
chore: bump granian 2.7.3 -> 2.7.9

See merge request glitchtip/glitchtip-backend!2467
- django-vtasks 2.1.2 -> 3.0.0: migrate the removed VTASKS_BATCH_QUEUES
  into VTASKS_QUEUES' consolidated dict form (ingest batching unchanged:
  count 100 / timeout 2.0). Brings run_after delayed tasks, multi-worker
  rescue safety, portable unique keys, and the embedded-worker SIGTERM
  stop. The Rust ingest producer already writes the PROTOCOL.md contract
  (run_after omitted = null per spec) — full suite incl. parity goldens
  green against 3.0.0.
- glitchtip-rust 0.6.0 -> 0.6.1: drop the dbapi.Binary shim from
  glitchtip/apps.py (constructor now ships in the wheel) and the
  empty-HOST startup warning (empty host now means the libpq unix
  socket, matching psycopg).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
chore: staging bumps — django-vtasks 3.0.0, glitchtip-rust 0.6.1

See merge request glitchtip/glitchtip-backend!2468
Set the release heading to 6.2.2 and record three fixes that landed
since 6.2.1 but were not yet in the changelog: the GRANIAN_WORKERS_KILL_TIMEOUT
default, the ASGI lifespan-shutdown wedge fix, and the MCP event-serialization
relation preload.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The e2e "no-valkey" matrix variant started failing with
`glitchtip is missing dependency valkey` once the floating `docker:29`
CI image began shipping Compose v5.3.1.

Two leaks in the override, both tolerated by older Compose:

- Compose merges long-form `depends_on` maps across files rather than
  replacing them, so listing only postgres still left the base file's
  valkey dependency on glitchtip. Use the `!override` tag to replace it.
- `deploy.replicas: 0` no longer disables the service for `up --wait`;
  v5.3.x reports a still-waited-on 0-replica service as a missing
  dependency. Exclude valkey with an inactive profile instead, which
  removes it from the project model entirely.

Verified against both Compose v5.1.3 and v5.3.1: `up --wait` exits 0 and
no valkey container is created.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The all-in-one process runs HTTP requests, the embedded vtasks worker,
and the scheduler on one event loop — and, for anything not wrapped by
Sentry's own middleware, one ambient isolation scope. Captures from the
Rust ingest shim (a raw ASGI app that bypasses Django middleware by
design) and from tasks carried whichever breadcrumbs the process last
accumulated: uptime-check SQL on an ingest anomaly event, other tasks'
queries on a task failure.

- rust_ingest: fork-and-clear an isolation scope per request (what
  Sentry's ASGI middleware does for Django-handled requests) and attach
  method/path/query + project id, which anomaly events previously
  lacked.
- vtasks_context.sentry_task_scope: fork-and-clear per task execution
  with vtasks.task/vtasks.queue tags, wired via VTASKS_TASK_CONTEXT
  (django-vtasks >= 3.1 hook; the setting is inert on older versions).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ope setup

Review fixes:

- Redact sentry_key/glitchtip_key values from the query string before
  attaching it to the isolation scope's request context. SDKs
  authenticate envelope POSTs with the DSN secret in the query string,
  and the context rides on every capture in the scope — un-redacted, a
  malformed envelope would put the customer's DSN secret into the
  operator's error tracker (which may be external). Sentry's
  EventScrubber does not scrub contexts.
- Move set_tag/set_context inside the try so the whole request body
  stays under the "unexpected failure is a reported 500, never a
  dropped connection" contract.
- Correct the comment: full clear() is deliberately stricter than
  Sentry's ASGI middleware (which clears only breadcrumbs) and also
  drops ambient user/tags set at startup.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ationale

The DSN key ships in browser bundles by Sentry's design; redaction is
hygiene (don't copy customers' keys around), not secret protection.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
feat: Add support for Scaleway email

See merge request glitchtip/glitchtip-backend!2475
- AI_POLICY.md: replace the full copy with a short stub linking the canonical
  org-wide policy, keeping the backend-specific clean-room requirements.
- .gitlab-ci.yml: include the shared ai-policy-check job so fork MRs are
  reminded to disclose AI usage (warn-only until AI_POLICY_ENFORCE=1).
- AGENTS.md: add guidance telling AI coding agents to leave the human summary
  to the contributor and to fill the AI disclosure field.

Depends on glitchtip/glitchtip!4 (adds /ci/ai-policy-check.yml and the
canonical AI_POLICY.md); merge that first or this pipeline can't resolve the
include.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
gt_rust 0.7.0 no longer carries its own Postgres driver — it embeds
django-vpg 0.3.0 (new transitive dep in the lock) on the shared tokio
runtime and pool. gt_rust.django_backend / gt_rust.dbapi are unchanged
aliases, so no settings or code changes here.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… tools

The old in-tree driver bound an oversized project id unchecked: the
f64 fallback's bit pattern went over the wire as int8, matched nothing,
and the request got its 4xx by accident. django-vpg 0.3.0's checked
binds refuse the conversion (DataError), which surfaced as a 500 in
test_oversized_project_id_falls_through_to_django. No project can
exist past BIGINT range, so reject in get_project before the query.

Also pin ruff/vulture in the lint job to the last green versions —
ruff 0.16.0 (released this week) adds rules the tree doesn't pass yet
(~900 findings); sweeping that is separate work.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The store/security routes capture project_id via re_path, so it
arrives as a str (the old annotation notwithstanding); comparing it
to an int raised TypeError -> 500 on every store request.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
chore: bump glitchtip-rust to 0.7.0 (embedded django-vpg driver)

See merge request glitchtip/glitchtip-backend!2485
Capital-S "Sentry" reads as the company (or its proprietary backend); the
scope APIs these comments describe are the MIT sentry SDK's. Also drop the
"django-vtasks >= 3.1; older versions ignore this" hedges — the pin moves
to 3.1 in the next commit, so the version history is noise to anyone
reading the released code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
3.1.0 ships VTASKS_TASK_CONTEXT, the per-task context hook this branch
wires up, so the setting stops being inert.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Per-request and per-task sentry-sdk scope isolation

See merge request glitchtip/glitchtip-backend!2476
Adopt org AI policy — stub, CI disclosure check, agent guidance

See merge request glitchtip/glitchtip-backend!2483
… fetches

GlitchTip makes server-side HTTP requests to user-supplied URLs from uptime
monitors and webhook alert recipients. Both validate the submitted URL against
private/internal IPs (default-on), but the aiohttp client that performs the
request followed redirects and re-resolved DNS at connect time, and neither the
redirect target nor the connect-time IP was re-validated.

An authenticated user could therefore point a monitor or webhook at a public URL
that 302-redirects to an internal address (e.g. 169.254.169.254 or an RFC1918
host), or use DNS rebinding, and GlitchTip would make the request on their
behalf into a network otherwise unreachable from outside (CWE-918). The webhook
case is fully blind; the uptime case leaks up/reason/response-time as an oracle.

Fix, in a new glitchtip/ssrf_protection.py:

- ValidatingResolver: an aiohttp resolver that refuses to return private/
  internal addresses, so the client only ever dials an address that has been
  validated at the moment of connect — closing the DNS-rebinding window.
- request_with_validated_redirects: disables aiohttp auto-redirects and
  re-validates every hop (including redirects to raw IP literals, which bypass
  a resolver) before dialing it. Used by uptime, which legitimately follows
  http->https redirects.
- Webhook delivery routes through one _post helper that dials via the validating
  connector and sets allow_redirects=False (webhooks have no need to follow a
  cross-host redirect). This also removes ~7x duplicated session boilerplate.

Both bypasses remain governed by the existing GLITCHTIP_ALLOW_PRIVATE_IPS /
GLITCHTIP_UPTIME_ALLOW_PRIVATE_IPS opt-outs for operators who intentionally
monitor internal assets.

Reported in work item #493. Adds regression tests for the resolver, the
redirect validator, the uptime path, and the webhook allow_redirects behavior.

This change was developed with AI assistance (Claude).

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

Applies fixes from adversarial self-review of the SSRF change:

- PORT uptime monitors used asyncio.open_connection(host), which re-resolves
  DNS at connect and bypasses the aiohttp ValidatingResolver -- leaving the
  same DNS-rebinding window the change set out to close. Resolve and validate
  once via resolve_validated_ip() and connect to the pinned IP literal.
- request_with_validated_redirects forwarded the per-request timeout to every
  hop, so a long redirect chain could run up to (max_redirects+1)x the
  configured timeout. Bound the whole chain to the ClientTimeout total, matching
  aiohttp's built-in auto-follow semantics.
- Removed the now-redundant pre-flight check_url_safe() in fetch() for HTTP
  monitors (request_with_validated_redirects already validates hop 0), dropping
  a duplicate getaddrinfo per check.

Adds tests for resolve_validated_ip (literal/hostname, rebind, pin-first-public,
unresolvable) and a PORT-monitor block test. Clarifies the CHANGELOG that
webhook delivery no longer follows redirects.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
POST /import/ is reachable by an org admin (not just superusers) and fetched a
user-supplied URL through a raw aiohttp session with redirects enabled and no
IP validation, returning the response body into the import flow — an
org-admin-reachable, non-blind SSRF of the same class as the uptime/webhook one.

Route the importer's outbound fetches through safe_session +
request_with_validated_redirects (validating the connected address and every
redirect hop), and reject internal URLs at submission time via an ImportIn
field validator. Both honor GLITCHTIP_ALLOW_PRIVATE_IPS so operators migrating
from an internal instance can still opt in.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The tree is moving to ruff 0.16, whose default rules cover combined `with`
statements (SIM117) and the `asyncio.TimeoutError` alias (UP041). Apply them
to the code this branch adds so it lands clean, and hoist the `socket` import
in the new test module to the top.
fix(ssrf): validate redirect targets and connect-time IPs on outbound fetches

See merge request glitchtip/glitchtip-backend!2482
ruff 0.16 expanded its default rule selection well beyond the E/F set the
tree was written against, so CI had been pinned to 0.15.22 to stay green.

Take the upgrade and sweep the two paths CI lints (`glitchtip/`, `apps/`):

- Autofixes: `datetime.timezone.utc` -> `datetime.UTC`, `Optional[X]` ->
  `X | None`, `dict()`/`set([...])` -> literals, combined `with` statements,
  dropped unused `noqa`s.
- By hand: implicit-`Optional` parameter defaults, `%`-format -> f-strings,
  `logger.error(..., exc_info=True)` -> `logger.exception(...)`,
  `ResolvedStacktrace(frames=[])` mutable default, a loop variable captured
  by a lambda, `subprocess.run(..., check=False)` in tests, and collapsed
  nested `if`s.
- Ignored, with rationale in `pyproject.toml`: RUF012 (Django's declarative
  class attributes), BLE001/S110 (deliberate broad excepts in ingest and
  delivery), DTZ001/005/007 (intentional naive datetimes), SIM115, and the
  EXE00x file-mode rules. django-ninja's parameter defaults are declared
  immutable via `flake8-bugbear.extend-immutable-calls`.

No behavior changes; full test suite passes.
chore(lint): adopt ruff 0.16 rule set

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

7 participants