mivita/public/js/error-reporting/error-reporting.js
Kevin Adametz 0b02b1b15c GlitchTip: Fehlerprotokoll fuer Server, Fehlersignale und Browser
- sentry/sentry-laravel 4.28; config/sentry.php gehaertet (keine PII,
  keine Request-Bodys, before_send ErrorReportScrubber)
- Handler: Integration::captureUnhandledException, CommandNotFoundException
  nicht melden
- ReportErrorSignals (global): toter interner Link (nach Pfad wegen
  Catch-all-Route), 403/429 fuer Angemeldete, 419; gedrosselt
- Browser: selbst gehostetes SDK (esbuild-Bundle 11.5.0), Loader ohne
  Build-Schritt und ohne Regex-Lookbehind, in 7 Layouts mit area-Tag
- phpunit.xml: DSNs und ntfy-URL leer, CACHE_DRIVER=array (Tests schrieben
  bisher in den lokalen Datei-Cache)
- dev/glitch-tip/README.md mit Instanz, Org, Projekt-IDs, API-Auswertung

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 11:46:36 +00:00

155 lines
5.5 KiB
JavaScript

/**
* Browserfehler an GlitchTip (ohne Build-Schritt, ES2018).
*
* Fehlerfreie Seiten laden nur diese Datei. Das Sentry-SDK (selbst gehostet,
* sentry-browser-<version>.min.js) wird erst beim ersten Fehler nachgeladen
* und meldet dann genau diesen Fehler. Konfiguration kommt aus
* <meta name="mivita-error-reporting">, das die Blade-Komponente
* x-monitoring.browser-errors setzt.
*
* Bewusst ohne Regex-Lookbehind: ältere Safari-Versionen brechen sonst beim
* Parsen ab.
*/
(function () {
'use strict';
var meta = document.querySelector('meta[name="mivita-error-reporting"]');
var config = null;
try {
config = meta ? JSON.parse(meta.content) : null;
} catch (e) {
config = null;
}
if (!config || !config.dsn || !config.sdk) {
return;
}
// wie App\Support\Monitoring\ErrorReportScrubber::AllowedQueryParameters
var ALLOWED_QUERY = ['page', 'q', 'tab', 'sort', 'lang', 'month', 'year'];
var TOKEN_SEGMENT = /^[A-Za-z0-9_-]{32,}$/;
var EMAIL = /[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi;
/** Gleiche Regeln wie ErrorReportScrubber auf dem Server. */
function scrubUrl(value) {
try {
var url = new URL(value, window.location.origin);
url.pathname = url.pathname
.split('/')
.map(function (segment) {
return TOKEN_SEGMENT.test(segment) ? '[Filtered]' : segment;
})
.join('/');
Array.from(url.searchParams.keys()).forEach(function (key) {
if (ALLOWED_QUERY.indexOf(key) === -1 && key.indexOf('utm_') !== 0) {
url.searchParams.set(key, '[Filtered]');
}
});
url.hash = '';
return url.toString().replace(EMAIL, '[email]');
} catch (e) {
return '[Filtered]';
}
}
var sdk = null;
function loadSdk() {
if (sdk) {
return sdk;
}
sdk = new Promise(function (resolve, reject) {
var script = document.createElement('script');
script.src = config.sdk;
script.async = true;
script.onload = function () {
resolve(window.MivitaSentry);
};
script.onerror = reject;
document.head.appendChild(script);
}).then(function (Sentry) {
Sentry.init({
dsn: config.dsn,
release: config.release || undefined,
environment: config.environment,
sampleRate: typeof config.sampleRate === 'number' ? config.sampleRate : 1,
sendDefaultPii: false,
// Eigene Listener unten statt GlobalHandlers (sonst doppelt);
// keine Breadcrumbs (Klicktexte, URLs) und keine Sessions.
integrations: function (defaults) {
return defaults.filter(function (integration) {
return ['GlobalHandlers', 'Breadcrumbs', 'BrowserSession', 'BrowserApiErrors'].indexOf(integration.name) === -1;
});
},
ignoreErrors: ['ResizeObserver loop', 'Script error.', 'AbortError'],
denyUrls: [/^chrome-extension:\/\//, /^moz-extension:\/\//, /^safari-(web-)?extension:\/\//],
beforeSend: function (event) {
if (event.request && event.request.url) {
event.request.url = scrubUrl(event.request.url);
}
if (event.request && event.request.headers && event.request.headers.Referer) {
event.request.headers.Referer = scrubUrl(event.request.headers.Referer);
}
((event.exception && event.exception.values) || []).forEach(function (exception) {
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 kämen als „Error: [object Object]“
* an — Typ und Inhalt mitschicken, damit die Quelle erkennbar ist.
*/
function rejectionToError(reason) {
if (reason instanceof Error) {
return reason;
}
var kind = (reason && reason.constructor && reason.constructor.name) || typeof reason;
var text;
try {
text = typeof reason === 'string' ? reason : JSON.stringify(reason);
} catch (e) {
text = String(reason);
}
return new Error('Unbehandelte Ablehnung (' + kind + '): ' + String(text == null ? '' : text).slice(0, 300));
}
function capture(error) {
loadSdk()
.then(function (Sentry) {
Sentry.captureException(error);
})
.catch(function () {});
}
window.addEventListener('error', function (event) {
// Ressourcenfehler (Bild, Skript) haben kein `error` — nur echte Skriptfehler.
if (event.error) {
capture(event.error);
}
});
window.addEventListener('unhandledrejection', function (event) {
capture(rejectionToError(event.reason));
});
})();