- GlitchTip über die Sentry-SDKs; Datenschutzfilter (nur User-ID, keine
Request-Bodys, Tokens aus URL/Query/Referer, E-Mails aus Meldungen).
Browser-SDK erst beim ersten Fehler nachgeladen.
- ReportErrorSignals: tote interne Links (404 mit eigenem Referrer), 403/429
für Angemeldete, 419 als gedrosselte Warnung.
- /health (Bearer-Token): DB, Cache, Queue-Rückstau, Scheduler-Herzschlag,
Plattenplatz → 200/503; Scheduler und Plattenplatz auch im Systemstatus.
- OperationalAlert zusätzlich per ntfy (JSON-POST), unabhängig von der Mail.
- Verlauf: login_failed (Grund unbekannt/kein Passwort/falsch),
login_lockout, two_factor_failed; IP nur als gekürzter HMAC.
- Umami: Kundenbereich mit eigenem Schalter und identify('user-<id>'),
nie im Admin/bei Impersonation; Query-Filter auch auf den Webseiten
(signierte Links gingen bisher an Umami).
- queue:prune-failed nach 30 Tagen.
- Versionsnummer: config('app.version') aus APP_VERSION oder Datei RELEASE
(git describe --tags), in GlitchTip, /health, Systemstatus, artisan about.
Plan: dev/betrieb/monitoring-plan.md (Entscheidungen M1–M9).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
107 lines
3.6 KiB
JavaScript
107 lines
3.6 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;
|
|
}
|
|
|
|
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(event.reason instanceof Error ? event.reason : new Error(String(event.reason)));
|
|
});
|
|
}
|