snapshot of https://github.com/zulip/zulip.git @ 48c69911c7dd4e03b20552eb3bd77ad8151c1357 (2026-09-15)
Some checks failed
Code scanning / CodeQL (push) Failing after 15s
Zulip production suite / Ubuntu 22.04 production build (push) Failing after 26s
Zulip production suite / Debian 12 production install with custom db name and user (push) Has been skipped
Zulip production suite / Ubuntu 22.04 production install and PostgreSQL upgrade with pgroonga (push) Has been skipped
Zulip production suite / Ubuntu 24.04 production install (push) Has been skipped
Zulip production suite / Ubuntu 26.04 production install (push) Has been skipped
Zulip production suite / Debian 13 production install (push) Has been skipped
Zulip production suite / 7.0 Version Upgrade (push) Has been skipped
Zulip production suite / 8.0 Version Upgrade (push) Has been skipped
Zulip production suite / 6.0 Version Upgrade (push) Has been skipped
Zulip production suite / 10.0 Version Upgrade (push) Has been skipped
Zulip production suite / 9.0 Version Upgrade (push) Has been skipped
Zulip production suite / 12.0 Version Upgrade (push) Has been skipped
Zulip production suite / 11.0 Version Upgrade (push) Has been skipped
GitHub Actions Security Analysis with zizmor / zizmor (push) Failing after 3s
Zulip CI / Debian 12 (Python 3.11, backend + documentation) (push) Failing after 26s
Zulip CI / Ubuntu 22.04 (Python 3.10, backend + frontend) (push) Failing after 3s
Zulip CI / Ubuntu 24.04 (Python 3.12, backend) (push) Failing after 25s
Zulip CI / Ubuntu 26.04 (Python 3.14, backend) (push) Failing after 25s
Zulip CI / Debian 13 (Python 3.13, backend) (push) Failing after 28s
Zulip production suite / Required jobs (push) Has been cancelled
Zulip CI / Required jobs (push) Has been cancelled
API Documentation Update Check / check-feature-level-updated (push) Has been cancelled
API Documentation Update Check / notify-if-api-docs-changed (push) Has been cancelled
Some checks failed
Code scanning / CodeQL (push) Failing after 15s
Zulip production suite / Ubuntu 22.04 production build (push) Failing after 26s
Zulip production suite / Debian 12 production install with custom db name and user (push) Has been skipped
Zulip production suite / Ubuntu 22.04 production install and PostgreSQL upgrade with pgroonga (push) Has been skipped
Zulip production suite / Ubuntu 24.04 production install (push) Has been skipped
Zulip production suite / Ubuntu 26.04 production install (push) Has been skipped
Zulip production suite / Debian 13 production install (push) Has been skipped
Zulip production suite / 7.0 Version Upgrade (push) Has been skipped
Zulip production suite / 8.0 Version Upgrade (push) Has been skipped
Zulip production suite / 6.0 Version Upgrade (push) Has been skipped
Zulip production suite / 10.0 Version Upgrade (push) Has been skipped
Zulip production suite / 9.0 Version Upgrade (push) Has been skipped
Zulip production suite / 12.0 Version Upgrade (push) Has been skipped
Zulip production suite / 11.0 Version Upgrade (push) Has been skipped
GitHub Actions Security Analysis with zizmor / zizmor (push) Failing after 3s
Zulip CI / Debian 12 (Python 3.11, backend + documentation) (push) Failing after 26s
Zulip CI / Ubuntu 22.04 (Python 3.10, backend + frontend) (push) Failing after 3s
Zulip CI / Ubuntu 24.04 (Python 3.12, backend) (push) Failing after 25s
Zulip CI / Ubuntu 26.04 (Python 3.14, backend) (push) Failing after 25s
Zulip CI / Debian 13 (Python 3.13, backend) (push) Failing after 28s
Zulip production suite / Required jobs (push) Has been cancelled
Zulip CI / Required jobs (push) Has been cancelled
API Documentation Update Check / check-feature-level-updated (push) Has been cancelled
API Documentation Update Check / notify-if-api-docs-changed (push) Has been cancelled
This commit is contained in:
124
zerver/views/sentry.py
Normal file
124
zerver/views/sentry.py
Normal file
@@ -0,0 +1,124 @@
|
||||
import logging
|
||||
from contextlib import suppress
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
import orjson
|
||||
from django.conf import settings
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from pybreaker import CircuitBreaker, CircuitBreakerError
|
||||
from requests.exceptions import HTTPError, ProxyError, RequestException, Timeout
|
||||
from sentry_sdk.integrations.logging import ignore_logger
|
||||
|
||||
from zerver.lib.exceptions import JsonableError
|
||||
from zerver.lib.outgoing_http import OutgoingSession
|
||||
from zerver.lib.validator import check_url, to_wild_value
|
||||
|
||||
# In order to not overload Sentry if it's having a bad day, we tell
|
||||
# Sentry to ignore exceptions that we have when talking to Sentry.
|
||||
logger = logging.getLogger(__name__)
|
||||
ignore_logger(logger.name)
|
||||
|
||||
|
||||
class SentryTunnelSession(OutgoingSession):
|
||||
def __init__(self) -> None:
|
||||
super().__init__(role="sentry_tunnel", timeout=1)
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
def sentry_tunnel(
|
||||
request: HttpRequest,
|
||||
) -> HttpResponse:
|
||||
try:
|
||||
envelope_header_line, envelope_items = request.body.split(b"\n", 1)
|
||||
envelope_header = to_wild_value("envelope_header", envelope_header_line.decode("utf-8"))
|
||||
dsn = urlsplit(envelope_header["dsn"].tame(check_url))
|
||||
except Exception:
|
||||
raise JsonableError(_("Invalid request format"))
|
||||
|
||||
if dsn.geturl() != settings.SENTRY_FRONTEND_DSN:
|
||||
raise JsonableError(_("Invalid DSN"))
|
||||
|
||||
assert dsn.hostname
|
||||
project_id = dsn.path.strip("/")
|
||||
url = dsn._replace(netloc=dsn.hostname, path=f"/api/{project_id}/envelope/").geturl()
|
||||
|
||||
# Adjust the payload to explicitly contain the IP address of the
|
||||
# user we see. If left blank, Sentry will assume the IP it
|
||||
# received the request from, which is Zulip's, which can make
|
||||
# debugging more complicated.
|
||||
updated_body = request.body
|
||||
# If we fail to update the body for any reason, leave it as-is; it
|
||||
# is better to misreport the IP than to drop the report entirely.
|
||||
with suppress(Exception):
|
||||
# This parses the Sentry ingestion format, known as an
|
||||
# Envelope. See https://develop.sentry.dev/sdk/envelopes/ for
|
||||
# spec.
|
||||
parts = [envelope_header_line, b"\n"]
|
||||
while envelope_items != b"":
|
||||
item_header_line, rest = envelope_items.split(b"\n", 1)
|
||||
parts.append(item_header_line)
|
||||
parts.append(b"\n")
|
||||
item_header = orjson.loads(item_header_line.decode("utf-8"))
|
||||
length = item_header.get("length")
|
||||
if length is None:
|
||||
item_body, envelope_items = [*rest.split(b"\n", 1), b""][:2]
|
||||
else:
|
||||
item_body, envelope_items = rest[0:length], rest[length:]
|
||||
if item_header.get("type") in ("transaction", "event"):
|
||||
# Event schema:
|
||||
# https://develop.sentry.dev/sdk/event-payloads/#core-interfaces
|
||||
# https://develop.sentry.dev/sdk/event-payloads/user/
|
||||
#
|
||||
# Transaction schema:
|
||||
# https://develop.sentry.dev/sdk/event-payloads/transaction/#anatomy
|
||||
# Note that "Transactions are Events enriched with Span data."
|
||||
payload_data = orjson.loads(item_body)
|
||||
if "user" in payload_data:
|
||||
payload_data["user"]["ip_address"] = request.META.get("REMOTE_ADDR")
|
||||
item_body = orjson.dumps(payload_data)
|
||||
parts.append(item_body)
|
||||
if length is None:
|
||||
parts.append(b"\n")
|
||||
updated_body = b"".join(parts)
|
||||
|
||||
try:
|
||||
sentry_request(url, updated_body)
|
||||
except CircuitBreakerError:
|
||||
logger.warning("Dropped a client exception due to circuit-breaking")
|
||||
except RequestException:
|
||||
# This logger has been configured, above, to not report to Sentry
|
||||
logger.exception("Error while reporting to Sentry")
|
||||
return HttpResponse(status=200)
|
||||
|
||||
|
||||
# Circuit-break and temporarily stop trying to report to
|
||||
# Sentry if it keeps timing out. We include ProxyError in
|
||||
# here because we are likely making our requests through
|
||||
# Smokescreen as a CONNECT proxy, so failures from Smokescreen
|
||||
# failing to connect at the TCP level will report as
|
||||
# ProxyErrors.
|
||||
def open_circuit_for(exc_value: Exception) -> bool:
|
||||
if isinstance(exc_value, ProxyError | Timeout):
|
||||
return True
|
||||
if isinstance(exc_value, HTTPError):
|
||||
response = exc_value.response
|
||||
if response.status_code == 429 or response.status_code >= 500:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
# Open the circuit after 2 failures, and leave it open for 30s.
|
||||
@CircuitBreaker(
|
||||
fail_max=2,
|
||||
reset_timeout=30,
|
||||
name="Sentry tunnel",
|
||||
exclude=(open_circuit_for,),
|
||||
)
|
||||
def sentry_request(url: str, data: bytes) -> None:
|
||||
SentryTunnelSession().post(
|
||||
url=url,
|
||||
data=data,
|
||||
headers={"Content-Type": "application/x-sentry-envelope"},
|
||||
).raise_for_status()
|
||||
Reference in New Issue
Block a user