Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 28s
Build & Deploy / 🏗️ Build (push) Has been cancelled
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🧪 QA (push) Has been cancelled
- Refactor GlitchtipErrorReportingService to support dynamic DSN and tracesSampleRate - Enable client-side performance tracing by setting tracesSampleRate: 0.1 - Configure production Mail variables and restart containers on alpha.mintel.me
125 lines
4.2 KiB
TypeScript
125 lines
4.2 KiB
TypeScript
import type {
|
|
ErrorReportingLevel,
|
|
ErrorReportingService,
|
|
ErrorReportingUser,
|
|
} from './error-reporting-service';
|
|
import type { NotificationService } from '../notifications/notification-service';
|
|
import type { LoggerService } from '../logging/logger-service';
|
|
|
|
export type GlitchtipErrorReportingServiceOptions = {
|
|
enabled: boolean;
|
|
dsn?: string;
|
|
tracesSampleRate?: number;
|
|
};
|
|
|
|
// GlitchTip speaks the Sentry protocol; @sentry/nextjs can send to GlitchTip via DSN.
|
|
// Sentry is dynamically imported to avoid a ~100KB main-thread execution penalty on initial load.
|
|
export class GlitchtipErrorReportingService implements ErrorReportingService {
|
|
private logger: LoggerService;
|
|
private sentryPromise: Promise<typeof import('@sentry/nextjs')> | null = null;
|
|
|
|
constructor(
|
|
private readonly options: GlitchtipErrorReportingServiceOptions,
|
|
logger: LoggerService,
|
|
private readonly notifications?: NotificationService,
|
|
) {
|
|
this.logger = logger.child({ component: 'error-reporting-glitchtip' });
|
|
|
|
if (this.options.enabled) {
|
|
if (typeof window !== 'undefined') {
|
|
// On client-side, wait until idle before fetching Sentry
|
|
if ('requestIdleCallback' in window) {
|
|
window.requestIdleCallback(() => {
|
|
this.getSentry();
|
|
});
|
|
} else {
|
|
setTimeout(() => {
|
|
this.getSentry();
|
|
}, 3000);
|
|
}
|
|
} else {
|
|
// Pre-fetch on server-side
|
|
this.getSentry();
|
|
}
|
|
}
|
|
}
|
|
|
|
private getSentry(): Promise<typeof import('@sentry/nextjs')> {
|
|
if (!this.sentryPromise) {
|
|
this.sentryPromise = import('@sentry/nextjs').then((Sentry) => {
|
|
// Client-side initialization must happen here since sentry.client.config.ts is empty
|
|
if (typeof window !== 'undefined' && this.options.enabled) {
|
|
Sentry.init({
|
|
dsn: this.options.dsn || 'https://public@errors.infra.mintel.me/1',
|
|
tunnel: '/errors/api/relay',
|
|
enabled: true,
|
|
tracesSampleRate: this.options.tracesSampleRate ?? 0.1,
|
|
replaysOnErrorSampleRate: 1.0,
|
|
replaysSessionSampleRate: 0.1,
|
|
});
|
|
}
|
|
return Sentry;
|
|
});
|
|
}
|
|
return this.sentryPromise;
|
|
}
|
|
|
|
async captureException(error: unknown, context?: Record<string, unknown>) {
|
|
if (!this.options.enabled) return undefined;
|
|
|
|
// Send to Gotify if it's considered critical or if we just want all exceptions there
|
|
if (this.notifications) {
|
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
const contextStr = context ? `\nContext: ${JSON.stringify(context, null, 2)}` : '';
|
|
|
|
await this.notifications.notify({
|
|
title: '🔥 Critical Error Captured',
|
|
message: `Error: ${errorMessage}${contextStr}`,
|
|
priority: 7,
|
|
});
|
|
}
|
|
|
|
const Sentry = await this.getSentry();
|
|
return Sentry.captureException(error, context as any) as any;
|
|
}
|
|
|
|
async captureMessage(message: string, level: ErrorReportingLevel = 'error') {
|
|
if (!this.options.enabled) return undefined;
|
|
|
|
// Send 404s and critical messages straight to Gotify
|
|
if (
|
|
this.notifications &&
|
|
(level === 'error' || level === 'fatal' || message.includes('Route Not Found'))
|
|
) {
|
|
await this.notifications.notify({
|
|
title: level === 'warning' ? '⚠️ Warning Captured' : '🔥 Critical Message Captured',
|
|
message: message,
|
|
priority: level === 'warning' ? 5 : 7,
|
|
});
|
|
}
|
|
|
|
const Sentry = await this.getSentry();
|
|
return Sentry.captureMessage(message, level as any) as any;
|
|
}
|
|
|
|
setUser(user: ErrorReportingUser | null) {
|
|
if (!this.options.enabled) return;
|
|
this.getSentry().then((Sentry) => Sentry.setUser(user as any));
|
|
}
|
|
|
|
setTag(key: string, value: string) {
|
|
if (!this.options.enabled) return;
|
|
this.getSentry().then((Sentry) => Sentry.setTag(key, value));
|
|
}
|
|
|
|
withScope<T>(fn: () => T, _context?: Record<string, unknown>): T {
|
|
if (!this.options.enabled) return fn();
|
|
|
|
// Since withScope mandates executing fn() synchronously to return T,
|
|
// and Sentry load is async, if context mapping is absolutely required
|
|
// for this feature we would need an async API.
|
|
// For now we degrade gracefully by just executing the function.
|
|
return fn();
|
|
}
|
|
}
|