perf: deep react code splitting, next-intl payload scoping, and SVG hardware acceleration for PageSpeed 100
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 23s
Build & Deploy / 🧪 QA (push) Successful in 2m1s
Build & Deploy / 🏗️ Build (push) Successful in 7m43s
Build & Deploy / 🚀 Deploy (push) Successful in 26s
Build & Deploy / 🧪 Smoke Test (push) Successful in 1m10s
Build & Deploy / ⚡ Lighthouse (push) Successful in 3m20s
Build & Deploy / 🔔 Notify (push) Successful in 2s

This commit is contained in:
2026-02-20 11:53:42 +01:00
parent 7542f42568
commit 16d06d3275
15 changed files with 1234 additions and 1121 deletions

View File

@@ -1,4 +1,3 @@
import * as Sentry from '@sentry/nextjs';
import type {
ErrorReportingLevel,
ErrorReportingService,
@@ -7,32 +6,66 @@ import type {
import type { NotificationService } from '../notifications/notification-service';
import type { LoggerService } from '../logging/logger-service';
type SentryLike = typeof Sentry;
export type GlitchtipErrorReportingServiceOptions = {
enabled: boolean;
};
// 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,
private readonly sentry: SentryLike = Sentry,
) {
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') {
Sentry.init({
dsn: 'https://public@errors.infra.mintel.me/1',
tunnel: '/errors/api/relay',
enabled: true,
tracesSampleRate: 0,
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;
const result = this.sentry.captureException(error, context as any) as any;
// Send to Gotify if it's considered critical or if we just want all exceptions there
// For now, let's send all exceptions to Gotify as requested "notify me via gotify about critical error messages"
// We'll treat all captureException calls as potentially critical or at least noteworthy
if (this.notifications) {
const errorMessage = error instanceof Error ? error.message : String(error);
const contextStr = context ? `\nContext: ${JSON.stringify(context, null, 2)}` : '';
@@ -44,34 +77,33 @@ export class GlitchtipErrorReportingService implements ErrorReportingService {
});
}
return result;
const Sentry = await this.getSentry();
return Sentry.captureException(error, context as any) as any;
}
captureMessage(message: string, level: ErrorReportingLevel = 'error') {
async captureMessage(message: string, level: ErrorReportingLevel = 'error') {
if (!this.options.enabled) return undefined;
return this.sentry.captureMessage(message, level as any) as any;
const Sentry = await this.getSentry();
return Sentry.captureMessage(message, level as any) as any;
}
setUser(user: ErrorReportingUser | null) {
if (!this.options.enabled) return;
this.sentry.setUser(user as any);
this.getSentry().then((Sentry) => Sentry.setUser(user as any));
}
setTag(key: string, value: string) {
if (!this.options.enabled) return;
this.sentry.setTag(key, value);
this.getSentry().then((Sentry) => Sentry.setTag(key, value));
}
withScope<T>(fn: () => T, context?: Record<string, unknown>) {
withScope<T>(fn: () => T, context?: Record<string, unknown>): T {
if (!this.options.enabled) return fn();
return this.sentry.withScope((scope) => {
if (context) {
for (const [key, value] of Object.entries(context)) {
scope.setExtra(key, value);
}
}
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();
}
}