diff --git a/.github/workflows/prod-build.yml b/.github/workflows/prod-build.yml index 0413b299097e..9c24ab12b745 100644 --- a/.github/workflows/prod-build.yml +++ b/.github/workflows/prod-build.yml @@ -285,6 +285,9 @@ jobs: REACT_APP_OBSERVATORY_API_URL: https://observatory-api.mdn.mozilla.net # Sentry. + REACT_APP_SENTRY_DSN: ${{ secrets.SENTRY_DSN_CLIENT }} + REACT_APP_SENTRY_ENVIRONMENT: prod + REACT_APP_SENTRY_RELEASE: ${{ github.sha }} SENTRY_DSN_BUILD: ${{ secrets.SENTRY_DSN_BUILD }} SENTRY_ENVIRONMENT: prod SENTRY_RELEASE: ${{ github.sha }} diff --git a/.github/workflows/stage-build.yml b/.github/workflows/stage-build.yml index 57561d91e1b8..6b4a7660e84d 100644 --- a/.github/workflows/stage-build.yml +++ b/.github/workflows/stage-build.yml @@ -302,6 +302,9 @@ jobs: REACT_APP_OBSERVATORY_API_URL: https://observatory-api.mdn.allizom.net # Sentry. + REACT_APP_SENTRY_DSN: ${{ secrets.SENTRY_DSN_CLIENT }} + REACT_APP_SENTRY_ENVIRONMENT: stage + REACT_APP_SENTRY_RELEASE: ${{ github.sha }} SENTRY_DSN_BUILD: ${{ secrets.SENTRY_DSN_BUILD }} SENTRY_ENVIRONMENT: stage SENTRY_RELEASE: ${{ github.sha }} diff --git a/.github/workflows/test-build.yml b/.github/workflows/test-build.yml index 8698df15a2dd..a2b34660bca3 100644 --- a/.github/workflows/test-build.yml +++ b/.github/workflows/test-build.yml @@ -238,6 +238,12 @@ jobs: # Observatory REACT_APP_OBSERVATORY_API_URL: https://observatory-api.mdn.allizom.net + + # Sentry. + REACT_APP_SENTRY_DSN: ${{ secrets.SENTRY_DSN_CLIENT }} + REACT_APP_SENTRY_ENVIRONMENT: test + REACT_APP_SENTRY_RELEASE: ${{ github.sha }} + run: | set -eo pipefail diff --git a/client/config/webpack.config.js b/client/config/webpack.config.js index f6d1b7375c4a..9df7f6f5289a 100644 --- a/client/config/webpack.config.js +++ b/client/config/webpack.config.js @@ -433,6 +433,14 @@ function config(webpackEnv) { // during a production build. // Otherwise React will be compiled in the very slow development mode. new webpack.DefinePlugin(env.stringified), + // Treeshake Sentry (saves about 12 kB on the chunk). + new webpack.DefinePlugin({ + __SENTRY_DEBUG__: false, + __SENTRY_TRACING__: false, + __RRWEB_EXCLUDE_IFRAME__: true, + __RRWEB_EXCLUDE_SHADOW_DOM__: true, + __SENTRY_EXCLUDE_REPLAY_WORKER__: true, + }), // Experimental hot reloading for React . // https://github.com/facebook/react/tree/main/packages/react-refresh isEnvDevelopment && diff --git a/client/src/env.ts b/client/src/env.ts index 5f9eea8c8fb0..0b3722d8dc57 100644 --- a/client/src/env.ts +++ b/client/src/env.ts @@ -103,6 +103,11 @@ export const GLEAN_LOG_CLICK = Boolean( JSON.parse(process.env.REACT_APP_GLEAN_LOG_CLICK || "false") ); +export const SENTRY_DSN = process.env.REACT_APP_SENTRY_DSN || ""; +export const SENTRY_ENVIRONMENT = + process.env.REACT_APP_SENTRY_ENVIRONMENT || "dev"; +export const SENTRY_RELEASE = process.env.REACT_APP_SENTRY_RELEASE || "dev"; + export const AI_FEEDBACK_GITHUB_REPO = process.env.REACT_APP_AI_FEEDBACK_GITHUB_REPO || "mdn/private-ai-feedback"; diff --git a/client/src/index.tsx b/client/src/index.tsx index b9cd51c2df86..3c875cb5f407 100644 --- a/client/src/index.tsx +++ b/client/src/index.tsx @@ -8,8 +8,10 @@ import { UserDataProvider } from "./user-context"; import { UIProvider } from "./ui-context"; import { GleanProvider } from "./telemetry/glean-context"; import { PlacementProvider } from "./placement-context"; +import { initSentry } from "./telemetry/sentry"; // import * as serviceWorker from './serviceWorker'; +initSentry(); const container = document.getElementById("root"); if (!container) { diff --git a/client/src/telemetry/sentry.ts b/client/src/telemetry/sentry.ts new file mode 100644 index 000000000000..14f7c2743630 --- /dev/null +++ b/client/src/telemetry/sentry.ts @@ -0,0 +1,55 @@ +import { SENTRY_DSN, SENTRY_ENVIRONMENT, SENTRY_RELEASE } from "../env"; + +let sentryPromise: Promise | null = null; + +/** + * Loads the Sentry module asynchronously and initializes it. + * Utilizes dynamic import to split Sentry related code into a separate chunk. + * Ensures Sentry is only loaded and initialized once. + * + * @returns A promise resolving to the initialized Sentry object. + */ +function loadSentry(): Promise { + if (!sentryPromise) { + sentryPromise = import( + /* webpackChunkName: "sentry" */ "@sentry/browser" + ).then((Sentry) => { + Sentry.init({ + dsn: SENTRY_DSN, + release: SENTRY_RELEASE || "dev", + environment: SENTRY_ENVIRONMENT || "dev", + }); + return Sentry; + }); + } + + return sentryPromise; +} + +export function initSentry() { + if (!SENTRY_DSN) { + return; + } + + let onSentryLoad: (() => void) | null = null; + const capturedMessages = new Set(); + + const handleError = (event: ErrorEvent) => { + loadSentry().then((Sentry) => { + if (onSentryLoad) { + onSentryLoad(); + onSentryLoad = null; + } + if (!capturedMessages.has(event.message)) { + // Capture every error only once. + Sentry.captureException(event); + capturedMessages.add(event.message); + } + }); + }; + + // Avoid capturing too many events by stop listening when Sentry is loaded. + onSentryLoad = () => window.removeEventListener("error", handleError); + + window.addEventListener("error", handleError); +} diff --git a/docs/envvars.md b/docs/envvars.md index 2f600f9bec9e..b10018a00e8b 100644 --- a/docs/envvars.md +++ b/docs/envvars.md @@ -286,6 +286,25 @@ for content editing as authentication is then not required. Enables features or setup which only make sense in local development. +### `REACT_APP_SENTRY_DSN` + +**Default: `""`** + +Enables client-side error tracking with Sentry. + +### `REACT_APP_SENTRY_ENVIRONMENT` + +**Default: `"dev"`** + +Specifies the name of the current environment. + +### `REACT_APP_SENTRY_RELEASE` + +**Default: `"dev"`** + +Specifies the version of the current build. This is set to the commit hash in +deployments. + ### `REACT_APP_WRITER_MODE` **Default: `false`** diff --git a/libs/constants/index.js b/libs/constants/index.js index 4a1b57643d48..a616b6a1a3cf 100644 --- a/libs/constants/index.js +++ b/libs/constants/index.js @@ -119,6 +119,7 @@ export const CSP_DIRECTIVES = { "https://api.github.com/search/issues", "stats.g.doubleclick.net", + "*.sentry.io", "https://api.stripe.com", ], "font-src": ["'self'"], diff --git a/package.json b/package.json index a084d9d7a770..d765aefbf72f 100644 --- a/package.json +++ b/package.json @@ -87,6 +87,7 @@ "@mdn/browser-compat-data": "^5.6.33", "@mdn/rari": "^0.1.20", "@mozilla/glean": "5.0.3", + "@sentry/browser": "^8.51.0", "@sentry/node": "^8.51.0", "@stripe/stripe-js": "^5.5.0", "@use-it/interval": "^1.0.0", diff --git a/yarn.lock b/yarn.lock index 8a086001a4ed..8011062334a8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2725,6 +2725,47 @@ resolved "https://registry.yarnpkg.com/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz#60de891bb126abfdc5410fdc6166aca065f10a0c" integrity sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg== +"@sentry-internal/browser-utils@8.51.0": + version "8.51.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/browser-utils/-/browser-utils-8.51.0.tgz#eaa245aefad8b3d893516ffe9535b1907f049094" + integrity sha512-r94yfRK17zNJER0hgQE4qOSy5pWzsnFcGTJQSqhSEKUcC4KK37qSfoPrPejFxtIqXhqlkd/dTWKvrMwXWcn0MQ== + dependencies: + "@sentry/core" "8.51.0" + +"@sentry-internal/feedback@8.51.0": + version "8.51.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/feedback/-/feedback-8.51.0.tgz#58e1de053b175e6fe5896e9b1282d754ea4e12d5" + integrity sha512-VgfxSZWLYUPKDnkt2zG+Oe5ccv8U3WPM6Mo4kfABIJT3Ai4VbZB7+vb2a4pm6lUCF9DeOPXHb5o9Tg17SHDAHw== + dependencies: + "@sentry/core" "8.51.0" + +"@sentry-internal/replay-canvas@8.51.0": + version "8.51.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/replay-canvas/-/replay-canvas-8.51.0.tgz#c896448d456290f8f4eb99df924d1bcc07908947" + integrity sha512-ERXIbwdULkdtIQnfkMLRVfpoGV2rClwySGRlTPepFKeLxlcXo9o09cPu+qbukiDnGK0cgEgRnrV961hMg21Bmw== + dependencies: + "@sentry-internal/replay" "8.51.0" + "@sentry/core" "8.51.0" + +"@sentry-internal/replay@8.51.0": + version "8.51.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/replay/-/replay-8.51.0.tgz#c121518ef493afcf38c900d37e12af68787351d5" + integrity sha512-lkm7id3a2n3yMZeF5socCVQUeEeShNOGr7Wtsmb5RORacEnld0z+NfbMTilo1mDwiWBzI5OYBjm62eglm1HFsQ== + dependencies: + "@sentry-internal/browser-utils" "8.51.0" + "@sentry/core" "8.51.0" + +"@sentry/browser@^8.51.0": + version "8.51.0" + resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-8.51.0.tgz#1d208785a432f4a4aee616da459e118994c8661f" + integrity sha512-1kbbyVfBBAx5Xyynp+lC5lLnAHo0qJ2r4mtmdT6koPjesvoOocEK0QQnouQBmdUbm3L0L/bPI1SgXjbeJyhzHQ== + dependencies: + "@sentry-internal/browser-utils" "8.51.0" + "@sentry-internal/feedback" "8.51.0" + "@sentry-internal/replay" "8.51.0" + "@sentry-internal/replay-canvas" "8.51.0" + "@sentry/core" "8.51.0" + "@sentry/core@8.51.0": version "8.51.0" resolved "https://registry.yarnpkg.com/@sentry/core/-/core-8.51.0.tgz#d0c73dfe3489788911b7ce784d3ef8458344482c"