Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:46:44 +08:00
commit d518f4f28d
13 changed files with 1653 additions and 0 deletions

View File

@@ -0,0 +1,71 @@
'use client';
import * as Sentry from '@sentry/nextjs';
import { Component, type ReactNode } from 'react';
interface Props {
children: ReactNode;
fallback?: ReactNode;
}
interface State {
hasError: boolean;
error?: Error;
}
export class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
// Log error to Sentry
Sentry.captureException(error, {
contexts: {
react: {
componentStack: errorInfo.componentStack,
},
},
});
}
render() {
if (this.state.hasError) {
// Render custom fallback UI
if (this.props.fallback) {
return this.props.fallback;
}
return (
<div className="flex min-h-screen items-center justify-center p-4">
<div className="max-w-md rounded-lg border border-red-200 bg-red-50 p-6">
<h2 className="mb-2 text-xl font-semibold text-red-900">
Something went wrong
</h2>
<p className="mb-4 text-red-700">
An error occurred while rendering this component.
</p>
{process.env.NODE_ENV === 'development' && this.state.error && (
<pre className="mb-4 overflow-auto rounded bg-red-100 p-2 text-xs text-red-900">
{this.state.error.message}
</pre>
)}
<button
onClick={() => this.setState({ hasError: false, error: undefined })}
className="rounded bg-red-600 px-4 py-2 text-white hover:bg-red-700"
>
Try again
</button>
</div>
</div>
);
}
return this.props.children;
}
}

View File

@@ -0,0 +1,61 @@
'use client';
import * as Sentry from '@sentry/nextjs';
import { useEffect } from 'react';
export default function ErrorPage({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
// Log error to Sentry
Sentry.captureException(error);
}, [error]);
return (
<div className="flex min-h-screen items-center justify-center p-4">
<div className="max-w-md rounded-lg border p-6">
<h2 className="mb-2 text-2xl font-semibold">Something went wrong</h2>
<p className="mb-4 text-gray-600">
An unexpected error occurred. Please try again.
</p>
{process.env.NODE_ENV === 'development' && (
<details className="mb-4">
<summary className="cursor-pointer text-sm text-gray-500">
Error details
</summary>
<pre className="mt-2 overflow-auto rounded bg-gray-100 p-2 text-xs">
{error.message}
</pre>
{error.digest && (
<p className="mt-2 text-xs text-gray-500">
Error ID: {error.digest}
</p>
)}
</details>
)}
<div className="flex gap-2">
<button
onClick={reset}
className="rounded bg-blue-600 px-4 py-2 text-white hover:bg-blue-700"
>
Try again
</button>
<a
href="/"
className="rounded border border-gray-300 px-4 py-2 hover:bg-gray-50"
>
Go home
</a>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,41 @@
'use client';
import * as Sentry from '@sentry/nextjs';
import { useEffect } from 'react';
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
Sentry.captureException(error);
}, [error]);
return (
<html>
<body>
<div className="flex min-h-screen items-center justify-center p-4">
<div className="max-w-md rounded-lg border p-6">
<h2 className="mb-2 text-2xl font-semibold">Application Error</h2>
<p className="mb-4 text-gray-600">
A critical error occurred. Please refresh the page.
</p>
<div className="flex gap-2">
<button
onClick={reset}
className="rounded bg-blue-600 px-4 py-2 text-white hover:bg-blue-700"
>
Reload page
</button>
</div>
</div>
</div>
</body>
</html>
);
}

View File

@@ -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');
}
}

View File

@@ -0,0 +1,79 @@
import * as Sentry from '@sentry/nextjs';
type LogLevel = 'debug' | 'info' | 'warn' | 'error';
interface LogContext {
[key: string]: unknown;
}
class Logger {
private log(level: LogLevel, message: string, context?: LogContext) {
// Add context as Sentry breadcrumb
if (context) {
Sentry.addBreadcrumb({
category: 'log',
message,
level,
data: context,
});
}
// Also log to console in development
if (process.env.NODE_ENV === 'development') {
const logFn = console[level] || console.log;
logFn(`[${level.toUpperCase()}] ${message}`, context || '');
}
}
debug(message: string, context?: LogContext) {
this.log('debug', message, context);
}
info(message: string, context?: LogContext) {
this.log('info', message, context);
}
warn(message: string, context?: LogContext) {
this.log('warn', message, context);
// Set context for next error
if (context) {
Sentry.setContext('warning', context);
}
}
error(message: string, context?: LogContext & { error?: Error }) {
this.log('error', message, context);
// Capture as Sentry error with context
const error = context?.error || new Error(message);
Sentry.captureException(error, {
level: 'error',
contexts: {
custom: context,
},
});
}
// Set user context for all subsequent logs/errors
setUser(user: { id: string; email?: string; username?: string }) {
Sentry.setUser(user);
}
// Clear user context (e.g., on logout)
clearUser() {
Sentry.setUser(null);
}
// Add custom tags for filtering in Sentry
setTag(key: string, value: string) {
Sentry.setTag(key, value);
}
// Add context that persists across logs
setContext(key: string, context: LogContext) {
Sentry.setContext(key, context);
}
}
export const logger = new Logger();

View File

@@ -0,0 +1,60 @@
import * as Sentry from '@sentry/nextjs';
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
// Set environment
environment: process.env.NODE_ENV,
// Adjust sample rate for production
tracesSampleRate: process.env.NODE_ENV === 'production' ? 0.1 : 1.0,
// Capture 100% of errors
sampleRate: 1.0,
// Disable in development
enabled: process.env.NODE_ENV !== 'development',
// Session Replay - captures user interactions for debugging
replaysSessionSampleRate: 0.1, // 10% of sessions
replaysOnErrorSampleRate: 1.0, // 100% of sessions with errors
integrations: [
Sentry.replayIntegration({
maskAllText: true,
blockAllMedia: true,
}),
],
// Filter out noise
beforeSend(event, hint) {
// Don't send in development
if (process.env.NODE_ENV === 'development') {
return null;
}
// Filter out specific client errors
const error = hint.originalException;
if (error && typeof error === 'object' && 'message' in error) {
const message = String(error.message);
// Common browser errors to ignore
if (
message.includes('ResizeObserver') ||
message.includes('Non-Error promise rejection') ||
message.includes('ChunkLoadError')
) {
return null;
}
}
return event;
},
// Add custom tags
initialScope: {
tags: {
runtime: 'client',
},
},
});

View File

@@ -0,0 +1,56 @@
import * as Sentry from '@sentry/nextjs';
Sentry.init({
dsn: process.env.SENTRY_DSN,
// Set environment
environment: process.env.NODE_ENV,
// Adjust sample rate for production in production
tracesSampleRate: process.env.NODE_ENV === 'production' ? 0.1 : 1.0,
// Capture 100% of errors
sampleRate: 1.0,
// Disable in development to reduce noise
enabled: process.env.NODE_ENV !== 'development',
// Capture Server Actions and API routes
integrations: [
Sentry.rewriteFramesIntegration({
root: process.cwd(),
}),
],
// Filter out noise
beforeSend(event, hint) {
// Don't send errors from development
if (process.env.NODE_ENV === 'development') {
return null;
}
// Filter out specific errors
const error = hint.originalException;
if (error && typeof error === 'object' && 'message' in error) {
const message = String(error.message);
// Ignore known non-critical errors
if (
message.includes('ResizeObserver') ||
message.includes('NotFoundError') ||
message.includes('AbortError')
) {
return null;
}
}
return event;
},
// Add custom tags
initialScope: {
tags: {
runtime: 'server',
},
},
});