presseportale/resources/js/error-reporting.js

128 lines
4.2 KiB
JavaScript

/**
* 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 <meta name="pk-error-reporting">, 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) => {
capture(rejectionToError(event.reason));
});
}