sentry: Add frontend event monitoring.

Zulip already has integrations for server-side Sentry integration;
however, it has historically used the Zulip-specific `blueslip`
library for monitoring browser-side errors.  However, the latter sends
errors to email, as well optionally to an internal `#errors` stream.
While this is sufficient for low volumes of users, and useful in that
it does not rely on outside services, at higher volumes it is very
difficult to do any analysis or filtering of the errors.  Client-side
errors are exceptionally noisy, with many false positives due to
browser extensions or similar, so determining real real errors from a
stream of un-grouped emails or messages in a stream is quite
difficult.

Add a client-side Javascript sentry integration.  To provide useful
backtraces, this requires extending the pre-deploy hooks to upload the
source-maps to Sentry.  Additional keys are added to the non-public
API of `page_params` to control the DSN, realm identifier, and sample
rates.
This commit is contained in:
Alex Vandiver 2023-02-13 19:50:57 +00:00 committed by Tim Abbott
parent fc40d74cda
commit 8f8a9f6f04
13 changed files with 257 additions and 11 deletions

View File

@ -14,6 +14,9 @@
"@formatjs/intl": "^2.0.0",
"@giphy/js-components": "^5.0.5",
"@giphy/js-fetch-api": "^4.0.1",
"@sentry/browser": "^7.41.0",
"@sentry/integrations": "7.41.0",
"@sentry/tracing": "^7.41.0",
"@uppy/core": "^3.0.2",
"@uppy/progress-bar": "^3.0.1",
"@uppy/xhr-upload": "^3.0.2",

View File

@ -20,8 +20,10 @@ if ! SENTRY_ORG=$(crudini --get /etc/zulip/zulip.conf sentry organization); then
exit 0
fi
if ! SENTRY_PROJECT=$(crudini --get /etc/zulip/zulip.conf sentry project); then
echo "sentry: No project set! Set sentry.project in /etc/zulip/zulip.conf"
SENTRY_PROJECT=$(crudini --get /etc/zulip/zulip.conf sentry project)
SENTRY_FRONTEND_PROJECT=$(crudini --get /etc/zulip/zulip.conf sentry frontend_project)
if [ -z "$SENTRY_PROJECT" ] && [ -z "$SENTRY_FRONTEND_PROJECT" ]; then
echo "sentry: No project set! Set sentry.project and/or sentry.frontend_project in /etc/zulip/zulip.conf"
exit 0
fi

View File

@ -5,8 +5,8 @@
set -e
set -u
if ! grep -q 'SENTRY_DSN' /etc/zulip/settings.py; then
echo "sentry: No DSN configured! Set SENTRY_DSN in /etc/zulip/settings.py"
if ! grep -Eq 'SENTRY_DSN|SENTRY_FRONTEND_DSN' /etc/zulip/settings.py; then
echo "sentry: No DSN configured! Set SENTRY_DSN or SENTRY_FRONTEND_DSN in /etc/zulip/settings.py"
exit 0
fi
@ -20,8 +20,19 @@ if ! SENTRY_ORG=$(crudini --get /etc/zulip/zulip.conf sentry organization); then
exit 0
fi
if ! SENTRY_PROJECT=$(crudini --get /etc/zulip/zulip.conf sentry project); then
echo "sentry: No project set! Set sentry.project in /etc/zulip/zulip.conf"
SENTRY_PROJECT=$(crudini --get /etc/zulip/zulip.conf sentry project)
SENTRY_FRONTEND_PROJECT=$(crudini --get /etc/zulip/zulip.conf sentry frontend_project)
if [ -z "$SENTRY_PROJECT" ] && [ -z "$SENTRY_FRONTEND_PROJECT" ]; then
echo "sentry: No project set! Set sentry.project and/or sentry.frontend_project in /etc/zulip/zulip.conf"
exit 0
fi
if [ -n "$SENTRY_PROJECT" ] && ! grep -q 'SENTRY_DSN' /etc/zulip/settings.py; then
echo "sentry: sentry.project is set but SENTRY_DSN is not set in /etc/zulip/settings.py"
exit 0
fi
if [ -n "$SENTRY_FRONTEND_PROJECT" ] && ! grep -q 'SENTRY_FRONTEND_DSN' /etc/zulip/settings.py; then
echo "sentry: sentry.frontend_project is set but SENTRY_FRONTEND_DSN is not set in /etc/zulip/settings.py"
exit 0
fi
@ -51,9 +62,26 @@ echo "$SENTRY_RELEASE" >./sentry-release
echo "sentry: Creating release $SENTRY_RELEASE"
export SENTRY_AUTH_TOKEN
sentry-cli releases --org="$SENTRY_ORG" --project="$SENTRY_PROJECT" new "$SENTRY_RELEASE"
# sentry-cli only supports passing one project when making a new
# release, and we want to possibly create more than once at once. Use
# curl to make the API request.
json=$(jq -nc '{version: $ARGS.named.version,
projects: $ARGS.positional | map(select( . != ""))}' \
--arg version "$SENTRY_RELEASE" \
--args "$SENTRY_PROJECT" "$SENTRY_FRONTEND_PROJECT")
curl "https://sentry.io/api/0/organizations/$SENTRY_ORG/releases/" \
-H "Authorization: Bearer $SENTRY_AUTH_TOKEN" \
-H 'Content-Type: application/json' \
-d "$json" \
--silent -o /dev/null
if [ -n "$MERGE_BASE" ]; then
echo "sentry: Setting commit range based on merge-base to upstream of $MERGE_BASE"
sudo -u zulip --preserve-env=SENTRY_AUTH_TOKEN sentry-cli releases --org="$SENTRY_ORG" set-commits "$SENTRY_RELEASE" --commit="zulip/zulip@$MERGE_BASE"
fi
if [ -n "$SENTRY_FRONTEND_PROJECT" ]; then
echo "sentry: Uploading sourcemaps"
sentry-cli releases --org="$SENTRY_ORG" --project="$SENTRY_FRONTEND_PROJECT" files "$SENTRY_RELEASE" upload-sourcemaps static/webpack-bundles/
fi

View File

@ -147,6 +147,7 @@ EXEMPT_FILES = make_set(
"web/src/scroll_bar.js",
"web/src/search_pill_widget.js",
"web/src/sent_messages.js",
"web/src/sentry.ts",
"web/src/server_events.js",
"web/src/settings.js",
"web/src/settings_account.js",

View File

@ -48,4 +48,4 @@ API_FEATURE_LEVEL = 167
# historical commits sharing the same major version, in which case a
# minor version bump suffices.
PROVISION_VERSION = (224, 0)
PROVISION_VERSION = (224, 1)

View File

@ -1,3 +1,4 @@
import "../sentry";
import "../webpack_public_path";
import "../../debug-require";
import "../alert_popup";

View File

@ -28,14 +28,23 @@ export const page_params: {
realm_move_messages_between_streams_policy: number;
realm_name_changes_disabled: boolean;
realm_push_notifications_enabled: boolean;
realm_sentry_key: string | undefined;
realm_uri: string;
realm_user_group_edit_policy: number;
realm_waiting_period_threshold: number;
request_language: string;
server_avatar_changes_disabled: boolean;
server_name_changes_disabled: boolean;
server_sentry_dsn: string | undefined;
server_sentry_environment: string | undefined;
server_sentry_sample_rate: number | undefined;
server_sentry_trace_rate: number | undefined;
server_web_public_streams_enabled: boolean;
translation_data: Record<string, string>;
user_id: number | undefined;
webpack_public_path: string;
zulip_plan_is_not_limited: boolean;
zulip_version: string;
muted_users: {id: number; timestamp: number}[];
} = $("#page-params").remove().data("params");
const t2 = performance.now();

59
web/src/sentry.ts Normal file
View File

@ -0,0 +1,59 @@
import * as Sentry from "@sentry/browser";
import {HttpClient as HttpClientIntegration} from "@sentry/integrations";
import {BrowserTracing} from "@sentry/tracing";
import _ from "lodash";
import {page_params} from "./page_params";
type UserInfo = {
id?: string;
realm: string;
role: string;
};
if (page_params.server_sentry_dsn) {
const url_regex = new RegExp("^" + _.escapeRegExp(page_params.realm_uri) + "/");
const user_info: UserInfo = {
realm: page_params.realm_sentry_key!,
role: page_params.is_owner
? "Organization owner"
: page_params.is_admin
? "Organization administrator"
: page_params.is_moderator
? "Moderator"
: page_params.is_guest
? "Guest"
: page_params.is_spectator
? "Spectator"
: "Member",
};
if (page_params.user_id) {
user_info.id = page_params.user_id.toString();
}
Sentry.init({
dsn: page_params.server_sentry_dsn,
environment: page_params.server_sentry_environment || "development",
release: "zulip-server@" + ZULIP_VERSION,
integrations: [
new BrowserTracing({
tracePropagationTargets: [url_regex],
}),
new HttpClientIntegration({
failedRequestStatusCodes: [500, 502, 503, 504],
failedRequestTargets: [url_regex],
}),
],
allowUrls: [url_regex, page_params.webpack_public_path],
sampleRate: page_params.server_sentry_sample_rate || 0,
tracesSampleRate: page_params.server_sentry_trace_rate || 0,
initialScope: {
tags: {
realm: page_params.realm_sentry_key || "(root)",
server_version: page_params.zulip_version,
},
user: user_info,
},
});
}

View File

@ -1608,6 +1608,68 @@
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.6.tgz#cee20bd55e68a1720bdab363ecf0c821ded4cd45"
integrity sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==
"@sentry/browser@^7.41.0":
version "7.41.0"
resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.41.0.tgz#f4e789417e3037cbc9cd15f3a000064b1873b964"
integrity sha512-ZEtgTXPOHZ9/Qn42rr9ZAPTKCV6fAjyDC4FFWMGP4HoUqJqr2woRddP9O5n1jvjsoIPAFOmGzbCuZwFrPVVnpQ==
dependencies:
"@sentry/core" "7.41.0"
"@sentry/replay" "7.41.0"
"@sentry/types" "7.41.0"
"@sentry/utils" "7.41.0"
tslib "^1.9.3"
"@sentry/core@7.41.0":
version "7.41.0"
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.41.0.tgz#a4a8291ef4e65f40c28fc38318e7dae721d5d609"
integrity sha512-yT3wl3wMfPymstIZRWNjuov4xhieIEPD0z9MIW9VmoemqkD5BEZsgPuvGaVIyQVMyx61GsN4H4xd0JCyNqNvLg==
dependencies:
"@sentry/types" "7.41.0"
"@sentry/utils" "7.41.0"
tslib "^1.9.3"
"@sentry/integrations@7.41.0":
version "7.41.0"
resolved "https://registry.yarnpkg.com/@sentry/integrations/-/integrations-7.41.0.tgz#1f4ac60c3905a87023e0c3c493befced687b49ca"
integrity sha512-wyVsDxTn/lslPSt02JS4Kw5iRBau+GYst1r7z55VKBl7YJm0XCaLnGsqv68qweaK9SI7PX8rj/+GmRl8G86wOg==
dependencies:
"@sentry/types" "7.41.0"
"@sentry/utils" "7.41.0"
localforage "^1.8.1"
tslib "^1.9.3"
"@sentry/replay@7.41.0":
version "7.41.0"
resolved "https://registry.yarnpkg.com/@sentry/replay/-/replay-7.41.0.tgz#73168d659b0e78ca58574831a6672a77ce9727ee"
integrity sha512-/vxuO17AysCoBbCl9wCwjsCFBD4lEbYgfC1GJm8ayWwPU1uhvZcEx6reUwi0rEFpWYGHSHh3+gi+QsOcY/EmnQ==
dependencies:
"@sentry/core" "7.41.0"
"@sentry/types" "7.41.0"
"@sentry/utils" "7.41.0"
"@sentry/tracing@^7.41.0":
version "7.41.0"
resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-7.41.0.tgz#28f57667a13b95cb8bce5af0809e7727b645e6b7"
integrity sha512-zh1ceuwQ8NzE5n8r4y78QrYD/alJl4qlkiEX9lAL6PnLMWJkVWM02BBu+x75yPFWSSDfDA/kZ9WqKkHNdjGpDw==
dependencies:
"@sentry/core" "7.41.0"
"@sentry/types" "7.41.0"
"@sentry/utils" "7.41.0"
tslib "^1.9.3"
"@sentry/types@7.41.0":
version "7.41.0"
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.41.0.tgz#3d53432a3d7693a31b606d3083ab9203c56f5aec"
integrity sha512-4z9VdObynwd64i0VHCqkeIAHmsFzapL21qN41Brzb7jY/eGxjn/0rxInDGH+vkoE9qacGqiYfWj4vRNPLsC/bw==
"@sentry/utils@7.41.0":
version "7.41.0"
resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.41.0.tgz#54224dba668dd8c8feb0ff1b4f39938b8fdefd3b"
integrity sha512-SL+MGitvkakbkrOTb48rDuJp9GYx/veB6EOzYygh49+zwz4DGM7dD4/rvf/mVlgmXUzPgdGDgkVmxgX3nT7I7g==
dependencies:
"@sentry/types" "7.41.0"
tslib "^1.9.3"
"@sinclair/typebox@^0.25.16":
version "0.25.21"
resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.25.21.tgz#763b05a4b472c93a8db29b2c3e359d55b29ce272"
@ -6090,6 +6152,11 @@ image-palette@^2.1.0:
pxls "^2.0.0"
quantize "^1.0.2"
immediate@~3.0.5:
version "3.0.6"
resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b"
integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==
import-fresh@^3.0.0, import-fresh@^3.1.0, import-fresh@^3.2.1:
version "3.3.0"
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"
@ -6909,6 +6976,13 @@ levn@~0.3.0:
prelude-ls "~1.1.2"
type-check "~0.3.2"
lie@3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/lie/-/lie-3.1.1.tgz#9a436b2cc7746ca59de7a41fa469b3efb76bd87e"
integrity sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==
dependencies:
immediate "~3.0.5"
lilconfig@^2.0.3:
version "2.0.6"
resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.0.6.tgz#32a384558bd58af3d4c6e077dd1ad1d397bc69d4"
@ -6942,6 +7016,13 @@ loader-utils@^2.0.0:
emojis-list "^3.0.0"
json5 "^2.1.2"
localforage@^1.8.1:
version "1.10.0"
resolved "https://registry.yarnpkg.com/localforage/-/localforage-1.10.0.tgz#5c465dc5f62b2807c3a84c0c6a1b1b3212781dd4"
integrity sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==
dependencies:
lie "3.1.1"
locate-path@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e"
@ -10388,7 +10469,7 @@ tslib@2.4.0:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3"
integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==
tslib@^1.8.1:
tslib@^1.8.1, tslib@^1.9.3:
version "1.14.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==

View File

@ -19,6 +19,7 @@ from zerver.lib.realm_description import get_realm_rendered_description
from zerver.lib.request import RequestNotes
from zerver.models import Message, Realm, Stream, UserProfile
from zerver.views.message_flags import get_latest_update_message_flag_activity
from zproject.config import get_config
@dataclass
@ -208,8 +209,17 @@ def build_page_params_for_home_page_load(
# There is no event queue for spectators since
# events support for spectators is not implemented yet.
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
for field_name in register_ret:
page_params[field_name] = register_ret[field_name]

View File

@ -2,7 +2,7 @@ import calendar
import datetime
import urllib
from datetime import timedelta
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, Dict
from unittest.mock import patch
import orjson
@ -198,6 +198,7 @@ class HomeTest(ZulipTestCase):
"server_needs_upgrade",
"server_presence_offline_threshold_seconds",
"server_presence_ping_interval_seconds",
"server_sentry_dsn",
"server_timestamp",
"server_web_public_streams_enabled",
"settings_send_digest_emails",
@ -355,6 +356,7 @@ class HomeTest(ZulipTestCase):
"realm_rendered_description",
"request_language",
"search_pills_enabled",
"server_sentry_dsn",
"show_billing",
"show_plans",
"show_webathena",
@ -367,6 +369,49 @@ class HomeTest(ZulipTestCase):
]
self.assertEqual(actual_keys, expected_keys)
def test_sentry_keys(self) -> None:
def home_params() -> Dict[str, Any]:
result = self._get_home_page()
self.assertEqual(result.status_code, 200)
return self._get_page_params(result)
self.login("hamlet")
page_params = home_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"):
page_params = home_params()
self.assertEqual(
page_params["server_sentry_dsn"], "https://aaa@bbb.ingest.sentry.io/1234"
)
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
realm = get_realm("zulip")
do_set_realm_property(realm, "enable_spectator_access", True, acting_user=None)
self.logout()
page_params = home_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"):
page_params = home_params()
self.assertEqual(
page_params["server_sentry_dsn"], "https://aaa@bbb.ingest.sentry.io/1234"
)
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:
with self.settings(TWO_FACTOR_AUTHENTICATION_ENABLED=True):
self.login("iago")

View File

@ -132,6 +132,9 @@ LOGGING_SHOW_PID = False
# Sentry.io error defaults to off
SENTRY_DSN: Optional[str] = None
SENTRY_FRONTEND_DSN: Optional[str] = None
SENTRY_FRONTEND_SAMPLE_RATE: float = 1.0
SENTRY_FRONTEND_TRACE_RATE: float = 0.1
# File uploads and avatars
# TODO: Rename MAX_FILE_UPLOAD_SIZE to have unit in name.

View File

@ -652,7 +652,11 @@ SOCIAL_AUTH_SAML_SUPPORT_CONTACT = {
# BROWSER_ERROR_REPORTING = False
## Controls the DSN used to report errors to Sentry.io
# SENTRY_DSN = "https://bbb@bbb.ingest.sentry.io/1235"
# SENTRY_DSN = "https://aaa@bbb.ingest.sentry.io/1234"
# SENTRY_FRONTEND_DSN = "https://aaa@bbb.ingest.sentry.io/1234"
## What portion of events are sampled (https://docs.sentry.io/platforms/javascript/configuration/sampling/):
# SENTRY_FRONTEND_SAMPLE_RATE = 1.0
# SENTRY_FRONTEND_TRACE_RATE = 0.1
## If True, each log message in the server logs will identify the
## Python module where it came from. Useful for tracking down a