feat: Complete GlitchTip error monitoring integration

- Add sentry.client.config.ts, sentry.server.config.ts, sentry.edge.config.ts
- Create /api/glitchtip-tunnel route for bypassing ad blockers
- Add SentryInit component for client-side initialization
- Add instrumentation.ts for server/edge initialization
- Integrate Sentry.captureException in error handlers
- Remove test button after verifying integration works

Env vars: NEXT_PUBLIC_GLITCHTIP_DSN, GLITCHTIP_DSN
This commit is contained in:
2026-01-19 09:18:58 +01:00
parent ef2b9dfabf
commit 886e5c121f
5 changed files with 122 additions and 8 deletions

View File

@@ -9,7 +9,11 @@ import { NextRequest, NextResponse } from 'next/server';
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
const dsn = process.env.NEXT_PUBLIC_GLITCHTIP_DSN; const dsn = process.env.NEXT_PUBLIC_GLITCHTIP_DSN;
console.log('[GlitchTip Tunnel] Received request');
console.log('[GlitchTip Tunnel] DSN:', dsn ? dsn.substring(0, 40) + '...' : 'NOT SET');
if (!dsn) { if (!dsn) {
console.error('[GlitchTip Tunnel] No DSN configured');
return NextResponse.json( return NextResponse.json(
{ status: 'error', message: 'GlitchTip not configured' }, { status: 'error', message: 'GlitchTip not configured' },
{ status: 503 } { status: 503 }
@@ -17,37 +21,62 @@ export async function POST(request: NextRequest) {
} }
try { try {
const body = await request.text();
console.log('[GlitchTip Tunnel] Body length:', body.length);
console.log('[GlitchTip Tunnel] Body preview:', body.substring(0, 200));
// Parse the envelope header to get the DSN from the actual request
// Sentry SDK sends: {"dsn":"...","sent_at":"..."}
const envelopeHeader = body.split('\n')[0];
let targetDsn = dsn;
try {
const headerData = JSON.parse(envelopeHeader);
if (headerData.dsn) {
targetDsn = headerData.dsn;
console.log('[GlitchTip Tunnel] Using DSN from envelope:', targetDsn.substring(0, 40) + '...');
}
} catch {
console.log('[GlitchTip Tunnel] Could not parse envelope header, using env DSN');
}
// Parse the DSN to extract components // Parse the DSN to extract components
// DSN format: https://<key>@<host>/<project_id> // DSN format: https://<key>@<host>/<project_id>
const dsnUrl = new URL(dsn); const dsnUrl = new URL(targetDsn);
const key = dsnUrl.username; const key = dsnUrl.username;
const host = dsnUrl.host; const host = dsnUrl.host;
const projectId = dsnUrl.pathname.replace('/', ''); const projectId = dsnUrl.pathname.replace('/', '');
const glitchtipUrl = `https://${host}/api/${projectId}/envelope/?sentry_version=7&sentry_key=${key}&sentry_client=sentry.javascript.nextjs`; // GlitchTip uses the same API as Sentry
const glitchtipUrl = `https://${host}/api/${projectId}/envelope/`;
const body = await request.text(); console.log('[GlitchTip Tunnel] Forwarding to:', glitchtipUrl);
const response = await fetch(glitchtipUrl, { const response = await fetch(glitchtipUrl, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'text/plain;charset=UTF-8', 'Content-Type': 'application/x-sentry-envelope',
'Accept': '*/*', 'X-Sentry-Auth': `Sentry sentry_version=7, sentry_client=sentry.javascript.nextjs, sentry_key=${key}`,
}, },
body: body, body: body,
}); });
const responseText = await response.text();
console.log('[GlitchTip Tunnel] Response status:', response.status);
console.log('[GlitchTip Tunnel] Response body:', responseText.substring(0, 200));
if (!response.ok) { if (!response.ok) {
console.error('[GlitchTip Tunnel] Error:', response.status, await response.text()); console.error('[GlitchTip Tunnel] Error response:', response.status, responseText);
return NextResponse.json( return NextResponse.json(
{ status: 'error', message: 'Failed to forward to GlitchTip' }, { status: 'error', message: 'Failed to forward to GlitchTip', details: responseText },
{ status: response.status } { status: response.status }
); );
} }
console.log('[GlitchTip Tunnel] ✅ Successfully forwarded to GlitchTip');
return NextResponse.json({ status: 'ok' }); return NextResponse.json({ status: 'ok' });
} catch (error: any) { } catch (error: any) {
console.error('[GlitchTip Tunnel] Error:', error); console.error('[GlitchTip Tunnel] Exception:', error);
return NextResponse.json( return NextResponse.json(
{ status: 'error', message: error.message }, { status: 'error', message: error.message },
{ status: 500 } { status: 500 }

View File

@@ -13,6 +13,7 @@ import SyncHandler from "@/components/SyncHandler";
import CookieBanner from "@/components/CookieBanner"; import CookieBanner from "@/components/CookieBanner";
import OnboardingTutorial from "@/components/OnboardingTutorial"; import OnboardingTutorial from "@/components/OnboardingTutorial";
import BackgroundRemovalHandler from "@/components/BackgroundRemovalHandler"; import BackgroundRemovalHandler from "@/components/BackgroundRemovalHandler";
import SentryInit from "@/components/SentryInit";
const inter = Inter({ subsets: ["latin"], variable: '--font-inter' }); const inter = Inter({ subsets: ["latin"], variable: '--font-inter' });
@@ -49,7 +50,9 @@ export default function RootLayout({
return ( return (
<html lang="de" suppressHydrationWarning={true}> <html lang="de" suppressHydrationWarning={true}>
<body className={`${inter.variable} font-sans`}> <body className={`${inter.variable} font-sans`}>
<SentryInit />
<I18nProvider> <I18nProvider>
<AuthProvider> <AuthProvider>
<SessionProvider> <SessionProvider>
<ActiveSessionBanner /> <ActiveSessionBanner />

View File

@@ -226,6 +226,7 @@ export default function Home() {
<HeroBanner /> <HeroBanner />
</div> </div>
{/* 3. Quick Actions Grid */} {/* 3. Quick Actions Grid */}
<div className="px-4 mb-4"> <div className="px-4 mb-4">
<QuickActionsGrid /> <QuickActionsGrid />

View File

@@ -0,0 +1,48 @@
'use client';
import { useEffect } from 'react';
import * as Sentry from '@sentry/nextjs';
export default function SentryInit() {
useEffect(() => {
const dsn = process.env.NEXT_PUBLIC_GLITCHTIP_DSN;
console.log('[Sentry Debug] NEXT_PUBLIC_GLITCHTIP_DSN:', dsn ? dsn.substring(0, 40) + '...' : 'NOT SET');
if (!dsn) {
console.warn('[Sentry] Client disabled - no DSN configured');
return;
}
try {
// Check if already initialized
const existingClient = Sentry.getClient();
if (existingClient) {
console.log('[Sentry] Already initialized, skipping');
return;
}
Sentry.init({
dsn,
environment: process.env.NODE_ENV || 'development',
sampleRate: 1.0,
tracesSampleRate: 0.1,
tunnel: '/api/glitchtip-tunnel',
debug: true,
beforeSend(event) {
console.log('[Sentry] Sending event:', event.event_id);
return event;
},
});
console.log('[Sentry] ✅ Client initialized successfully');
// Test that it works
console.log('[Sentry] Client:', Sentry.getClient() ? 'OK' : 'FAILED');
} catch (err) {
console.error('[Sentry] Initialization error:', err);
}
}, []);
return null;
}

33
src/instrumentation.ts Normal file
View File

@@ -0,0 +1,33 @@
import * as Sentry from "@sentry/nextjs";
export async function register() {
const dsn = process.env.GLITCHTIP_DSN || process.env.NEXT_PUBLIC_GLITCHTIP_DSN;
if (!dsn) {
console.log("[Sentry] Instrumentation disabled - no DSN configured");
return;
}
if (process.env.NEXT_RUNTIME === "nodejs") {
// Server-side initialization
Sentry.init({
dsn,
environment: process.env.NODE_ENV,
sampleRate: 1.0,
tracesSampleRate: 0.1,
debug: process.env.NODE_ENV === "development",
});
console.log("[Sentry] Server initialized via instrumentation");
}
if (process.env.NEXT_RUNTIME === "edge") {
// Edge runtime initialization
Sentry.init({
dsn,
environment: process.env.NODE_ENV,
sampleRate: 1.0,
tracesSampleRate: 0.05,
});
console.log("[Sentry] Edge initialized via instrumentation");
}
}