- 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>
155 lines
5.5 KiB
JavaScript
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));
|
|
});
|
|
})();
|