/** * Browserfehler an GlitchTip (Monitoring-Plan 1.2, M5). * * Fehlerfreie Seiten laden nur diese paar Zeilen: Das Sentry-SDK wird erst * beim ersten Fehler nachgeladen (eigener Chunk) und meldet dann genau diesen * Fehler. Konfiguration kommt aus , das die * Blade-Komponente x-monitoring.browser-errors setzt. * * Fehlgeschlagene Livewire-Requests (5xx, 419) meldet bereits der Server. */ const config = (() => { const meta = document.querySelector('meta[name="pk-error-reporting"]'); try { return meta ? JSON.parse(meta.content) : null; } catch { return null; } })(); const ALLOWED_QUERY = ['src', 'pr', 'ref', 'page', 'q', 'tab', 'edition']; const TOKEN_SEGMENT = /(?<=\/)[A-Za-z0-9_-]{32,}(?=\/|$)/g; const EMAIL = /[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi; /** Gleiche Regeln wie ErrorReportScrubber auf dem Server. */ function scrubUrl(value) { try { const url = new URL(value, window.location.origin); url.pathname = url.pathname.replace(TOKEN_SEGMENT, '[Filtered]'); for (const key of [...url.searchParams.keys()]) { if (!ALLOWED_QUERY.includes(key) && !key.startsWith('utm_')) { url.searchParams.set(key, '[Filtered]'); } } url.hash = ''; return url.toString().replace(EMAIL, '[email]'); } catch { return '[Filtered]'; } } let sdk = null; function loadSdk() { sdk ??= import('./error-reporting-sdk.js').then((Sentry) => { Sentry.init({ dsn: config.dsn, release: config.release || undefined, environment: config.environment, sampleRate: config.sampleRate ?? 1, sendDefaultPii: false, // Eigene Listener unten statt GlobalHandlers (sonst doppelt); // keine Breadcrumbs (Klicktexte, URLs) und keine Sessions. integrations: (defaults) => defaults.filter( (integration) => !['GlobalHandlers', 'Breadcrumbs', 'BrowserSession', 'BrowserApiErrors'].includes(integration.name), ), ignoreErrors: ['ResizeObserver loop', 'Script error.', 'AbortError'], denyUrls: [/^chrome-extension:\/\//, /^moz-extension:\/\//, /^safari-(web-)?extension:\/\//], beforeSend(event) { if (event.request?.url) { event.request.url = scrubUrl(event.request.url); } if (event.request?.headers?.Referer) { event.request.headers.Referer = scrubUrl(event.request.headers.Referer); } for (const exception of event.exception?.values ?? []) { if (exception.value) { exception.value = exception.value.replace(EMAIL, '[email]'); } } return event; }, }); if (config.userId) { Sentry.setUser({ id: String(config.userId) }); } Sentry.setTag('area', config.area); return Sentry; }); return sdk; } /** * Promise-Ablehnungen mit Objekt statt Error kamen als „Error: [object Object]“ * an (GlitchTip browser-pressekonto, Issue 10) — Typ und Inhalt mitschicken, * damit die Quelle erkennbar ist. */ function rejectionToError(reason) { if (reason instanceof Error) { return reason; } const kind = reason?.constructor?.name ?? typeof reason; let text; try { text = typeof reason === 'string' ? reason : JSON.stringify(reason); } catch { text = String(reason); } return new Error(`Unbehandelte Ablehnung (${kind}): ${String(text ?? '').slice(0, 300)}`); } function capture(error) { loadSdk() .then((Sentry) => Sentry.captureException(error)) .catch(() => {}); } if (config?.dsn) { window.addEventListener('error', (event) => { // Ressourcenfehler (Bild, Skript) haben kein `error` — nur echte Skriptfehler. if (event.error) { capture(event.error); } }); window.addEventListener('unhandledrejection', (event) => { // Alpine bricht laufende x-transition-Übergänge mit diesem Objekt ab // (GlitchTip browser-pressekonto, Issue 18) — kein Fehler. if (event.reason?.isFromCancelledTransition === true) { return; } capture(rejectionToError(event.reason)); }); }