diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 314c94ba73..f8cd27c49d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -177,11 +177,29 @@ jobs: SUPPORT_EMAIL: mitlearn-support@mit.edu MITOL_AXIOS_WITH_CREDENTIALS: true CSRF_COOKIE_NAME: learn-rc-csrftoken + NEXT_PUBLIC_SENTRY_DSN: ${{ secrets.SENTRY_DSN_RC }} + NEXT_PUBLIC_SENTRY_ENV: rc + NEXT_PUBLIC_SENTRY_PROFILES_SAMPLE_RATE: 0.0 + NEXT_PUBLIC_SENTRY_TRACES_SAMPLE_RATE: 0.0 + NEXT_PUBLIC_VERSION: ${{ github.sha }} + NEXT_PUBLIC_POSTHOG_API_KEY: ${{ secrets.POSTHOG_API_KEY_RC }} run: | heroku container:push web \ --app mitopen-rc-nextjs \ --recursive \ - --arg NEXT_PUBLIC_ORIGIN=$ORIGIN,NEXT_PUBLIC_MITOL_API_BASE_URL=$MITOL_API_BASE_URL,NEXT_PUBLIC_SITE_NAME="$SITE_NAME",NEXT_PUBLIC_MITOL_SUPPORT_EMAIL=$SUPPORT_EMAIL,NEXT_PUBLIC_EMBEDLY_KEY=$EMBEDLY_KEY,NEXT_PUBLIC_MITOL_AXIOS_WITH_CREDENTIALS=$MITOL_AXIOS_WITH_CREDENTIALS,NEXT_PUBLIC_CSRF_COOKIE_NAME=$CSRF_COOKIE_NAME \ + --arg NEXT_PUBLIC_ORIGIN=$ORIGIN,\ + NEXT_PUBLIC_MITOL_API_BASE_URL=$MITOL_API_BASE_URL,\ + NEXT_PUBLIC_SITE_NAME="$SITE_NAME",\ + NEXT_PUBLIC_MITOL_SUPPORT_EMAIL=$SUPPORT_EMAIL,\ + NEXT_PUBLIC_EMBEDLY_KEY=$EMBEDLY_KEY,\ + NEXT_PUBLIC_MITOL_AXIOS_WITH_CREDENTIALS=$MITOL_AXIOS_WITH_CREDENTIALS,\ + NEXT_PUBLIC_CSRF_COOKIE_NAME=$CSRF_COOKIE_NAME,\ + NEXT_PUBLIC_VERSION=$NEXT_PUBLIC_VERSION,\ + NEXT_PUBLIC_SENTRY_DSN=$NEXT_PUBLIC_SENTRY_DSN,\ + NEXT_PUBLIC_SENTRY_ENV=$NEXT_PUBLIC_SENTRY_ENV,\ + NEXT_PUBLIC_SENTRY_PROFILES_SAMPLE_RATE=$NEXT_PUBLIC_SENTRY_PROFILES_SAMPLE_RATE,\ + NEXT_PUBLIC_SENTRY_TRACES_SAMPLE_RATE=$NEXT_PUBLIC_SENTRY_TRACES_SAMPLE_RATE,\ + NEXT_PUBLIC_POSTHOG_API_KEY=$NEXT_PUBLIC_POSTHOG_API_KEY \ --context-path . - name: Release on Heroku diff --git a/frontends/main/.gitignore b/frontends/main/.gitignore index fd3dbb571a..1dd45b2022 100644 --- a/frontends/main/.gitignore +++ b/frontends/main/.gitignore @@ -34,3 +34,6 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +# Sentry Config File +.env.sentry-build-plugin diff --git a/frontends/main/Dockerfile.web b/frontends/main/Dockerfile.web index 4033007f28..36e4b9b550 100644 --- a/frontends/main/Dockerfile.web +++ b/frontends/main/Dockerfile.web @@ -73,6 +73,24 @@ ENV NEXT_PUBLIC_MITOL_SUPPORT_EMAIL=$NEXT_PUBLIC_MITOL_SUPPORT_EMAIL ARG NEXT_PUBLIC_EMBEDLY_KEY=None ENV NEXT_PUBLIC_EMBEDLY_KEY=$NEXT_PUBLIC_EMBEDLY_KEY +ARG NEXT_PUBLIC_SENTRY_DSN +ENV NEXT_PUBLIC_SENTRY_DSN=$NEXT_PUBLIC_SENTRY_DSN + +ARG NEXT_PUBLIC_SENTRY_ENV +ENV NEXT_PUBLIC_SENTRY_ENV=$NEXT_PUBLIC_SENTRY_ENV + +ARG NEXT_PUBLIC_VERSION +ENV NEXT_PUBLIC_VERSION=$NEXT_PUBLIC_VERSION + +ARG NEXT_PUBLIC_SENTRY_PROFILES_SAMPLE_RATE +ENV NEXT_PUBLIC_SENTRY_PROFILES_SAMPLE_RATE=$NEXT_PUBLIC_SENTRY_PROFILES_SAMPLE_RATE + +ARG NEXT_PUBLIC_SENTRY_TRACES_SAMPLE_RATE +ENV NEXT_PUBLIC_SENTRY_TRACES_SAMPLE_RATE=$NEXT_PUBLIC_SENTRY_TRACES_SAMPLE_RATE + +ARG NEXT_PUBLIC_POSTHOG_API_KEY +ENV NEXT_PUBLIC_POSTHOG_API_KEY=$NEXT_PUBLIC_POSTHOG_API_KEY + ARG NEXT_PUBLIC_MITOL_AXIOS_WITH_CREDENTIALS=true ENV NEXT_PUBLIC_MITOL_AXIOS_WITH_CREDENTIALS=$NEXT_PUBLIC_MITOL_AXIOS_WITH_CREDENTIALS diff --git a/frontends/main/next.config.js b/frontends/main/next.config.js index 62a8549aa3..be6660d469 100644 --- a/frontends/main/next.config.js +++ b/frontends/main/next.config.js @@ -21,6 +21,7 @@ const processFeatureFlags = () => { /** @type {import('next').NextConfig} */ const nextConfig = { + productionBrowserSourceMaps: true, async rewrites() { return [ /* Images moved from /static, though image paths are sometimes @@ -99,4 +100,32 @@ const nextConfig = { }, } -module.exports = nextConfig +// Injected content via Sentry wizard below + +const { withSentryConfig } = require("@sentry/nextjs") + +module.exports = withSentryConfig(nextConfig, { + // For all available options, see: + // https://github.com/getsentry/sentry-webpack-plugin#options + + org: "mit-office-of-digital-learning", + project: "open-next", + + // Only print logs for uploading source maps in CI + silent: !process.env.CI, + + // For all available options, see: + // https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/ + + // Upload a larger set of source maps for prettier stack traces (increases build time) + widenClientFileUpload: true, + + // Uncomment to route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers. + // This can increase your server load as well as your hosting bill. + // Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client- + // side errors will fail. + // tunnelRoute: "/monitoring", + + // Automatically tree-shake Sentry logger statements to reduce bundle size + disableLogger: true, +}) diff --git a/frontends/main/package.json b/frontends/main/package.json index bf9db484e3..0baa05c26e 100644 --- a/frontends/main/package.json +++ b/frontends/main/package.json @@ -14,6 +14,7 @@ "@emotion/cache": "^11.13.1", "@mitodl/course-search-utils": "3.2.4", "@remixicon/react": "^4.2.0", + "@sentry/nextjs": "^8", "@tanstack/react-query": "^4.36.1", "api": "workspace:*", "formik": "^2.4.6", diff --git a/frontends/main/sentry.client.config.ts b/frontends/main/sentry.client.config.ts new file mode 100644 index 0000000000..b0feb7266b --- /dev/null +++ b/frontends/main/sentry.client.config.ts @@ -0,0 +1,24 @@ +// Added by @sentry/wizard +// This file configures the initialization of Sentry on the client. +// The config you add here will be used whenever a users loads a page in their browser. +// https://docs.sentry.io/platforms/javascript/guides/nextjs/ + +import * as Sentry from "@sentry/nextjs" + +Sentry.init({ + dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, + release: process.env.NEXT_PUBLIC_VERSION, + environment: process.env.NEXT_PUBLIC_SENTRY_ENV, + profilesSampleRate: Number( + process.env.NEXT_PUBLIC_SENTRY_PROFILES_SAMPLE_RATE, + ), + tracesSampleRate: Number(process.env.NEXT_PUBLIC_SENTRY_TRACES_SAMPLE_RATE), + tracePropagationTargets: process.env.NEXT_PUBLIC_MITOL_API_BASE_URL + ? [process.env.NEXT_PUBLIC_MITOL_API_BASE_URL] + : [], + // Add optional integrations for additional features + integrations: [ + Sentry.browserTracingIntegration(), + Sentry.browserProfilingIntegration(), + ], +}) diff --git a/frontends/main/sentry.edge.config.ts b/frontends/main/sentry.edge.config.ts new file mode 100644 index 0000000000..72510830d2 --- /dev/null +++ b/frontends/main/sentry.edge.config.ts @@ -0,0 +1,18 @@ +// Added by @sentry/wizard +// This file configures the initialization of Sentry for edge features (middleware, edge routes, and so on). +// The config you add here will be used whenever one of the edge features is loaded. +// Note that this config is unrelated to the Vercel Edge Runtime and is also required when running locally. +// https://docs.sentry.io/platforms/javascript/guides/nextjs/ + +import * as Sentry from "@sentry/nextjs" + +Sentry.init({ + dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, + release: process.env.NEXT_PUBLIC_VERSION, + environment: process.env.NEXT_PUBLIC_SENTRY_ENV, + // Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control. + tracesSampleRate: 1, + + // Setting this option to true will print useful information to the console while you're setting up Sentry. + debug: false, +}) diff --git a/frontends/main/sentry.server.config.ts b/frontends/main/sentry.server.config.ts new file mode 100644 index 0000000000..a7c03a3866 --- /dev/null +++ b/frontends/main/sentry.server.config.ts @@ -0,0 +1,17 @@ +// Added by @sentry/wizard +// This file configures the initialization of Sentry on the server. +// The config you add here will be used whenever the server handles a request. +// https://docs.sentry.io/platforms/javascript/guides/nextjs/ + +import * as Sentry from "@sentry/nextjs" + +Sentry.init({ + dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, + release: process.env.NEXT_PUBLIC_VERSION, + environment: process.env.NEXT_PUBLIC_SENTRY_ENV, + // Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control. + tracesSampleRate: 1, + + // Setting this option to true will print useful information to the console while you're setting up Sentry. + debug: false, +}) diff --git a/frontends/main/src/app-pages/ErrorPage/ErrorPageTemplate.tsx b/frontends/main/src/app-pages/ErrorPage/ErrorPageTemplate.tsx index 54a567d86e..bcea4ee62e 100644 --- a/frontends/main/src/app-pages/ErrorPage/ErrorPageTemplate.tsx +++ b/frontends/main/src/app-pages/ErrorPage/ErrorPageTemplate.tsx @@ -17,10 +17,6 @@ const ErrorPageTemplate: React.FC = ({ children }) => { return ( - {/* TODO - {`${title} | ${APP_SETTINGS.SITE_NAME}`} - - */} {children} diff --git a/frontends/main/src/app-pages/ErrorPage/GlobalErrorPage.tsx b/frontends/main/src/app-pages/ErrorPage/FallbackErrorPage.tsx similarity index 64% rename from frontends/main/src/app-pages/ErrorPage/GlobalErrorPage.tsx rename to frontends/main/src/app-pages/ErrorPage/FallbackErrorPage.tsx index 19c8f77f76..50b7984c4a 100644 --- a/frontends/main/src/app-pages/ErrorPage/GlobalErrorPage.tsx +++ b/frontends/main/src/app-pages/ErrorPage/FallbackErrorPage.tsx @@ -4,15 +4,15 @@ import React from "react" import ErrorPageTemplate from "./ErrorPageTemplate" import { Typography } from "ol-components" -const GlobalErrorPage = ({ error }: { error: Pick }) => { +const FallbackErrorPage = ({ error }: { error: Pick }) => { return ( - Unexpected Error + Something went wrong. - {error.message || ""} + {error.message} ) } -export default GlobalErrorPage +export default FallbackErrorPage diff --git a/frontends/main/src/app/error.test.tsx b/frontends/main/src/app/error.test.tsx new file mode 100644 index 0000000000..e85263fc6c --- /dev/null +++ b/frontends/main/src/app/error.test.tsx @@ -0,0 +1,22 @@ +import React from "react" +import { renderWithProviders, screen } from "@/test-utils" +import { HOME } from "@/common/urls" +import ErrorPage from "./error" +import { ForbiddenError } from "@/common/permissions" + +test("The error page shows error message", () => { + const error = new Error("Ruh roh") + renderWithProviders() + screen.getByRole("heading", { name: "Something went wrong." }) + screen.getByText("Ruh roh") + const homeLink = screen.getByRole("link", { name: "Home" }) + expect(homeLink).toHaveAttribute("href", HOME) +}) + +test("The NotFoundPage loads with a link that directs to HomePage", () => { + const error = new ForbiddenError("You can't do that") + renderWithProviders(, { user: {} }) + screen.getByRole("heading", { name: "Not Allowed" }) + const homeLink = screen.getByRole("link", { name: "Home" }) + expect(homeLink).toHaveAttribute("href", HOME) +}) diff --git a/frontends/main/src/app/error.tsx b/frontends/main/src/app/error.tsx new file mode 100644 index 0000000000..9f9e217d31 --- /dev/null +++ b/frontends/main/src/app/error.tsx @@ -0,0 +1,29 @@ +"use client" +/* + * Fallback error UI for errors within page content. + * + * Notes: + * - DOES use root layout + * + * See for more: + * https://nextjs.org/docs/app/building-your-application/routing/error-handling#handling-errors-in-root-layouts + */ + +import React, { useEffect } from "react" +import * as Sentry from "@sentry/nextjs" +import FallbackErrorPage from "@/app-pages/ErrorPage/FallbackErrorPage" +import { ForbiddenError } from "@/common/permissions" +import ForbiddenPage from "@/app-pages/ErrorPage/ForbiddenPage" +const Error = ({ error }: { error: Error }) => { + useEffect(() => { + Sentry.captureException(error) + }, [error]) + + if (error instanceof ForbiddenError) { + return + } + + return +} + +export default Error diff --git a/frontends/main/src/app/global-error.tsx b/frontends/main/src/app/global-error.tsx index 43d2266c5c..17198a0c85 100644 --- a/frontends/main/src/app/global-error.tsx +++ b/frontends/main/src/app/global-error.tsx @@ -1,19 +1,34 @@ "use client" -/* This is the catch-all error page that receives errors from server rendered root layout - * components and metadata. - * It is only enabled in production so that in development we see the Next.js error overlay. - * It is passed an error object as an argument, though this has been stripped of everything except - * the message and a digest for server logs correlation; to prevent leaking anything to the client +/* + * Fallback error UI for errors within root layout. + * (error.tsx is the fallback error page for UI within page content.) + * + * Notes: + * - does NOT use root layout (since error occured there!) + * - therefore, must definie its own HTML tags and providers + * Must define its own HTML tag + * - NOT used in development mode * * https://nextjs.org/docs/app/building-your-application/routing/error-handling#handling-errors-in-root-layouts */ +import React, { useEffect } from "react" +import * as Sentry from "@sentry/nextjs" +import FallbackErrorPage from "@/app-pages/ErrorPage/FallbackErrorPage" +import { MITLearnGlobalStyles, ThemeProvider } from "ol-components" -import React from "react" -import GlobalErrorPage from "@/app-pages/ErrorPage/GlobalErrorPage" - -const GlobalError = ({ error }: { error: Error }) => { - return +export default function GlobalError({ error }: { error: Error }) { + useEffect(() => { + Sentry.captureException(error) + }, [error]) + return ( + + + + + + + + + ) } - -export default GlobalError diff --git a/frontends/main/src/app/layout.tsx b/frontends/main/src/app/layout.tsx index d483f47d20..a43a4e6ab5 100644 --- a/frontends/main/src/app/layout.tsx +++ b/frontends/main/src/app/layout.tsx @@ -5,7 +5,6 @@ import { PageWrapper, PageWrapperInner } from "./styled" import Providers from "./providers" import { MITLearnGlobalStyles } from "ol-components" import Script from "next/script" -import ErrorBoundary from "@/components/ErrorBoundary/ErrorBoundary" import "./GlobalStyles" @@ -30,9 +29,7 @@ export default function RootLayout({
- - {children} - + {children}