diff --git a/.gitignore b/.gitignore index 050500df..b686e205 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ # dependencies /node_modules +package-lock.json # testing /coverage diff --git a/next.config.js b/next.config.js index 26fc962d..e59592fb 100644 --- a/next.config.js +++ b/next.config.js @@ -1,9 +1,11 @@ // eslint-disable-next-line @typescript-eslint/no-var-requires +const { withSentryConfig } = require("@sentry/nextjs") +// eslint-disable-next-line @typescript-eslint/no-var-requires const withBundleAnalyzer = require("@next/bundle-analyzer")({ enabled: process.env.ANALYZE === "true" }) -module.exports = withBundleAnalyzer({ +const nextConfig = { experimental: { ppr: true }, @@ -16,7 +18,9 @@ module.exports = withBundleAnalyzer({ APP_VERSION: process.env.APP_VERSION, BUNGIE_API_KEY: process.env.BUNGIE_API_KEY, RAIDHUB_API_URL: process.env.RAIDHUB_API_URL ?? "https://api.raidhub.io", - RAIDHUB_API_KEY: process.env.RAIDHUB_API_KEY + RAIDHUB_API_KEY: process.env.RAIDHUB_API_KEY, + NEXT_PUBLIC_SENTRY_DSN: process.env.NEXT_PUBLIC_SENTRY_DSN, + NEXT_PUBLIC_APP_ENV: process.env.APP_ENV }, images: { remotePatterns: [ @@ -49,4 +53,19 @@ module.exports = withBundleAnalyzer({ source: "/:vanity([a-zA-Z0-9]+)" } ] +} + +module.exports = withSentryConfig(withBundleAnalyzer(nextConfig), { + org: process.env.SENTRY_ORG, + project: process.env.SENTRY_PROJECT, + silent: !process.env.CI, + widenClientFileUpload: true, + tunnelRoute: "/monitoring", + hideSourceMaps: true, + webpack: { + treeshake: { + removeDebugLogging: true + }, + automaticVercelMonitors: true + } }) diff --git a/package.json b/package.json index e3caf124..0bc40fea 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "@radix-ui/react-slot": "^1.2.2", "@radix-ui/react-tabs": "^1.1.11", "@radix-ui/react-tooltip": "^1.2.6", + "@sentry/nextjs": "^10.38.0", "@tailwindcss/postcss": "^4.1.5", "@tanstack/react-query": "4.36.1", "@trpc/client": "10.44.1", diff --git a/sentry.edge.config.ts b/sentry.edge.config.ts new file mode 100644 index 00000000..8a4d22fb --- /dev/null +++ b/sentry.edge.config.ts @@ -0,0 +1,7 @@ +import * as Sentry from "@sentry/nextjs" + +Sentry.init({ + dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, + tracesSampleRate: 1.0, + environment: process.env.APP_ENV ?? "development" +}) diff --git a/sentry.server.config.ts b/sentry.server.config.ts new file mode 100644 index 00000000..8a4d22fb --- /dev/null +++ b/sentry.server.config.ts @@ -0,0 +1,7 @@ +import * as Sentry from "@sentry/nextjs" + +Sentry.init({ + dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, + tracesSampleRate: 1.0, + environment: process.env.APP_ENV ?? "development" +}) diff --git a/src/app/error.tsx b/src/app/error.tsx index b3e8cbdb..cf0f742f 100644 --- a/src/app/error.tsx +++ b/src/app/error.tsx @@ -1,5 +1,6 @@ "use client" // Error components must be Client Components +import * as Sentry from "@sentry/nextjs" import { useParams, usePathname, useSearchParams } from "next/navigation" import { useEffect } from "react" import { type ErrorBoundaryProps } from "~/types/generic" @@ -22,6 +23,7 @@ export default function ErrorBoundary({ error, reset }: ErrorBoundaryProps) { stack: error.stack } } + Sentry.captureException(error) console.error(err) }, [error, params, pathname, searchParams]) diff --git a/src/app/global-error.tsx b/src/app/global-error.tsx index 5219a327..d379bf6b 100644 --- a/src/app/global-error.tsx +++ b/src/app/global-error.tsx @@ -1,5 +1,6 @@ "use client" +import * as Sentry from "@sentry/nextjs" import { useEffect } from "react" import { PageWrapper } from "~/components/PageWrapper" @@ -11,6 +12,7 @@ export default function GlobalError({ reset: () => void }) { useEffect(() => { + Sentry.captureException(error) console.error(error) }, [error]) diff --git a/src/instrumentation-client.ts b/src/instrumentation-client.ts new file mode 100644 index 00000000..15b70d39 --- /dev/null +++ b/src/instrumentation-client.ts @@ -0,0 +1,12 @@ +import * as Sentry from "@sentry/nextjs" + +Sentry.init({ + dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, + tracesSampleRate: 1.0, + replaysSessionSampleRate: 0.1, + replaysOnErrorSampleRate: 1.0, + integrations: [Sentry.replayIntegration()], + environment: process.env.NEXT_PUBLIC_APP_ENV ?? "development" +}) + +export const onRouterTransitionStart = Sentry.captureRouterTransitionStart diff --git a/src/instrumentation.ts b/src/instrumentation.ts new file mode 100644 index 00000000..8e0b3f26 --- /dev/null +++ b/src/instrumentation.ts @@ -0,0 +1,9 @@ +export async function register() { + if (process.env.NEXT_RUNTIME === "nodejs") { + await import("../sentry.server.config") + } + + if (process.env.NEXT_RUNTIME === "edge") { + await import("../sentry.edge.config") + } +} diff --git a/src/lib/server/auth/index.ts b/src/lib/server/auth/index.ts index d7702732..2265aa50 100644 --- a/src/lib/server/auth/index.ts +++ b/src/lib/server/auth/index.ts @@ -1,5 +1,6 @@ import "server-only" +import * as Sentry from "@sentry/nextjs" import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library" import NextAuth from "next-auth" import DiscordProvider from "next-auth/providers/discord" @@ -37,6 +38,7 @@ const { }, logger: { error(err) { + Sentry.captureException(err) console.error(err) if (err.cause instanceof PrismaClientKnownRequestError) { console.error("Error Metadata", JSON.stringify(err.cause.meta, null, 2)) diff --git a/src/lib/server/trpc/error-handler.ts b/src/lib/server/trpc/error-handler.ts index 3716d447..b5cafece 100644 --- a/src/lib/server/trpc/error-handler.ts +++ b/src/lib/server/trpc/error-handler.ts @@ -1,5 +1,5 @@ +import * as Sentry from "@sentry/nextjs" import type { ProcedureType, TRPCError } from "@trpc/server" -import { DiscordColors, sendDiscordWebhook } from "~/services/discord/webhook" export const trpcErrorHandler = async ({ error, @@ -15,53 +15,13 @@ export const trpcErrorHandler = async ({ }) => { console.error(`❌ tRPC failed on ${path ?? ""}:`, error) - if (process.env.NODE_ENV === "production" && process.env.TRPC_ALERTS_WEBHOOK_URL) { - await sendDiscordWebhook(process.env.TRPC_ALERTS_WEBHOOK_URL, { - embeds: [ - { - color: DiscordColors.RED, - fields: [ - { - name: error.cause?.constructor.name ?? error.name, - value: error.cause?.message ?? error.message, - inline: false - }, - { - name: "Path", - value: `\`${path}\``, - inline: false - }, - { - name: "Input", - value: `\`\`\`json\n${JSON.stringify(input ?? {}, null, 2).slice( - 0, - 1006 - )}\`\`\``, - inline: false - }, - { - name: "Stack Trace", - value: - error.stack - ?.split("\n") - .slice(1, 5) - .map(line => `\`\`\`${line.trim().replaceAll("at ", "")}\`\`\``) - .join("") ?? "", - inline: false - }, - { - name: "Source", - value: source, - inline: false - }, - { - name: "App Version", - value: `\`${process.env.APP_VERSION ?? "N/A"}\``, - inline: false - } - ] - } - ] + // Capture exception with Sentry with additional context + Sentry.withScope(scope => { + scope.setContext("trpc", { + path: path ?? "", + input: input, + source: source }) - } + Sentry.captureException(error) + }) }