The app is now wired to GlitchTip with the Sentry SDKs
Some checks failed
Production — tag build, push, deploy / build-and-push (push) Successful in 1h13m36s
Production — tag build, push, deploy / deploy (push) Failing after 1m1s

This commit is contained in:
2026-08-31 07:34:07 +03:30
parent 573e5e0886
commit 50eda34e8e
34 changed files with 2495 additions and 70 deletions

View File

@@ -0,0 +1,30 @@
'use client';
import { useEffect } from 'react';
import { useTranslations } from 'next-intl';
import * as Sentry from '@sentry/nextjs';
import { Button } from '@/components/ui/shared/Button';
export default function LocaleError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
const t = useTranslations('common');
useEffect(() => {
Sentry.captureException(error);
}, [error]);
return (
<div className="flex min-h-[50vh] flex-col items-center justify-center gap-4 px-4 text-center">
<h1 className="text-lg font-semibold">{t('pageErrorTitle')}</h1>
<p className="max-w-md text-sm text-text-secondary">{t('pageErrorBody')}</p>
<Button type="button" onClick={() => reset()}>
{t('tryAgain')}
</Button>
</div>
);
}

View File

@@ -0,0 +1,44 @@
'use client';
import { useEffect } from 'react';
import * as Sentry from '@sentry/nextjs';
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
Sentry.captureException(error);
}, [error]);
return (
<html lang="en">
<body>
<div
style={{
display: 'flex',
minHeight: '100vh',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: '1rem',
padding: '1rem',
textAlign: 'center',
fontFamily: 'system-ui, sans-serif',
}}
>
<h1 style={{ fontSize: '1.125rem', fontWeight: 600 }}>Something went wrong</h1>
<p style={{ maxWidth: '28rem', fontSize: '0.875rem' }}>
The page failed to load. You can try again.
</p>
<button type="button" onClick={() => reset()}>
Try again
</button>
</div>
</body>
</html>
);
}

View File

@@ -1,5 +1,6 @@
'use client';
import * as Sentry from '@sentry/nextjs';
import { Component, type ErrorInfo, type ReactNode } from 'react';
interface TodayWidgetErrorBoundaryProps {
@@ -23,6 +24,7 @@ export class TodayWidgetErrorBoundary extends Component<
componentDidCatch(error: Error, info: ErrorInfo) {
console.error('Today widget render error:', error, info);
Sentry.captureException(error, { extra: { componentStack: info.componentStack } });
}
render() {

View File

@@ -0,0 +1,15 @@
import * as Sentry from '@sentry/nextjs';
import { sentrySharedOptions } from '@/lib/error-tracking/sentrySharedOptions';
const dsn = process.env.NEXT_PUBLIC_SENTRY_DSN?.trim();
if (dsn) {
Sentry.init({
dsn,
environment:
process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT?.trim() ||
process.env.NODE_ENV ||
'development',
...sentrySharedOptions(),
});
}

View File

@@ -0,0 +1,9 @@
import * as Sentry from '@sentry/nextjs';
export async function register(): Promise<void> {
if (process.env.NEXT_RUNTIME === 'nodejs') {
await import('./lib/error-tracking/sentry.server.config');
}
}
export const onRequestError = Sentry.captureRequestError;

View File

@@ -2,6 +2,7 @@
import axios, { AxiosError, InternalAxiosRequestConfig } from 'axios';
import type { ApiError } from '@/types/api';
import { notifyAccessTokenRefreshed } from '@/lib/auth/accessTokenEvents';
import { reportUnexpectedApiFailure } from '@/lib/error-tracking/reportUnexpectedApiFailure';
interface CustomAxiosRequestConfig extends InternalAxiosRequestConfig {
_retry?: boolean;
@@ -46,6 +47,7 @@ function shouldSkipRefreshRetry(url: string | undefined): boolean {
apiClient.interceptors.response.use(
(response) => response,
async (error: AxiosError) => {
reportUnexpectedApiFailure(error);
const originalRequest = error.config as CustomAxiosRequestConfig;
if (

View File

@@ -0,0 +1,20 @@
import * as Sentry from '@sentry/nextjs';
import type { AxiosError } from 'axios';
/** Report network failures and HTTP 5xx only — not coded 4xx AppExceptions. */
export function reportUnexpectedApiFailure(error: AxiosError): void {
const status = error.response?.status;
if (status !== undefined && status < 500) {
return;
}
Sentry.captureException(error, {
tags: {
api_status: status ? String(status) : 'network',
},
extra: {
url: error.config?.url,
method: error.config?.method,
},
});
}

View File

@@ -0,0 +1,15 @@
import * as Sentry from '@sentry/nextjs';
import { sentrySharedOptions } from '@/lib/error-tracking/sentrySharedOptions';
const dsn = process.env.NEXT_PUBLIC_SENTRY_DSN?.trim();
if (dsn) {
Sentry.init({
dsn,
environment:
process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT?.trim() ||
process.env.NODE_ENV ||
'development',
...sentrySharedOptions(),
});
}

View File

@@ -0,0 +1,28 @@
import type { ErrorEvent } from '@sentry/core';
/** Shared Sentry/GlitchTip options — no session replay, no PII in payloads. */
export function sentrySharedOptions() {
return {
sendDefaultPii: false as const,
tracesSampleRate: 0,
replaysSessionSampleRate: 0,
replaysOnErrorSampleRate: 0,
beforeSend(event: ErrorEvent): ErrorEvent {
if (event.request) {
delete event.request.cookies;
delete event.request.data;
if (event.request.headers) {
delete event.request.headers.cookie;
delete event.request.headers.authorization;
delete event.request.headers.Authorization;
}
}
if (event.user) {
delete event.user.email;
delete event.user.ip_address;
delete event.user.username;
}
return event;
},
};
}