[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
Open
[UPSTREAM CHANGES] latest changes as of
Mon Jul 27 2026 03:48:49 GMT+0000 (Coordinated Universal Time)#239github-actions[bot] wants to merge 5814 commits into
github-actions[bot] wants to merge 5814 commits into
Conversation
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>
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
… into upstream-changes-2026-07-27
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR is auto-generated by
actions/github-script.