mivita/app/Support/Monitoring/ErrorSignal.php
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

68 lines
2 KiB
PHP

<?php
namespace App\Support\Monitoring;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Str;
use Sentry\Severity;
use Sentry\State\Scope;
use function Sentry\captureMessage;
use function Sentry\withScope;
/**
* Fehler ohne Exception als GlitchTip-Warnung: tote
* interne Links, verweigerte Zugriffe, abgelaufene Seiten. Gruppiert wird je
* Signal und Ort; je Gruppe gehen höchstens fünf Meldungen in zehn Minuten
* raus, damit ein Crawler mit gefälschtem Referrer GlitchTip nicht flutet.
*/
class ErrorSignal
{
public const DeadInternalLink = 'dead_internal_link';
public const AccessDenied = 'access_denied';
public const PageExpired = 'page_expired';
public const Throttled = 'throttled';
private const Labels = [
self::DeadInternalLink => 'Toter interner Link',
self::AccessDenied => 'Zugriff verweigert (angemeldet)',
self::PageExpired => 'Seite abgelaufen (419)',
self::Throttled => 'Drosselung trifft angemeldeten Nutzer',
];
private const MaxReportsPerWindow = 5;
private const WindowSeconds = 600;
/**
* @param array<string, mixed> $context
*/
public function report(string $signal, string $location, array $context = []): bool
{
$location = Str::limit($location, 200, '');
$reported = RateLimiter::attempt(
'error-signal:'.sha1($signal.'|'.$location),
self::MaxReportsPerWindow,
static fn (): bool => true,
self::WindowSeconds,
);
if ($reported === false) {
return false;
}
withScope(function (Scope $scope) use ($signal, $location, $context): void {
$scope->setFingerprint(['error-signal', $signal, $location]);
$scope->setTag('signal', $signal);
$scope->setContext('signal', $context);
captureMessage((self::Labels[$signal] ?? $signal).': '.$location, Severity::warning());
});
return true;
}
}