diff --git a/package.json b/package.json index 2e7af2c140..b8789790eb 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/puppet/zulip/files/hooks/post-deploy.d/sentry.hook b/puppet/zulip/files/hooks/post-deploy.d/sentry.hook index dd3a756992..69115fd985 100755 --- a/puppet/zulip/files/hooks/post-deploy.d/sentry.hook +++ b/puppet/zulip/files/hooks/post-deploy.d/sentry.hook @@ -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 diff --git a/puppet/zulip/files/hooks/pre-deploy.d/sentry.hook b/puppet/zulip/files/hooks/pre-deploy.d/sentry.hook index e8af25a869..17d774b463 100755 --- a/puppet/zulip/files/hooks/pre-deploy.d/sentry.hook +++ b/puppet/zulip/files/hooks/pre-deploy.d/sentry.hook @@ -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 diff --git a/tools/test-js-with-node b/tools/test-js-with-node index bb5ccec977..c460337474 100755 --- a/tools/test-js-with-node +++ b/tools/test-js-with-node @@ -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", diff --git a/version.py b/version.py index 07449c4d9e..6420979846 100644 --- a/version.py +++ b/version.py @@ -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) diff --git a/web/src/bundles/common.ts b/web/src/bundles/common.ts index 88947b7276..368277cde0 100644 --- a/web/src/bundles/common.ts +++ b/web/src/bundles/common.ts @@ -1,3 +1,4 @@ +import "../sentry"; import "../webpack_public_path"; import "../../debug-require"; import "../alert_popup"; diff --git a/web/src/page_params.ts b/web/src/page_params.ts index 44f11ab9ae..d53a1f7ede 100644 --- a/web/src/page_params.ts +++ b/web/src/page_params.ts @@ -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; + 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(); diff --git a/web/src/sentry.ts b/web/src/sentry.ts new file mode 100644 index 0000000000..028de2efeb --- /dev/null +++ b/web/src/sentry.ts @@ -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, + }, + }); +} diff --git a/yarn.lock b/yarn.lock index db29732ce7..96b08686f2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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== diff --git a/zerver/lib/home.py b/zerver/lib/home.py index 59e92cdb18..9baf46fe5a 100644 --- a/zerver/lib/home.py +++ b/zerver/lib/home.py @@ -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] diff --git a/zerver/tests/test_home.py b/zerver/tests/test_home.py index e67464bc7f..aaa8f3dbfb 100644 --- a/zerver/tests/test_home.py +++ b/zerver/tests/test_home.py @@ -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") diff --git a/zproject/default_settings.py b/zproject/default_settings.py index 1effcbdaad..c0a11e1890 100644 --- a/zproject/default_settings.py +++ b/zproject/default_settings.py @@ -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. diff --git a/zproject/prod_settings_template.py b/zproject/prod_settings_template.py index 68f6d504b5..2175b6a0c1 100644 --- a/zproject/prod_settings_template.py +++ b/zproject/prod_settings_template.py @@ -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