[UPSTREAM CHANGES] latest changes as of Mon Jun 29 2026 01:15:24 GMT+0000 (Coordinated Universal Time) - #235
Open
github-actions[bot] wants to merge 5676 commits into
Open
[UPSTREAM CHANGES] latest changes as of
Mon Jun 29 2026 01:15:24 GMT+0000 (Coordinated Universal Time)#235github-actions[bot] wants to merge 5676 commits into
github-actions[bot] wants to merge 5676 commits into
Conversation
fix(event-ingest): scope invalid-DSN block cache by (project, key) See merge request glitchtip/glitchtip-backend!2322
Fix _rewrite_path for protected resource metadata See merge request glitchtip/glitchtip-backend!2302
Webhook alert recipients (generic webhook, Discord, Google Chat, ntfy, Microsoft Teams, Zulip) previously accepted any URL, including loopback, RFC1918, and cloud instance-metadata addresses. Any user with alert-create permission could point a webhook at an internal service and use a delivered alert payload as an exfiltration channel or to poke unauthenticated internal APIs. Introduce a shared url_validation module with is_ip_blocked, check_url_safe (async, for runtime), and validate_public_url (sync, for schema validation). Alerts schema rejects private-IP recipient URLs at creation time so operators see a 400 up front; each send_* transport also checks at delivery time to cover DNS rebinding and config changes. Gated by a new GLITCHTIP_ALLOW_PRIVATE_IPS env var (default False), separate from GLITCHTIP_UPTIME_ALLOW_PRIVATE_IPS: an operator may want to monitor internal services without also opening webhooks as an exfiltration channel. Uptime's existing validator is unchanged in behaviour; it now imports is_ip_blocked from the shared module so there is one source of truth.
chore(deps): update dependency boto3 to v1.42.93 See merge request glitchtip/glitchtip-backend!2325
…ault `DATA_UPLOAD_MAX_MEMORY_SIZE` was set to 4294967295 with a `# TMP REMOVE THIS` note. The value existed to paper over `DecompressBodyMiddleware`, which sets `CONTENT_LENGTH` to the same 4 GB sentinel after wrapping the request stream (the true decompressed length is unknown up front). A single request could force Django to buffer up to 4 GB before any view ran — a few concurrent requests could OOM the pod. Tie the two ends together properly: - Middleware now advertises `GLITCHTIP_MAX_UNZIPPED_PAYLOAD_SIZE` as the `CONTENT_LENGTH` sentinel. The decoder already enforces that cap at read time, so the advertised length matches the real hard ceiling. - `DATA_UPLOAD_MAX_MEMORY_SIZE` default becomes `GLITCHTIP_MAX_UNZIPPED_PAYLOAD_SIZE + 1 MB`, env-overridable, so raising the unzipped cap via env automatically widens the Django check to match. - Constants reordered so the dependent default compiles cleanly. Multipart file uploads (minidumps, source-map chunks) go through `FILE_UPLOAD_MAX_MEMORY_SIZE` and spill to disk, so this limit does not need to cover them.
The envelope event path checked `isinstance(item.user, dict)` before setting `user.ip_address = client_ip`, but `WebIngestIssueEvent.user` is always an `EventUser` pydantic model or None — never a dict. The branch never fired, so payload-supplied `user.ip_address` passed through unchanged even with `scrub_ip_addresses=True` (the project / organization setting anonymizes the connection IP but does not touch payload-supplied IPs). Mirror the `/store/` and `/security/` handlers: when the connection IP is available, overwrite the payload field or construct a fresh `EventUser`. The connection IP is already anonymized upstream in `get_ip_address()` when scrubbing is enabled, so this restores the intended privacy behavior. Adds regression tests covering both scrub states and the case where the payload does not include a `user` object. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The DNS rebinding test patches `BaseEventLoop.getaddrinfo` with a stub that must accept the same positional and keyword arguments as the real method but does not use them. Rename to the conventional `*_, **__` to satisfy `vulture --min-confidence 100`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
OAuth refresh tokens and paired access tokens were previously stored in plaintext (DB column for the refresh token, cache key for the access token). A read-only snapshot of either store yielded directly usable credentials. Store only digests at rest: * ``OAuthRefreshToken.token`` and ``access_token_key`` are replaced with ``token_prefix`` (indexed, first 8 chars) + ``token_digest`` and ``access_token_digest`` (SHA-256 hex). Lookup by prefix + a constant-time digest compare. * ``_access_cache_key()`` now hashes the token string into the key, so the Valkey keyspace no longer contains access-token plaintext either. * SHA-256 is sufficient here: inputs are 256 random bits from ``generate_token()``, so a KDF adds no value. The migration truncates ``OAuthRefreshToken`` because the old columns held the only plaintext we could have rehashed. Connected OAuth clients will see their next access-token exchange fail and transparently re-authorize via the OAuth flow. Access tokens are cache-backed and expire within 8h regardless. Minor behavioural change: client-initiated revocation via ``revoke_token(RefreshToken)`` can no longer proactively purge the paired access cache entry (the digest is one-way). The cache entry expires naturally; a simultaneous ``revoke_token(AccessToken)`` from the client still purges it because the caller supplies the plaintext. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix(settings): replace 4 GB DATA_UPLOAD_MAX workaround with sized default See merge request glitchtip/glitchtip-backend!2326
fix(alerts): validate outbound webhook URLs to block SSRF See merge request glitchtip/glitchtip-backend!2324
refactor(event-ingest): apply client IP to envelope events consistently See merge request glitchtip/glitchtip-backend!2328
chore(deps): update dependency boto3 to v1.42.94 See merge request glitchtip/glitchtip-backend!2331
chore(deps): update dependency symbolic to v12.18.3 See merge request glitchtip/glitchtip-backend!2332
…faults Two related hardening changes that preserve out-of-the-box deployment behaviour across k8s, reverse-proxied, and single-host topologies: 1. SESSION_COOKIE_SECURE and CSRF_COOKIE_SECURE default based on GLITCHTIP_URL.scheme. https deployments get Secure cookies with zero operator action; http deployments (local, internal LAN) still let users log in without manual config. Both remain env-overridable in either direction. 2. Startup RuntimeWarnings (stderr, not hard failures) when DEBUG=False and either SECRET_KEY is the placeholder "change_me" or ALLOWED_HOSTS is the wildcard default. Escalation to a hard boot-time check is deferred to a major release so existing self-hosters aren't broken on upgrade. ALLOWED_HOSTS must stay wildcard-by-default because real deployments need Service DNS + Ingress host + pod IP + LB healthcheck name to work without orchestration changes; similarly SECURE_SSL_REDIRECT and HSTS stay opt-in because TLS typically terminates upstream. Those move to docs rather than defaults.
Having both `glitchtip/tests.py` and `glitchtip/tests/` in the same Python package confuses unittest discovery with "'tests' module incorrectly imported" on case-sensitive filesystems. Move the deploy-defaults test classes back into the single `glitchtip/tests.py` module, renaming the local helpers to avoid colliding with any neighbours. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix(settings): derive cookie Secure flags from URL; warn on unsafe defaults See merge request glitchtip/glitchtip-backend!2327
chore(deps): update dependency boto3 to v1.42.95 See merge request glitchtip/glitchtip-backend!2334
chore(deps): update dependency granian to v2.7.4 See merge request glitchtip/glitchtip-backend!2335
The access-token cache is keyed by sha256(plaintext), and the paired refresh-token row already stores that same sha256 in access_token_digest. Reconstructing the cache key from the digest preserves the original revocation semantics without needing the plaintext: both exchange_refresh_token and revoke_token(RefreshToken) can again delete the paired cache entry immediately rather than waiting out ACCESS_TOKEN_LIFETIME. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
chore(deps): update dependency boto3 to v1.42.96 See merge request glitchtip/glitchtip-backend!2337
chore(deps): update dependency ruff to v0.15.12 See merge request glitchtip/glitchtip-backend!2338
chore(deps): update dependency ipython to v9.13.0 See merge request glitchtip/glitchtip-backend!2339
The async fixture migration left 21 tests raising SynchronousOnlyOperation (plus dead/duplicate code in test_search). Finish the conversion: - Add async fixture helpers `amake_issue` / `arefresh_issue` that bake the Issue and cache its IssueIndex leaf, so leaf-backed proxy reads (count/status/level/last_release) don't hit the sync ORM from async tests. - Await IssueIndex updates (`.update`->`.aupdate`), make `_set_search_document` async, and await missed async test-client calls. - Replace post-refresh proxy reads with `arefresh_issue`; read merged/bulk status off the leaf via async queries. - Rebuild `test_search`: drop the duplicate definition, the `baker.maake` typo, and references to the removed `Issue.search_vector` /undefined `PipeConcat`/`F`; route through the IssueIndex.fts_document path. - teams: wrap sync `add_user`/`force_login` in `sync_to_async`, use the async client for the member login, and `aexists()` the final assertion. All 515 previously-modified tests pass; ruff clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
chore(deps): update dependency symbolic to ~=13.4.0 See merge request glitchtip/glitchtip-backend!2419
The partition-skip tests assert on INFO/WARNING records via assertLogs, but the test settings call logging.disable(logging.WARNING) (settings.py, the TESTING-without-BILLING_ENABLED branch). That global disable suppresses INFO and WARNING regardless of assertLogs's own handler, so both test_skips_when_existing_modulus_differs and test_skips_when_children_have_no_hash_modulus failed in CI with "no logs of level ... triggered" (they passed locally only because BILLING_ENABLED was set, skipping the disable). Add a _assert_partition_logs helper that temporarily lifts the global disable around the assertLogs block and restores it afterwards. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
chore(deps): switch test fixtures to model-bakery-async fork See merge request glitchtip/glitchtip-backend!2414
chore(deps): update dependency symbolic to ~=13.5.0 See merge request glitchtip/glitchtip-backend!2423
fix(partitions): skip hash-child creation when existing modulus differs from configured bucket count See merge request glitchtip/glitchtip-backend!2417
feat: server-side PII scrubbing at ingest Closes #315 See merge request glitchtip/glitchtip-backend!2403
Also remove redundant tf.seek(0) after creating the combined blob.
Notification.send_notifications() awaited each recipient's send() one at a time in an async for loop. Every send is an independent outbound POST (Slack, Discord, Teams, ntfy, etc. webhooks, each with its own ClientSession and 10s timeout) or a thread-bridged email -- there is no data dependency between recipients, so the sequential await made the total cost the sum of every recipient's round-trip. A single slow or timing-out destination delayed all the others. Collect the recipients and asyncio.gather their sends instead, so the wall time is the slowest single recipient rather than the sum. return_exceptions keeps one unexpected failure from blocking the rest (the individual webhook senders already swallow timeouts/client errors and return None). The no- recipient fallback email and the trailing is_sent/asave are unchanged. Also document the same-connection serialization of the gather in stripe/api.py's daily-statistics collection: that gather reads tidily as concurrent but same-alias async ORM calls share one connection, so it does not parallelize at the DB. Comment added so a future reader does not assume query parallelism that isn't there. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds DATABASE_ENGINE (env), defaulting to the current async-backend (psycopg) so production behavior is unchanged. Set DATABASE_ENGINE=gt_rust.django_backend to run the ORM on gt_rust's Rust Postgres driver — one shared tokio pool serving sync + async, on the same runtime as the valkey driver. - settings.py: ENGINE now comes from DATABASE_ENGINE (default unchanged). - tests.py: DatabaseSettingsTestCase accepts either the async-backend or the gt_rust engine (still guards against SQLite fallback / typos). Validated locally: full suite runs under DATABASE_ENGINE=gt_rust.django_backend (1129 tests; all pass except a pre-existing batched-delete lock-count check under investigation — see the gt_rust MR). Requires glitchtip-rust >= 0.3.0 (the release that ships the Postgres surface) for the switch to resolve; the default path needs nothing new. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
pg_locks is cluster-wide, so the bare `SELECT count(*) FROM pg_locks` poller summed every parallel test worker's locks (each worker runs in its own test DB). Under `--parallel` that conflates unrelated concurrent tests and makes the 2000 threshold depend on the DB driver's connection/concurrency footprint rather than on whether *this* org delete batches correctly. Empirically, a single org delete peaks at ~1000 relation locks under both psycopg and the gt_rust driver (identical per-operation batching — verified with a per-table pg_locks breakdown); only the cluster-wide sum diverged. Scoping the poller to current_database() measures the batching we actually care about and is robust to parallelism and driver choice. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Catches a high-severity, easy-to-miss class of bug: a read executing on a different connection than the active transaction. Bites any layer that manages connections outside Django's view — a Rust "fused" read checking out a fresh pool connection (which can't see uncommitted writes, and deadlocks a size-1 test pool), or an async read landing on a different async connection than the write (the same divergence django-async-backend can exhibit). Invariant: write a row in a transaction, then every read shape (exists/values_list/values/list(qs)) must see it — else it's on the wrong connection. Sync (TestCase) + async (async_atomic) variants. Passes on psycopg and on the gt_rust engine; designed to fail loudly for any engine that breaks the connection/transaction contract. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
0.3.0 is the release that ships gt_rust.django_backend (the Postgres driver surface). Bumping the pin so the deployed image contains it, enabling DATABASE_ENGINE=gt_rust.django_backend. Relock also corrects symbolic 13.3.1->13.5.0: master's lock held 13.3.1 while pyproject pins symbolic~=13.5.0, so any lock regen satisfies the constraint. Unrelated to the engine switch but required for a consistent lockfile. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(db): opt-in Rust Postgres engine (DATABASE_ENGINE switch) + guards See merge request glitchtip/glitchtip-backend!2399
fix(files): assemble multi-chunk blobs correctly See merge request glitchtip/glitchtip-backend!2415
…equest Sentry's Django integration binds the deprecated asyncio.iscoroutinefunction alias in django/views.py and calls it once per request in sentry_patched_make_view_atomic. On Python 3.14 each call runs the warnings machinery (frame inspection + message formatting) even when the DeprecationWarning is filtered out, allocating transient heap and burning CPU on every request to produce output that is never logged. Point that one module-level name at inspect.iscoroutinefunction instead of mutating the asyncio module process-wide. This is what Sentry already does in django/asgi.py and what upstream did for the other integrations; only django/views.py was missed (getsentry/sentry-python#6085). Measured in isolation (50k calls, CPython 3.14.5, DeprecationWarning ignored): deprecated alias: 942 B/call churn, 1397 ns/call inspect: 0 B/call, 319 ns/call Also explicitly enable AsyncioIntegration for asyncio task tracing. Refs getsentry/sentry-python#6085 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tems LogItemSchema is a plain pydantic BaseModel, so its .dict() is the deprecated Pydantic v2 alias. It is called once per log item on the log-ingest hot path (otlp.py building OTLP items, views.py scrubbing each log), and on pydantic 2.x each call emits a PydanticDeprecatedSince20 warning. Even with the warning filtered out and never logged, that runs the warnings machinery (frame inspection + message formatting) on every item, churning heap for no output. Switch both call sites to model_dump(), the exact non-deprecated equivalent that .dict() simply forwards to. django-ninja's Schema.dict() is a separate, non-deprecated override (verified against ninja 1.6.2) and is left as-is, so this is scoped to the pure-BaseModel LogItemSchema calls only. Verified via the test suite under `-W always`: the PydanticDeprecatedSince20 "dict" warnings from these two sites drop to zero with all log and ingest tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Same deprecated pydantic BaseModel.dict() call as otlp.py/views.py, on the self-referencing (InternalTransport) log loopback path: log_payload.items are LogItemSchema (a plain BaseModel), so .dict() is the deprecated v2 alias that churns the warnings machinery per item. Switch to model_dump(). This path is only exercised when SENTRY_DSN points back at the same instance, which is why the earlier normal-transport test sweep didn't surface it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
perf(sentry): reduce per-request churn on Python 3.14 + enable AsyncioIntegration See merge request glitchtip/glitchtip-backend!2427
perf(ingest): use model_dump() instead of deprecated pydantic .dict() on log items See merge request glitchtip/glitchtip-backend!2428
perf(alerts): fan out notification recipients concurrently See merge request glitchtip/glitchtip-backend!2425
… into upstream-changes-2026-06-29
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.