Skip to content

[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
masterfrom
upstream-changes-2026-06-29
Open

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

Conversation

@github-actions

Copy link
Copy Markdown

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

bufke and others added 30 commits April 21, 2026 20:17
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
Woodsii and others added 30 commits June 23, 2026 17:04
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
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