diff --git a/.github/workflows/prod-build.yml b/.github/workflows/prod-build.yml index da267b3640f3..faa497d6aa7c 100644 --- a/.github/workflows/prod-build.yml +++ b/.github/workflows/prod-build.yml @@ -247,6 +247,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 5c1a301b94ab..d4564306e05f 100644 --- a/.github/workflows/stage-build.yml +++ b/.github/workflows/stage-build.yml @@ -264,6 +264,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 bfa70d1d960d..a59e8624e6d1 100644 --- a/.github/workflows/test-build.yml +++ b/.github/workflows/test-build.yml @@ -159,6 +159,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/build/index.ts b/build/index.ts index c9cb88533910..2a2b14267705 100644 --- a/build/index.ts +++ b/build/index.ts @@ -3,7 +3,10 @@ import path from "node:path"; import chalk from "chalk"; import * as cheerio from "cheerio"; -import * as Sentry from "@sentry/node"; +import { + setContext as setSentryContext, + setTags as setSentryTags, +} from "@sentry/node"; import { MacroInvocationError, @@ -182,12 +185,12 @@ export async function buildDocument( document, documentOptions: DocumentOptions = {} ): Promise { - Sentry.setContext("doc", { + setSentryContext("doc", { path: document?.fileInfo?.path, title: document?.metadata?.title, url: document?.url, }); - Sentry.setTags({ + setSentryTags({ doc_slug: document?.metadata?.slug, doc_locale: document?.metadata?.locale, }); diff --git a/client/config/webpack.config.js b/client/config/webpack.config.js index 92906a67a3b3..6033c56898e4 100644 --- a/client/config/webpack.config.js +++ b/client/config/webpack.config.js @@ -426,6 +426,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..39e69a3802e5 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 || ""; +export const SENTRY_RELEASE = process.env.REACT_APP_SENTRY_RELEASE || ""; + 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..149b1b08f12c --- /dev/null +++ b/client/src/telemetry/sentry.ts @@ -0,0 +1,41 @@ +import { SENTRY_DSN, SENTRY_ENVIRONMENT, SENTRY_RELEASE } from "../env"; + +let sentryPromise: Promise | null = null; + +function loadSentry(): Promise { + if (!sentryPromise) { + sentryPromise = import( + /* webpackChunkName: "sentry" */ "@sentry/react" + ).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 removeEventListener: (() => void) | null = null; + const capturedMessages = new Set(); + const errorHandler = (event: ErrorEvent) => { + loadSentry().then((Sentry) => { + if (removeEventListener) { + removeEventListener(); + removeEventListener = null; + } + if (!capturedMessages.has(event.message)) { + Sentry.captureException(event); + capturedMessages.add(event.message); + } + }); + }; + window.addEventListener("error", errorHandler); + removeEventListener = () => window.removeEventListener("error", errorHandler); +} diff --git a/libs/constants/index.js b/libs/constants/index.js index cf445f4149f2..d445cc016eda 100644 --- a/libs/constants/index.js +++ b/libs/constants/index.js @@ -116,6 +116,7 @@ export const CSP_DIRECTIVES = { "https://observatory-api.mdn.mozilla.net", "stats.g.doubleclick.net", + "*.sentry.io", "https://api.stripe.com", ], "font-src": ["'self'"], diff --git a/package.json b/package.json index d56fe0c66266..c7e797742967 100644 --- a/package.json +++ b/package.json @@ -74,6 +74,7 @@ "@mdn/browser-compat-data": "^5.5.51", "@mozilla/glean": "5.0.3", "@sentry/node": "^8.29.0", + "@sentry/react": "^8.29.0", "@stripe/stripe-js": "^4.4.0", "@use-it/interval": "^1.0.0", "@vscode/ripgrep": "^1.15.9", diff --git a/yarn.lock b/yarn.lock index d323bf7db091..3c38ef4f6be5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2946,6 +2946,57 @@ 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.29.0": + version "8.29.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/browser-utils/-/browser-utils-8.29.0.tgz#c84e8d8a08170dbf52968e6b563775949c2ac532" + integrity sha512-6HpyQkaqPvK6Lnigjlarq/LDYgXT2OBNf24RK7z0ipJSxSIpmtelfzHbnwWYnypNDXfTDdPm97fZEenQHryYJA== + dependencies: + "@sentry/core" "8.29.0" + "@sentry/types" "8.29.0" + "@sentry/utils" "8.29.0" + +"@sentry-internal/feedback@8.29.0": + version "8.29.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/feedback/-/feedback-8.29.0.tgz#9c562f7d13794131b6ac87860cda5492ed538e37" + integrity sha512-yAL5YMEFk4XaeVRUGEguydahRzaQrNPAaWRv6k+XRzCv9CGBhxb14KXQc9X/penlauMFcDfgelCPKcTqcf6wDw== + dependencies: + "@sentry/core" "8.29.0" + "@sentry/types" "8.29.0" + "@sentry/utils" "8.29.0" + +"@sentry-internal/replay-canvas@8.29.0": + version "8.29.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/replay-canvas/-/replay-canvas-8.29.0.tgz#57a08adec35641607b53ea079f7a6ef539e98c00" + integrity sha512-W2YbZRvp2lYC50V51fNLcnoIiK1Km4vSc+v6SL7c//lv2qpyumoUAAIDKY+14s8Lgt1RsR6rfZhfheD4O/6WSQ== + dependencies: + "@sentry-internal/replay" "8.29.0" + "@sentry/core" "8.29.0" + "@sentry/types" "8.29.0" + "@sentry/utils" "8.29.0" + +"@sentry-internal/replay@8.29.0": + version "8.29.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/replay/-/replay-8.29.0.tgz#d704ad5a137c3dd6fe398e0c9856c4fc043be707" + integrity sha512-Xgv/eYucsm7GaGKms2ClQ02NpD07MxjoTjp1/vYZm0H4Q08dVphVZrQp7hL1oX/VD9mb5SFyyKuuIRqIu7S8RA== + dependencies: + "@sentry-internal/browser-utils" "8.29.0" + "@sentry/core" "8.29.0" + "@sentry/types" "8.29.0" + "@sentry/utils" "8.29.0" + +"@sentry/browser@8.29.0": + version "8.29.0" + resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-8.29.0.tgz#d60a754a26c5235fab05fe2e675ced07209aaa88" + integrity sha512-aKTy4H/3RI0q9LIeepesjWGlGNeh4HGFfwQjzHME8gcWCQ5LSlzYX4U+hu2yp7r1Jfd9MUTFfOuuLih2HGLGsQ== + dependencies: + "@sentry-internal/browser-utils" "8.29.0" + "@sentry-internal/feedback" "8.29.0" + "@sentry-internal/replay" "8.29.0" + "@sentry-internal/replay-canvas" "8.29.0" + "@sentry/core" "8.29.0" + "@sentry/types" "8.29.0" + "@sentry/utils" "8.29.0" + "@sentry/core@8.29.0": version "8.29.0" resolved "https://registry.yarnpkg.com/@sentry/core/-/core-8.29.0.tgz#52032ece2d7b60f3775f10189c27e26b1cebdbca" @@ -3001,6 +3052,17 @@ "@sentry/types" "8.29.0" "@sentry/utils" "8.29.0" +"@sentry/react@^8.29.0": + version "8.29.0" + resolved "https://registry.yarnpkg.com/@sentry/react/-/react-8.29.0.tgz#f69b87a7947213dcaabdd18bafb6ac818d30f0a2" + integrity sha512-ux+9rNHx2ZyWC94OBb5K1HFQU/v64gL/n3co9e/3cD9nUnqXMJuw/IofiwD1fv6nfdWECLU50A1OtXhA9/c+XQ== + dependencies: + "@sentry/browser" "8.29.0" + "@sentry/core" "8.29.0" + "@sentry/types" "8.29.0" + "@sentry/utils" "8.29.0" + hoist-non-react-statics "^3.3.2" + "@sentry/types@8.29.0": version "8.29.0" resolved "https://registry.yarnpkg.com/@sentry/types/-/types-8.29.0.tgz#c19e43524b8e7766028f4da8f02eddcc33518541" @@ -8723,6 +8785,13 @@ history@^5.2.0: dependencies: "@babel/runtime" "^7.7.6" +hoist-non-react-statics@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" + integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== + dependencies: + react-is "^16.7.0" + hoopy@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/hoopy/-/hoopy-0.1.4.tgz#609207d661100033a9a9402ad3dea677381c1b1d" @@ -13279,7 +13348,7 @@ react-dom@^18.3.1: loose-envify "^1.1.0" scheduler "^0.23.2" -react-is@^16.13.1: +react-is@^16.13.1, react-is@^16.7.0: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==