- 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>
57 lines
2.8 KiB
PHP
57 lines
2.8 KiB
PHP
@props(['area' => 'web'])
|
|
@php
|
|
// Selbst gehostete, cookielose Webanalyse (Datenschutz Abschnitt 5).
|
|
// Webseiten: UMAMI_ENABLED. Kundenbereich (Monitoring-Plan M2, M6, M7):
|
|
// zusätzlich UMAMI_PORTAL_ENABLED, nur unter `me.*` und nie während einer
|
|
// Impersonation; die Zuordnung zur User-ID nur mit UMAMI_IDENTIFY_USERS.
|
|
// Die Website-ID kommt je Brand aus config/domains.php, `data-tag`
|
|
// trennt Portal und Webseiten in der Auswertung.
|
|
$umamiHost = request()->getHost();
|
|
$umamiBrandKey = str_contains($umamiHost, 'pressekonto')
|
|
? 'pressekonto'
|
|
: (str_contains($umamiHost, 'presseecho') ? 'presseecho' : 'businessportal24');
|
|
$umamiWebsiteId = config('domains.domains.'.$umamiBrandKey.'.brand.umami_website_id');
|
|
$umamiIsPortal = $area === 'portal';
|
|
$umamiEnabled = config('services.umami.enabled') && $umamiWebsiteId && (! $umamiIsPortal || (
|
|
config('services.umami.portal_enabled')
|
|
&& request()->routeIs('me.*')
|
|
&& ! app(\App\Actions\Admin\UserImpersonation::class)->isActive()
|
|
));
|
|
$umamiIdentity = $umamiIsPortal && config('services.umami.identify_users') && auth()->check()
|
|
? 'user-'.auth()->id()
|
|
: null;
|
|
@endphp
|
|
@if ($umamiEnabled)
|
|
{{-- Query-Filter (M8): Nur utm_*, src, pr, ref, page, q, tab gehen raus —
|
|
signierte Links und Tokens nie. Tokenartige Pfadsegmente ebenso. --}}
|
|
<script>
|
|
window.pkUmamiBeforeSend = window.pkUmamiBeforeSend || function (type, payload) {
|
|
var allowed = ['src', 'pr', 'ref', 'page', 'q', 'tab'];
|
|
var scrub = function (value) {
|
|
if (!value) { return value; }
|
|
try {
|
|
var url = new URL(value, window.location.origin);
|
|
var relative = value.charAt(0) === '/';
|
|
url.pathname = url.pathname.replace(/\/[A-Za-z0-9_-]{32,}(?=\/|$)/g, '/[Filtered]');
|
|
Array.from(url.searchParams.keys()).forEach(function (key) {
|
|
if (allowed.indexOf(key) === -1 && key.indexOf('utm_') !== 0) { url.searchParams.delete(key); }
|
|
});
|
|
return relative ? url.pathname + url.search : url.toString();
|
|
} catch (e) {
|
|
return value;
|
|
}
|
|
};
|
|
payload.url = scrub(payload.url);
|
|
payload.referrer = scrub(payload.referrer);
|
|
return payload;
|
|
};
|
|
</script>
|
|
<script defer
|
|
src="{{ config('services.umami.script_url') }}"
|
|
data-website-id="{{ $umamiWebsiteId }}"
|
|
data-tag="{{ $area }}"
|
|
data-before-send="pkUmamiBeforeSend"
|
|
data-performance="true"
|
|
@if ($umamiIdentity) onload="window.umami && window.umami.identify('{{ $umamiIdentity }}')" @endif
|
|
></script>
|
|
@endif
|