diff --git a/tools/lib/capitalization.py b/tools/lib/capitalization.py index 543bba4aeb..bf9c5de5bd 100644 --- a/tools/lib/capitalization.py +++ b/tools/lib/capitalization.py @@ -15,6 +15,7 @@ IGNORED_PHRASES = [ r"Botserver", r"Cookie Bot", r"DevAuthBackend", + r"DSN", r"GCM", r"GitHub", r"Gravatar", diff --git a/tools/test-backend b/tools/test-backend index 24613d422c..f887e6ac50 100755 --- a/tools/test-backend +++ b/tools/test-backend @@ -137,6 +137,8 @@ not_yet_fully_covered = [ "zerver/webhooks/teamcity/view.py", "zerver/webhooks/travis/view.py", "zerver/webhooks/zapier/view.py", + # This is hard to get test coverage for, and low value to do so + "zerver/views/sentry.py", # Cannot have coverage, as tests run in a transaction "zerver/lib/safe_session_cached_db.py", "zerver/lib/singleton_bmemcached.py", diff --git a/web/src/sentry.ts b/web/src/sentry.ts index 16ae241d8a..6009a34195 100644 --- a/web/src/sentry.ts +++ b/web/src/sentry.ts @@ -47,6 +47,7 @@ if (page_params.server_sentry_dsn) { Sentry.init({ dsn: page_params.server_sentry_dsn, environment: page_params.server_sentry_environment || "development", + tunnel: "/error_tracing", release: "zulip-server@" + ZULIP_VERSION, integrations: [ diff --git a/zerver/views/sentry.py b/zerver/views/sentry.py new file mode 100644 index 0000000000..0a66c683b6 --- /dev/null +++ b/zerver/views/sentry.py @@ -0,0 +1,41 @@ +import urllib + +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 zerver.lib.exceptions import JsonableError +from zerver.lib.outgoing_http import OutgoingSession +from zerver.lib.validator import ( + check_url, + to_wild_value, +) + + +class SentryTunnelSession(OutgoingSession): + def __init__(self) -> None: + super().__init__(role="sentry_tunnel", timeout=5) + + +@csrf_exempt +def sentry_tunnel( + request: HttpRequest, +) -> HttpResponse: + try: + envelope = request.body + header = to_wild_value("envelope", envelope.split(b"\n")[0].decode("utf-8")) + dsn = urllib.parse.urlparse(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() + SentryTunnelSession().post( + url=url, data=envelope, headers={"Content-Type": "application/x-sentry-envelope"} + ).raise_for_status() + return HttpResponse(status=200) diff --git a/zproject/urls.py b/zproject/urls.py index 18a6175e66..5168892473 100644 --- a/zproject/urls.py +++ b/zproject/urls.py @@ -137,6 +137,7 @@ from zerver.views.report import ( report_send_times, report_unnarrow_times, ) +from zerver.views.sentry import sentry_tunnel from zerver.views.storage import get_storage, remove_storage, update_storage from zerver.views.streams import ( add_default_stream, @@ -790,6 +791,10 @@ urls += [ path("scim/v2/", include("django_scim.urls", namespace="scim")), ] +# Front-end Sentry requests tunnel through the server, if enabled +if settings.SENTRY_FRONTEND_DSN: + urls += [path("error_tracing", sentry_tunnel)] + # User documentation site help_documentation_view = MarkdownDirectoryView.as_view( template_name="zerver/documentation_main.html",