sentry: Untangle from page_params.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
This commit is contained in:
Anders Kaseorg 2024-06-22 16:32:19 -07:00 committed by Anders Kaseorg
parent 3db05666ac
commit f7eecb0e03
8 changed files with 92 additions and 102 deletions

View File

@ -23,6 +23,10 @@
{% include 'zerver/meta_tags.html' %} {% include 'zerver/meta_tags.html' %}
{% endif %} {% endif %}
{% if sentry_params is defined %}
<script id="sentry-params" type="text/json">{{ sentry_params|tojson }}</script>
{% endif %}
{% block webpack %} {% block webpack %}
{% for filename in webpack_entry(entrypoint) -%} {% for filename in webpack_entry(entrypoint) -%}
{% if filename.endswith(".css") -%} {% if filename.endswith(".css") -%}

View File

@ -10,12 +10,7 @@ const default_params_schema = z.object({
page_type: z.literal("default"), page_type: z.literal("default"),
development_environment: z.boolean(), development_environment: z.boolean(),
google_analytics_id: z.optional(z.string()), google_analytics_id: z.optional(z.string()),
realm_sentry_key: z.optional(z.string()),
request_language: z.string(), request_language: z.string(),
server_sentry_dsn: z.nullable(z.string()),
server_sentry_environment: z.optional(z.string()),
server_sentry_sample_rate: z.optional(z.number()),
server_sentry_trace_rate: z.optional(z.number()),
}); });
// These parameters are sent in #page-params for both users and spectators. // These parameters are sent in #page-params for both users and spectators.

View File

@ -1,8 +1,5 @@
import * as Sentry from "@sentry/browser"; import * as Sentry from "@sentry/browser";
import assert from "minimalistic-assert"; import {z} from "zod";
import {page_params} from "./base_page_params";
import {current_user, realm} from "./state_data";
type UserInfo = { type UserInfo = {
id?: string; id?: string;
@ -10,13 +7,22 @@ type UserInfo = {
role?: string; role?: string;
}; };
const sentry_key = const sentry_params_schema = z.object({
// No parameter is the portico pages, empty string is the empty realm dsn: z.string(),
page_params.realm_sentry_key === undefined environment: z.string(),
? "www" realm_key: z.string(),
: page_params.realm_sentry_key === "" sample_rate: z.number(),
? "(root)" server_version: z.string(),
: page_params.realm_sentry_key; trace_rate: z.number(),
user: z.object({id: z.number(), role: z.string()}).optional(),
});
const sentry_params_json =
window.document?.querySelector("script#sentry-params")?.textContent ?? undefined;
const sentry_params =
sentry_params_json === undefined
? undefined
: sentry_params_schema.parse(JSON.parse(sentry_params_json));
export function normalize_path(path: string, is_portico = false): string { export function normalize_path(path: string, is_portico = false): string {
if (path === undefined) { if (path === undefined) {
@ -39,36 +45,7 @@ export function shouldCreateSpanForRequest(url: string): boolean {
return parsed.pathname !== "/json/events"; return parsed.pathname !== "/json/events";
} }
export function initialize(): void { if (sentry_params !== undefined) {
// The current_user and realm structures are not available until this is
// called from ui_init.initialize_everything.
assert(page_params.page_type === "home");
const user_role = current_user.is_owner
? "Organization owner"
: current_user.is_admin
? "Organization administrator"
: current_user.is_moderator
? "Moderator"
: current_user.is_guest
? "Guest"
: page_params.is_spectator
? "Spectator"
: current_user.user_id
? "Member"
: "Logged out";
const user_info: UserInfo = {realm: sentry_key, role: user_role};
if (current_user.user_id) {
user_info.id = current_user.user_id.toString();
}
Sentry.setTags({
user_role,
server_version: realm.zulip_version,
});
Sentry.setUser(user_info);
}
if (page_params.server_sentry_dsn) {
const sample_rates = new Map([ const sample_rates = new Map([
// This is controlled by shouldCreateSpanForRequest, above, but also put here for consistency // This is controlled by shouldCreateSpanForRequest, above, but also put here for consistency
["call GET /json/events", 0], ["call GET /json/events", 0],
@ -78,8 +55,8 @@ if (page_params.server_sentry_dsn) {
]); ]);
Sentry.init({ Sentry.init({
dsn: page_params.server_sentry_dsn, dsn: sentry_params.dsn,
environment: page_params.server_sentry_environment ?? "development", environment: sentry_params.environment,
tunnel: "/error_tracing", tunnel: "/error_tracing",
release: "zulip-server@" + ZULIP_VERSION, release: "zulip-server@" + ZULIP_VERSION,
@ -90,25 +67,34 @@ if (page_params.server_sentry_dsn) {
return { return {
...context, ...context,
metadata: {source: "custom"}, metadata: {source: "custom"},
name: normalize_path(window.location.pathname, sentry_key === "www"), name: normalize_path(
window.location.pathname,
sentry_params.realm_key === "www",
),
}; };
}, },
shouldCreateSpanForRequest, shouldCreateSpanForRequest,
}), }),
], ],
sampleRate: page_params.server_sentry_sample_rate ?? 0, sampleRate: sentry_params.sample_rate,
tracesSampler(samplingContext) { tracesSampler(samplingContext) {
const base_rate = page_params.server_sentry_trace_rate ?? 0; const base_rate = sentry_params.trace_rate;
const name = samplingContext.transactionContext.name; const name = samplingContext.transactionContext.name;
return base_rate * (sample_rates.get(name) ?? 1); return base_rate * (sample_rates.get(name) ?? 1);
}, },
initialScope(scope) { initialScope(scope) {
const user_role = sentry_params.user?.role ?? "Logged out";
const user_info: UserInfo = { const user_info: UserInfo = {
realm: sentry_key, realm: sentry_params.realm_key,
role: user_role,
}; };
if (sentry_params.user !== undefined) {
user_info.id = sentry_params.user.id.toString();
}
scope.setTags({ scope.setTags({
realm: sentry_key, realm: sentry_params.realm_key,
user_role: "Browser", server_version: sentry_params.server_version,
user_role,
}); });
scope.setUser(user_info); scope.setUser(user_info);
return scope; return scope;

View File

@ -103,7 +103,6 @@ import * as scheduled_messages_ui from "./scheduled_messages_ui";
import * as scroll_bar from "./scroll_bar"; import * as scroll_bar from "./scroll_bar";
import * as scroll_util from "./scroll_util"; import * as scroll_util from "./scroll_util";
import * as search from "./search"; import * as search from "./search";
import * as sentry from "./sentry";
import * as server_events from "./server_events"; import * as server_events from "./server_events";
import * as settings from "./settings"; import * as settings from "./settings";
import * as settings_data from "./settings_data"; import * as settings_data from "./settings_data";
@ -414,7 +413,6 @@ export function initialize_everything(state_data) {
set_current_user(state_data.current_user); set_current_user(state_data.current_user);
set_realm(state_data.realm); set_realm(state_data.realm);
sentry.initialize();
/* To store theme data for spectators, we need to initialize /* To store theme data for spectators, we need to initialize
user_settings before setting the theme. Because information user_settings before setting the theme. Because information

View File

@ -6,6 +6,7 @@ from django.http import HttpRequest
from django.utils.html import escape from django.utils.html import escape
from django.utils.safestring import SafeString from django.utils.safestring import SafeString
from django.utils.translation import get_language from django.utils.translation import get_language
from django.utils.translation import override as override_language
from version import ( from version import (
LATEST_MAJOR_VERSION, LATEST_MAJOR_VERSION,
@ -168,17 +169,8 @@ def zulip_default_context(request: HttpRequest) -> Dict[str, Any]:
# Sync this with default_params_schema in base_page_params.ts. # Sync this with default_params_schema in base_page_params.ts.
default_page_params: Dict[str, Any] = { default_page_params: Dict[str, Any] = {
**DEFAULT_PAGE_PARAMS, **DEFAULT_PAGE_PARAMS,
"server_sentry_dsn": settings.SENTRY_FRONTEND_DSN,
"request_language": get_language(), "request_language": get_language(),
} }
if settings.SENTRY_FRONTEND_DSN is not None:
if realm is not None:
default_page_params["realm_sentry_key"] = realm.string_id
default_page_params["server_sentry_environment"] = get_config(
"machine", "deploy_type", "development"
)
default_page_params["server_sentry_sample_rate"] = settings.SENTRY_FRONTEND_SAMPLE_RATE
default_page_params["server_sentry_trace_rate"] = settings.SENTRY_FRONTEND_TRACE_RATE
context = { context = {
"root_domain_landing_page": settings.ROOT_DOMAIN_LANDING_PAGE, "root_domain_landing_page": settings.ROOT_DOMAIN_LANDING_PAGE,
@ -217,6 +209,23 @@ def zulip_default_context(request: HttpRequest) -> Dict[str, Any]:
"corporate_enabled": corporate_enabled, "corporate_enabled": corporate_enabled,
} }
if settings.SENTRY_FRONTEND_DSN is not None:
sentry_params = {
"dsn": settings.SENTRY_FRONTEND_DSN,
"environment": get_config("machine", "deploy_type", "development"),
"realm_key": "www" if realm is None else realm.string_id or "(root)",
"sample_rate": settings.SENTRY_FRONTEND_SAMPLE_RATE,
"server_version": ZULIP_VERSION,
"trace_rate": settings.SENTRY_FRONTEND_TRACE_RATE,
}
if request.user.is_authenticated:
with override_language(None):
sentry_params["user"] = {
"id": request.user.id,
"role": request.user.get_role_name(),
}
context["sentry_params"] = sentry_params
context["PAGE_METADATA_URL"] = f"{realm_url}{request.path}" context["PAGE_METADATA_URL"] = f"{realm_url}{request.path}"
if realm is not None and realm.icon_source == realm.ICON_UPLOADED: if realm is not None and realm.icon_source == realm.ICON_UPLOADED:
context["PAGE_METADATA_IMAGE"] = urljoin(realm_url, realm_icon) context["PAGE_METADATA_IMAGE"] = urljoin(realm_url, realm_icon)

View File

@ -20,7 +20,6 @@ from zerver.lib.realm_description import get_realm_rendered_description
from zerver.lib.request import RequestNotes from zerver.lib.request import RequestNotes
from zerver.models import Message, Realm, Stream, UserProfile from zerver.models import Message, Realm, Stream, UserProfile
from zerver.views.message_flags import get_latest_update_message_flag_activity from zerver.views.message_flags import get_latest_update_message_flag_activity
from zproject.config import get_config
@dataclass @dataclass
@ -230,17 +229,8 @@ def build_page_params_for_home_page_load(
# There is no event queue for spectators since # There is no event queue for spectators since
# events support for spectators is not implemented yet. # events support for spectators is not implemented yet.
no_event_queue=user_profile is None, no_event_queue=user_profile is None,
server_sentry_dsn=settings.SENTRY_FRONTEND_DSN,
) )
if settings.SENTRY_FRONTEND_DSN is not None:
page_params["realm_sentry_key"] = realm.string_id
page_params["server_sentry_environment"] = get_config(
"machine", "deploy_type", "development"
)
page_params["server_sentry_sample_rate"] = settings.SENTRY_FRONTEND_SAMPLE_RATE
page_params["server_sentry_trace_rate"] = settings.SENTRY_FRONTEND_TRACE_RATE
page_params["state_data"] = state_data page_params["state_data"] = state_data
if narrow_stream is not None and state_data is not None: if narrow_stream is not None and state_data is not None:

View File

@ -699,6 +699,15 @@ Output:
page_params = orjson.loads(page_params_json) page_params = orjson.loads(page_params_json)
return page_params return page_params
def _get_sentry_params(self, response: "TestHttpResponse") -> Optional[Dict[str, Any]]:
doc = lxml.html.document_fromstring(response.content)
try:
script = cast(lxml.html.HtmlMixin, doc).get_element_by_id("sentry-params")
except KeyError:
return None
assert script is not None and script.text is not None
return orjson.loads(script.text)
def check_rendered_logged_in_app(self, result: "TestHttpResponse") -> None: def check_rendered_logged_in_app(self, result: "TestHttpResponse") -> None:
"""Verifies that a visit of / was a 200 that rendered page_params """Verifies that a visit of / was a 200 that rendered page_params
and not for a (logged-out) spectator.""" and not for a (logged-out) spectator."""

View File

@ -11,6 +11,7 @@ from django.test import override_settings
from django.utils.timezone import now as timezone_now from django.utils.timezone import now as timezone_now
from corporate.models import Customer, CustomerPlan from corporate.models import Customer, CustomerPlan
from version import ZULIP_VERSION
from zerver.actions.create_user import do_create_user from zerver.actions.create_user import do_create_user
from zerver.actions.realm_settings import do_change_realm_plan_type, do_set_realm_property from zerver.actions.realm_settings import do_change_realm_plan_type, do_set_realm_property
from zerver.actions.users import change_user_is_active from zerver.actions.users import change_user_is_active
@ -55,7 +56,6 @@ class HomeTest(ZulipTestCase):
"page_type", "page_type",
"promote_sponsoring_zulip", "promote_sponsoring_zulip",
"request_language", "request_language",
"server_sentry_dsn",
"show_billing", "show_billing",
"show_plans", "show_plans",
"show_remote_billing", "show_remote_billing",
@ -358,7 +358,6 @@ class HomeTest(ZulipTestCase):
"promote_sponsoring_zulip", "promote_sponsoring_zulip",
"realm_rendered_description", "realm_rendered_description",
"request_language", "request_language",
"server_sentry_dsn",
"show_billing", "show_billing",
"show_plans", "show_plans",
"show_remote_billing", "show_remote_billing",
@ -497,47 +496,47 @@ class HomeTest(ZulipTestCase):
) )
def test_sentry_keys(self) -> None: def test_sentry_keys(self) -> None:
def home_params() -> Dict[str, Any]: def sentry_params() -> Dict[str, Any] | None:
result = self._get_home_page() result = self._get_home_page()
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
return self._get_page_params(result) return self._get_sentry_params(result)
self.login("hamlet") user = self.example_user("hamlet")
page_params = home_params() self.login_user(user)
self.assertEqual(page_params["server_sentry_dsn"], None) self.assertIsNone(sentry_params())
self.assertEqual(
[], [key for key in page_params if key != "server_sentry_dsn" and "sentry" in key]
)
with self.settings(SENTRY_FRONTEND_DSN="https://aaa@bbb.ingest.sentry.io/1234"): with self.settings(SENTRY_FRONTEND_DSN="https://aaa@bbb.ingest.sentry.io/1234"):
page_params = home_params()
self.assertEqual( self.assertEqual(
page_params["server_sentry_dsn"], "https://aaa@bbb.ingest.sentry.io/1234" sentry_params(),
{
"dsn": "https://aaa@bbb.ingest.sentry.io/1234",
"environment": "development",
"realm_key": "zulip",
"sample_rate": 1.0,
"server_version": ZULIP_VERSION,
"trace_rate": 0.1,
"user": {"id": user.id, "role": "Member"},
},
) )
self.assertEqual(page_params["realm_sentry_key"], "zulip")
self.assertEqual(page_params["server_sentry_environment"], "development")
self.assertEqual(page_params["server_sentry_sample_rate"], 1.0)
self.assertEqual(page_params["server_sentry_trace_rate"], 0.1)
# Make sure these still exist for logged-out users as well # Make sure these still exist for logged-out users as well
realm = get_realm("zulip") realm = get_realm("zulip")
do_set_realm_property(realm, "enable_spectator_access", True, acting_user=None) do_set_realm_property(realm, "enable_spectator_access", True, acting_user=None)
self.logout() self.logout()
page_params = home_params() self.assertIsNone(sentry_params())
self.assertEqual(page_params["server_sentry_dsn"], None)
self.assertEqual(
[], [key for key in page_params if key != "server_sentry_dsn" and "sentry" in key]
)
with self.settings(SENTRY_FRONTEND_DSN="https://aaa@bbb.ingest.sentry.io/1234"): with self.settings(SENTRY_FRONTEND_DSN="https://aaa@bbb.ingest.sentry.io/1234"):
page_params = home_params()
self.assertEqual( self.assertEqual(
page_params["server_sentry_dsn"], "https://aaa@bbb.ingest.sentry.io/1234" sentry_params(),
{
"dsn": "https://aaa@bbb.ingest.sentry.io/1234",
"environment": "development",
"realm_key": "zulip",
"sample_rate": 1.0,
"server_version": ZULIP_VERSION,
"trace_rate": 0.1,
},
) )
self.assertEqual(page_params["realm_sentry_key"], "zulip")
self.assertEqual(page_params["server_sentry_environment"], "development")
self.assertEqual(page_params["server_sentry_sample_rate"], 1.0)
self.assertEqual(page_params["server_sentry_trace_rate"], 0.1)
def test_home_under_2fa_without_otp_device(self) -> None: def test_home_under_2fa_without_otp_device(self) -> None:
with self.settings(TWO_FACTOR_AUTHENTICATION_ENABLED=True): with self.settings(TWO_FACTOR_AUTHENTICATION_ENABLED=True):