Skip to content

Commit 9989060

Browse files
committed
fix: migrate Sentry configuration to Next.js 15 App Router
Resolves critical build failure caused by outdated Sentry configuration incompatible with Next.js 15 App Router. Changes: - Created src/instrumentation.ts for server-side Sentry initialization - Created src/instrumentation-client.ts for client-side initialization - Added src/app/global-error.tsx to handle React rendering errors - Added src/app/not-found.tsx for custom 404 page - Removed deprecated sentry.*.config.ts files - Updated Sentry packages to v10.16.0 - Removed old Sentry import from layout.tsx Technical Details: - Sentry's withSentryConfig wrapper temporarily disabled due to Next.js 15 compatibility issue (generates Pages Router error pages incompatible with App Router) - Implemented manual instrumentation hooks following Next.js App Router patterns - Added onRequestError and onRouterTransitionStart exports - Build now succeeds with proper NODE_ENV=production Testing: - ✅ Production build passes (yarn build:ci) - ✅ All 254 tests passing - ✅ TypeScript compilation successful - ⚠️ ESLint warning in useMartianTime.ts (pre-existing, tracked separately) References: - https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/ - getsentry/sentry-javascript#14526
1 parent 3bb2c4c commit 9989060

File tree

8 files changed

+645
-283
lines changed

8 files changed

+645
-283
lines changed

next.config.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { withSentryConfig } from '@sentry/nextjs';
1+
// import { withSentryConfig } from '@sentry/nextjs';
22
import type { NextConfig } from 'next';
33

44
const nextConfig: NextConfig = {
@@ -46,14 +46,19 @@ const nextConfig: NextConfig = {
4646
},
4747
};
4848

49-
// Sentry options
49+
// Sentry options (currently unused, see note below)
50+
/* eslint-disable @typescript-eslint/no-unused-vars */
5051
const sentryOptions = {
5152
// For all available options, see:
5253
// https://github.com/getsentry/sentry-webpack-plugin#options
5354

5455
// Suppresses source map uploading logs during build
5556
silent: true,
5657

58+
// Disable automatic error page injection (we use custom global-error.tsx)
59+
autoInstrumentServerFunctions: false,
60+
autoInstrumentMiddleware: false,
61+
5762
// Only include if all required values are present
5863
...(process.env.SENTRY_ORG &&
5964
process.env.SENTRY_PROJECT &&
@@ -74,8 +79,16 @@ const sentryOptions = {
7479
telemetry: false,
7580
}),
7681
};
82+
/* eslint-enable @typescript-eslint/no-unused-vars */
7783

78-
// Export with Sentry configuration if DSN is provided (auth token only needed for uploads)
79-
export default process.env.NEXT_PUBLIC_SENTRY_DSN || process.env.SENTRY_DSN
80-
? withSentryConfig(nextConfig, sentryOptions)
81-
: nextConfig;
84+
// Export configuration
85+
// NOTE: Sentry withSentryConfig wrapper temporarily disabled due to Next.js 15 compatibility issue
86+
// The Sentry SDK (v10.16.0) generates Pages Router error pages that use `<Html>` component
87+
// which is not compatible with App Router in Next.js 15.
88+
// Issue: https://github.com/getsentry/sentry-javascript/issues/14526
89+
// Tracking: We've implemented custom instrumentation files and global-error.tsx
90+
// Once Sentry releases a fix, re-enable with:
91+
// export default process.env.NEXT_PUBLIC_SENTRY_DSN || process.env.SENTRY_DSN
92+
// ? withSentryConfig(nextConfig, sentryOptions)
93+
// : nextConfig;
94+
export default nextConfig;

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,8 @@
5656
]
5757
},
5858
"dependencies": {
59-
"@sentry/nextjs": "^10.8.0",
60-
"@sentry/react": "^10.8.0",
59+
"@sentry/nextjs": "^10.16.0",
60+
"@sentry/react": "^10.16.0",
6161
"@tanstack/react-query": "^5.85.9",
6262
"date-fns": "^4.1.0",
6363
"framer-motion": "^12.23.12",

src/app/global-error.tsx

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/**
2+
* Global Error Handler for Next.js App Router
3+
* Catches React rendering errors and reports them to Sentry
4+
* @see https://nextjs.org/docs/app/building-your-application/routing/error-handling#handling-errors-in-root-layouts
5+
*/
6+
7+
'use client';
8+
9+
import * as Sentry from '@sentry/nextjs';
10+
import { useEffect } from 'react';
11+
import Link from 'next/link';
12+
import { AlertTriangle, RefreshCw, Home } from 'lucide-react';
13+
14+
interface GlobalErrorProps {
15+
error: Error & { digest?: string };
16+
reset: () => void;
17+
}
18+
19+
export default function GlobalError({
20+
error,
21+
reset,
22+
}: GlobalErrorProps): React.ReactElement {
23+
useEffect(() => {
24+
// Report error to Sentry
25+
Sentry.captureException(error);
26+
}, [error]);
27+
28+
return (
29+
<html lang="en">
30+
<body>
31+
<div className="flex min-h-screen items-center justify-center bg-slate-950 p-4">
32+
<div className="max-w-md rounded-lg border border-red-800 bg-red-950/20 p-8 text-center backdrop-blur">
33+
<div className="mb-6 flex justify-center">
34+
<AlertTriangle className="h-16 w-16 text-red-400" />
35+
</div>
36+
37+
<h2 className="mb-4 text-2xl font-bold text-red-300">
38+
Something went wrong
39+
</h2>
40+
41+
<p className="mb-6 text-slate-300">
42+
We encountered an unexpected error. This has been reported and
43+
we&apos;ll look into it.
44+
</p>
45+
46+
{process.env.NODE_ENV === 'development' && (
47+
<details className="mb-6 text-left">
48+
<summary className="cursor-pointer text-sm font-medium text-slate-400 hover:text-slate-300">
49+
Technical Details
50+
</summary>
51+
<div className="mt-4 rounded bg-slate-900/50 p-4 font-mono text-xs text-red-300">
52+
<div className="mb-2">
53+
<strong>Error:</strong> {error.name}
54+
</div>
55+
<div className="mb-2">
56+
<strong>Message:</strong> {error.message}
57+
</div>
58+
{error.digest && (
59+
<div className="mb-2">
60+
<strong>Digest:</strong> {error.digest}
61+
</div>
62+
)}
63+
{error.stack && (
64+
<div>
65+
<strong>Stack:</strong>
66+
<pre className="mt-1 whitespace-pre-wrap">
67+
{error.stack}
68+
</pre>
69+
</div>
70+
)}
71+
</div>
72+
</details>
73+
)}
74+
75+
<div className="flex flex-wrap justify-center gap-4">
76+
<button
77+
onClick={() => reset()}
78+
className="flex items-center gap-2 rounded-lg bg-red-600 px-6 py-3 text-white transition-colors hover:bg-red-700 focus:ring-2 focus:ring-red-500 focus:ring-offset-2 focus:ring-offset-slate-900 focus:outline-none"
79+
type="button"
80+
aria-label="Try again"
81+
>
82+
<RefreshCw className="h-4 w-4" />
83+
Try Again
84+
</button>
85+
86+
<Link
87+
href="/"
88+
className="flex items-center gap-2 rounded-lg border border-slate-600 bg-slate-800 px-6 py-3 text-slate-300 transition-colors hover:bg-slate-700 focus:ring-2 focus:ring-slate-500 focus:ring-offset-2 focus:ring-offset-slate-900 focus:outline-none"
89+
aria-label="Go to home page"
90+
>
91+
<Home className="h-4 w-4" />
92+
Go Home
93+
</Link>
94+
</div>
95+
96+
<p className="mt-6 text-sm text-slate-500">
97+
If this problem persists, please{' '}
98+
<a
99+
href="https://github.com/jimmcq/mars-weather-dashboard/issues"
100+
target="_blank"
101+
rel="noopener noreferrer"
102+
className="text-red-400 underline hover:text-red-300"
103+
>
104+
report the issue
105+
</a>
106+
</p>
107+
</div>
108+
</div>
109+
</body>
110+
</html>
111+
);
112+
}

src/app/layout.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,6 @@ import { Geist, Geist_Mono } from 'next/font/google';
33
import './globals.css';
44
import { Providers } from './providers';
55

6-
// Import Sentry configuration
7-
import '../../sentry.client.config';
8-
96
const geistSans = Geist({
107
variable: '--font-geist-sans',
118
subsets: ['latin'],

src/app/not-found.tsx

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/**
2+
* Custom 404 Not Found page for App Router
3+
* @see https://nextjs.org/docs/app/api-reference/file-conventions/not-found
4+
*/
5+
6+
'use client';
7+
8+
import Link from 'next/link';
9+
import { Home, ArrowLeft } from 'lucide-react';
10+
11+
export default function NotFound(): React.ReactElement {
12+
return (
13+
<div className="flex min-h-screen items-center justify-center bg-slate-950 p-4">
14+
<div className="max-w-md rounded-lg border border-slate-700 bg-slate-800/50 p-8 text-center backdrop-blur">
15+
<div className="mb-6">
16+
<h1 className="text-6xl font-bold text-blue-400">404</h1>
17+
</div>
18+
19+
<h2 className="mb-4 text-2xl font-bold text-white">Page Not Found</h2>
20+
21+
<p className="mb-6 text-slate-300">
22+
The page you&apos;re looking for doesn&apos;t exist or has been moved.
23+
</p>
24+
25+
<div className="flex flex-wrap justify-center gap-4">
26+
<Link
27+
href="/"
28+
className="flex items-center gap-2 rounded-lg bg-blue-600 px-6 py-3 text-white transition-colors hover:bg-blue-700 focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:ring-offset-slate-900 focus:outline-none"
29+
>
30+
<Home className="h-4 w-4" />
31+
Go Home
32+
</Link>
33+
34+
<button
35+
onClick={() => window.history.back()}
36+
className="flex items-center gap-2 rounded-lg border border-slate-600 bg-slate-800 px-6 py-3 text-slate-300 transition-colors hover:bg-slate-700 focus:ring-2 focus:ring-slate-500 focus:ring-offset-2 focus:ring-offset-slate-900 focus:outline-none"
37+
type="button"
38+
>
39+
<ArrowLeft className="h-4 w-4" />
40+
Go Back
41+
</button>
42+
</div>
43+
</div>
44+
</div>
45+
);
46+
}

src/instrumentation-client.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/**
2+
* Client-side instrumentation file
3+
* This file configures error monitoring and performance tracking for the browser
4+
* @see https://nextjs.org/docs/app/api-reference/file-conventions/instrumentation-client
5+
*/
6+
7+
import * as Sentry from '@sentry/nextjs';
8+
9+
const SENTRY_DSN = process.env.NEXT_PUBLIC_SENTRY_DSN || process.env.SENTRY_DSN;
10+
11+
// Export router transition hook for navigation instrumentation
12+
export const onRouterTransitionStart = Sentry.captureRouterTransitionStart;
13+
14+
if (SENTRY_DSN) {
15+
Sentry.init({
16+
dsn: SENTRY_DSN,
17+
environment: process.env.NODE_ENV || 'development',
18+
release:
19+
process.env.NEXT_PUBLIC_SENTRY_RELEASE || '[email protected]',
20+
21+
// Performance Monitoring
22+
tracesSampleRate: process.env.NODE_ENV === 'production' ? 0.1 : 1.0,
23+
24+
// Session Replay
25+
replaysSessionSampleRate:
26+
process.env.NODE_ENV === 'production' ? 0.01 : 0.1,
27+
replaysOnErrorSampleRate: 1.0,
28+
29+
// Configure error filtering
30+
ignoreErrors: [
31+
'Non-Error promise rejection captured',
32+
'ResizeObserver loop limit exceeded',
33+
'Script error.',
34+
'Network request failed',
35+
/Rate limit exceeded/,
36+
/429/,
37+
],
38+
39+
// User context
40+
initialScope: {
41+
tags: {
42+
component: 'mars-weather-dashboard',
43+
},
44+
contexts: {
45+
app: {
46+
name: 'Mars Weather Dashboard',
47+
version: '0.1.0',
48+
},
49+
},
50+
},
51+
52+
// Debug mode disabled to reduce console spam
53+
debug: false,
54+
});
55+
}

0 commit comments

Comments
 (0)