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>
This commit is contained in:
parent
36feed6324
commit
0b02b1b15c
28 changed files with 1453 additions and 61 deletions
|
|
@ -167,6 +167,12 @@ Live wird per **Git-Tag** deployt (`bash dev/betrieb/deploy.sh vX.Y.Z [--migrati
|
||||||
- Assets (`public/js`, `public/css`, `public/vendor`, `public/assets`) sind versioniert; auf dem Server wird nichts gebaut.
|
- Assets (`public/js`, `public/css`, `public/vendor`, `public/assets`) sind versioniert; auf dem Server wird nichts gebaut.
|
||||||
- Version: Datei `RELEASE` → `config('app.version')`, `php artisan about --only=mivita`.
|
- Version: Datei `RELEASE` → `config('app.version')`, `php artisan about --only=mivita`.
|
||||||
|
|
||||||
|
## Überwachung / Fehlerprotokoll
|
||||||
|
|
||||||
|
- Fehler laufen nach **GlitchTip** (`https://errors.adametz.media`, Org `adametz-media`, Projekte `server-mivitacare` = 4, `browser-mivitacare` = 3). Auswerten per API mit `GLITCHTIP_API_TOKEN` aus der `.env`: `dev/glitch-tip/README.md`.
|
||||||
|
- Systemstatus: Sysadmin → Systemstatus bzw. `/health` (Token); Alarme bei fehlgeschlagenen geplanten Aufgaben/Jobs per Mail und ntfy (`App\Support\OperationalAlert`). Details: `dev/betrieb/README.md`.
|
||||||
|
- **Tests melden nie nach außen** (`phpunit.xml` leert DSNs und ntfy-URL) — beim Anlegen neuer Tests nichts daran ändern.
|
||||||
|
|
||||||
## Scheduled Tasks (Cron)
|
## Scheduled Tasks (Cron)
|
||||||
|
|
||||||
| Zeit | Command | Beschreibung |
|
| Zeit | Command | Beschreibung |
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@ use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
use Illuminate\Support\Facades\Mail;
|
use Illuminate\Support\Facades\Mail;
|
||||||
use Laravel\Passport\Exceptions\OAuthServerException;
|
use Laravel\Passport\Exceptions\OAuthServerException;
|
||||||
|
use Sentry\Laravel\Integration;
|
||||||
|
use Symfony\Component\Console\Exception\CommandNotFoundException;
|
||||||
use Symfony\Component\ErrorHandler\ErrorRenderer\HtmlErrorRenderer;
|
use Symfony\Component\ErrorHandler\ErrorRenderer\HtmlErrorRenderer;
|
||||||
use Symfony\Component\ErrorHandler\Exception\FlattenException;
|
use Symfony\Component\ErrorHandler\Exception\FlattenException;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
@ -25,6 +27,8 @@ class Handler extends ExceptionHandler
|
||||||
// PAYONE-Transportfehler werden bereits via MyLog protokolliert (inkl. Mail);
|
// PAYONE-Transportfehler werden bereits via MyLog protokolliert (inkl. Mail);
|
||||||
// kein zusätzlicher Exception-Report nötig (entspricht dem alten abort(403)-Verhalten).
|
// kein zusätzlicher Exception-Report nötig (entspricht dem alten abort(403)-Verhalten).
|
||||||
PayoneTransportException::class,
|
PayoneTransportException::class,
|
||||||
|
// Vertippte artisan-Befehle sind kein Anwendungsfehler
|
||||||
|
CommandNotFoundException::class,
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -63,6 +67,9 @@ class Handler extends ExceptionHandler
|
||||||
|
|
||||||
if ($this->shouldReport($exception)) {
|
if ($this->shouldReport($exception)) {
|
||||||
$this->sendEmail($exception);
|
$this->sendEmail($exception);
|
||||||
|
|
||||||
|
// An GlitchTip (ohne SENTRY_LARAVEL_DSN ein No-op)
|
||||||
|
Integration::captureUnhandledException($exception);
|
||||||
}
|
}
|
||||||
parent::report($exception);
|
parent::report($exception);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,8 @@ class Kernel extends HttpKernel
|
||||||
\App\Http\Middleware\TrimStrings::class,
|
\App\Http\Middleware\TrimStrings::class,
|
||||||
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
|
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
|
||||||
\App\Http\Middleware\TrustProxies::class,
|
\App\Http\Middleware\TrustProxies::class,
|
||||||
|
// Fehler ohne Exception (toter interner Link, 403/429 für Angemeldete, 419) an GlitchTip
|
||||||
|
\App\Http\Middleware\ReportErrorSignals::class,
|
||||||
# \App\Http\Middleware\RemoveExcessWhitespaceMiddleware::class,
|
# \App\Http\Middleware\RemoveExcessWhitespaceMiddleware::class,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
||||||
96
app/Http/Middleware/ReportErrorSignals.php
Normal file
96
app/Http/Middleware/ReportErrorSignals.php
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use App\Support\Monitoring\ErrorReportScrubber;
|
||||||
|
use App\Support\Monitoring\ErrorSignal;
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
use function Illuminate\Support\defer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Meldet Antworten, hinter denen ein Bedien- oder Programmierfehler steckt,
|
||||||
|
* obwohl keine Exception gemeldet wird:
|
||||||
|
*
|
||||||
|
* - 404 mit Referrer der eigenen Domain — irgendwo bei uns steht ein toter Link
|
||||||
|
* - 403/429 für angemeldete Nutzer — die Oberfläche führt sie dorthin
|
||||||
|
* - 419 — Formular abgelaufen (CSRF-Token), Eingaben sind verloren
|
||||||
|
*
|
||||||
|
* Gesendet wird nach der Antwort (`defer()`), der Nutzer wartet nicht auf GlitchTip.
|
||||||
|
*/
|
||||||
|
class ReportErrorSignals
|
||||||
|
{
|
||||||
|
public function __construct(private readonly ErrorSignal $errorSignal) {}
|
||||||
|
|
||||||
|
public function handle(Request $request, Closure $next): Response
|
||||||
|
{
|
||||||
|
$response = $next($request);
|
||||||
|
|
||||||
|
$signal = $this->signalFor($request, $response->getStatusCode());
|
||||||
|
|
||||||
|
if ($signal !== null) {
|
||||||
|
$location = $this->locationOf($request, $signal);
|
||||||
|
$context = [
|
||||||
|
'status' => $response->getStatusCode(),
|
||||||
|
'method' => $request->method(),
|
||||||
|
'path' => '/'.ltrim($request->path(), '/'),
|
||||||
|
'referer' => $this->refererPath($request),
|
||||||
|
];
|
||||||
|
|
||||||
|
defer(fn () => $this->errorSignal->report($signal, $location, $context));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function signalFor(Request $request, int $status): ?string
|
||||||
|
{
|
||||||
|
return match (true) {
|
||||||
|
$status === 404 && $this->hasInternalReferer($request) => ErrorSignal::DeadInternalLink,
|
||||||
|
$status === 403 && $request->user() !== null => ErrorSignal::AccessDenied,
|
||||||
|
$status === 419 => ErrorSignal::PageExpired,
|
||||||
|
$status === 429 && $request->user() !== null => ErrorSignal::Throttled,
|
||||||
|
default => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tote Links nach Pfad: auf der Haupt-Domain fängt die Catch-all-Route
|
||||||
|
* `/{site}/{subsite?}/{product_slug?}` fast jeden Pfad — mit dem
|
||||||
|
* Routennamen landeten alle toten Links in einem Eintrag.
|
||||||
|
*/
|
||||||
|
private function locationOf(Request $request, string $signal): string
|
||||||
|
{
|
||||||
|
if ($signal === ErrorSignal::DeadInternalLink) {
|
||||||
|
return '/'.ltrim($request->path(), '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
$routeName = $request->route()?->getName();
|
||||||
|
|
||||||
|
return $routeName !== null
|
||||||
|
? $routeName
|
||||||
|
: '/'.ltrim($request->path(), '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function hasInternalReferer(Request $request): bool
|
||||||
|
{
|
||||||
|
$referer = $request->headers->get('referer');
|
||||||
|
|
||||||
|
return is_string($referer) && parse_url($referer, PHP_URL_HOST) === $request->getHost();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function refererPath(Request $request): ?string
|
||||||
|
{
|
||||||
|
$referer = $request->headers->get('referer');
|
||||||
|
|
||||||
|
if (! is_string($referer) || $referer === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$path = parse_url($referer, PHP_URL_PATH);
|
||||||
|
|
||||||
|
return is_string($path) ? ErrorReportScrubber::scrubPath($path) : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
159
app/Support/Monitoring/ErrorReportScrubber.php
Normal file
159
app/Support/Monitoring/ErrorReportScrubber.php
Normal file
|
|
@ -0,0 +1,159 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Support\Monitoring;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Sentry\Event;
|
||||||
|
use Sentry\EventHint;
|
||||||
|
use Sentry\UserDataBag;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Letzte Station vor GlitchTip (`before_send` in config/sentry.php).
|
||||||
|
*
|
||||||
|
* Personenbezug nur über die interne User-ID. Tokens und Signaturen stecken
|
||||||
|
* in Query-Strings (signierte Links) und in Pfaden
|
||||||
|
* (`/reset-password/{token}`, Magic Links) — beides wird ersetzt, bevor das
|
||||||
|
* Ereignis den Server verlässt. E-Mail-Adressen in Fehlermeldungen (z. B.
|
||||||
|
* QueryException mit eingesetzten Bindings) ebenso.
|
||||||
|
*/
|
||||||
|
class ErrorReportScrubber
|
||||||
|
{
|
||||||
|
public const Filtered = '[Filtered]';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query-Parameter, die unverändert mitgehen dürfen (`utm_*` immer).
|
||||||
|
* Dieselbe Liste steht in public/js/error-reporting/error-reporting.js.
|
||||||
|
*
|
||||||
|
* @var list<string>
|
||||||
|
*/
|
||||||
|
public const AllowedQueryParameters = ['page', 'q', 'tab', 'sort', 'lang', 'month', 'year'];
|
||||||
|
|
||||||
|
private const EmailPattern = '/[A-Z0-9._%+\-]+@[A-Z0-9.\-]+\.[A-Z]{2,}/i';
|
||||||
|
|
||||||
|
/** Pfadsegmente, die wie ein Token aussehen (Magic Links, Reset-Links). */
|
||||||
|
private const TokenSegmentPattern = '#(?<=/)[A-Za-z0-9_\-]{32,}(?=/|$)#';
|
||||||
|
|
||||||
|
public static function beforeSend(Event $event, ?EventHint $hint = null): ?Event
|
||||||
|
{
|
||||||
|
$userId = self::authenticatedUserId();
|
||||||
|
|
||||||
|
$event->setUser($userId !== null ? UserDataBag::createFromUserIdentifier($userId) : null);
|
||||||
|
$event->setRequest(self::scrubRequest($event->getRequest()));
|
||||||
|
|
||||||
|
foreach ($event->getExceptions() as $exception) {
|
||||||
|
$exception->setValue(self::scrubText($exception->getValue()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($event->getMessage() !== null) {
|
||||||
|
$formatted = $event->getMessageFormatted();
|
||||||
|
|
||||||
|
$event->setMessage(
|
||||||
|
self::scrubText($event->getMessage()),
|
||||||
|
$event->getMessageParams(),
|
||||||
|
$formatted !== null ? self::scrubText($formatted) : null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $event;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $request
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public static function scrubRequest(array $request): array
|
||||||
|
{
|
||||||
|
if (isset($request['url']) && is_string($request['url'])) {
|
||||||
|
$request['url'] = self::scrubUrl($request['url']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($request['query_string']) && is_string($request['query_string'])) {
|
||||||
|
$request['query_string'] = self::scrubQuery($request['query_string']);
|
||||||
|
}
|
||||||
|
|
||||||
|
unset($request['data'], $request['cookies']);
|
||||||
|
|
||||||
|
foreach ($request['headers'] ?? [] as $name => $values) {
|
||||||
|
if (strtolower((string) $name) === 'referer') {
|
||||||
|
$request['headers'][$name] = array_map(
|
||||||
|
static fn (mixed $value): string => self::scrubUrl((string) $value),
|
||||||
|
(array) $values,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $request;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function scrubUrl(string $url): string
|
||||||
|
{
|
||||||
|
$query = parse_url($url, PHP_URL_QUERY);
|
||||||
|
$withoutQuery = $query !== null && $query !== false ? Str::before($url, '?') : $url;
|
||||||
|
$scrubbedPath = self::scrubPath(self::scrubPathTokens($withoutQuery));
|
||||||
|
|
||||||
|
return $query !== null && $query !== false && $query !== ''
|
||||||
|
? $scrubbedPath.'?'.self::scrubQuery($query)
|
||||||
|
: $scrubbedPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function scrubQuery(string $query): string
|
||||||
|
{
|
||||||
|
parse_str($query, $parameters);
|
||||||
|
|
||||||
|
$scrubbed = [];
|
||||||
|
foreach ($parameters as $name => $value) {
|
||||||
|
$isAllowed = in_array((string) $name, self::AllowedQueryParameters, true)
|
||||||
|
|| str_starts_with((string) $name, 'utm_');
|
||||||
|
|
||||||
|
$scrubbed[$name] = $isAllowed && is_string($value) ? $value : self::Filtered;
|
||||||
|
}
|
||||||
|
|
||||||
|
return http_build_query($scrubbed, '', '&', PHP_QUERY_RFC3986);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function scrubPath(string $path): string
|
||||||
|
{
|
||||||
|
return self::scrubText((string) preg_replace(self::TokenSegmentPattern, self::Filtered, $path));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function scrubText(string $text): string
|
||||||
|
{
|
||||||
|
return (string) preg_replace(self::EmailPattern, '[email]', $text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ersetzt Routenparameter, deren Name auf ein Geheimnis hindeutet
|
||||||
|
* (`token`, `hash`, `signature`), im Pfad der aktuellen Route.
|
||||||
|
*/
|
||||||
|
private static function scrubPathTokens(string $url): string
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$route = request()->route();
|
||||||
|
} catch (Throwable) {
|
||||||
|
return $url;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($route === null) {
|
||||||
|
return $url;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($route->parameters() as $name => $value) {
|
||||||
|
if (is_string($value) && $value !== '' && Str::contains(strtolower((string) $name), ['token', 'hash', 'signature'])) {
|
||||||
|
$url = str_replace(rawurlencode($value), self::Filtered, str_replace($value, self::Filtered, $url));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $url;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function authenticatedUserId(): int|string|null
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
return Auth::id();
|
||||||
|
} catch (Throwable) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
68
app/Support/Monitoring/ErrorSignal.php
Normal file
68
app/Support/Monitoring/ErrorSignal.php
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
<?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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -23,6 +23,7 @@
|
||||||
"laravel/tinker": "^2.9",
|
"laravel/tinker": "^2.9",
|
||||||
"laravel/ui": "^4.2",
|
"laravel/ui": "^4.2",
|
||||||
"maatwebsite/excel": "^3.1",
|
"maatwebsite/excel": "^3.1",
|
||||||
|
"sentry/sentry-laravel": "^4.28",
|
||||||
"setasign/fpdf": "^1.8.6",
|
"setasign/fpdf": "^1.8.6",
|
||||||
"setasign/fpdi": "^2.6",
|
"setasign/fpdi": "^2.6",
|
||||||
"spatie/laravel-html": "^3.7",
|
"spatie/laravel-html": "^3.7",
|
||||||
|
|
|
||||||
382
composer.lock
generated
382
composer.lock
generated
|
|
@ -4,7 +4,7 @@
|
||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "2df0d325d151049579625a307ff864af",
|
"content-hash": "e6e519d35f9743e47b8c08379da6f25e",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "barryvdh/laravel-dompdf",
|
"name": "barryvdh/laravel-dompdf",
|
||||||
|
|
@ -1990,6 +1990,66 @@
|
||||||
],
|
],
|
||||||
"time": "2026-05-01T08:20:10+00:00"
|
"time": "2026-05-01T08:20:10+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "jean85/pretty-package-versions",
|
||||||
|
"version": "2.1.1",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/Jean85/pretty-package-versions.git",
|
||||||
|
"reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/4d7aa5dab42e2a76d99559706022885de0e18e1a",
|
||||||
|
"reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"composer-runtime-api": "^2.1.0",
|
||||||
|
"php": "^7.4|^8.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"friendsofphp/php-cs-fixer": "^3.2",
|
||||||
|
"jean85/composer-provided-replaced-stub-package": "^1.0",
|
||||||
|
"phpstan/phpstan": "^2.0",
|
||||||
|
"phpunit/phpunit": "^7.5|^8.5|^9.6",
|
||||||
|
"rector/rector": "^2.0",
|
||||||
|
"vimeo/psalm": "^4.3 || ^5.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-master": "1.x-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Jean85\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Alessandro Lai",
|
||||||
|
"email": "alessandro.lai85@gmail.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "A library to get pretty versions strings of installed dependencies",
|
||||||
|
"keywords": [
|
||||||
|
"composer",
|
||||||
|
"package",
|
||||||
|
"release",
|
||||||
|
"versions"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/Jean85/pretty-package-versions/issues",
|
||||||
|
"source": "https://github.com/Jean85/pretty-package-versions/tree/2.1.1"
|
||||||
|
},
|
||||||
|
"time": "2025-03-19T14:43:43+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "jenssegers/date",
|
"name": "jenssegers/date",
|
||||||
"version": "v4.0.0",
|
"version": "v4.0.0",
|
||||||
|
|
@ -5989,6 +6049,195 @@
|
||||||
},
|
},
|
||||||
"time": "2025-07-11T13:20:48+00:00"
|
"time": "2025-07-11T13:20:48+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "sentry/sentry",
|
||||||
|
"version": "4.34.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/getsentry/sentry-php.git",
|
||||||
|
"reference": "3d8f4e56becb10ede64e2ac6f8077f6434715461"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/getsentry/sentry-php/zipball/3d8f4e56becb10ede64e2ac6f8077f6434715461",
|
||||||
|
"reference": "3d8f4e56becb10ede64e2ac6f8077f6434715461",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-curl": "*",
|
||||||
|
"ext-json": "*",
|
||||||
|
"ext-mbstring": "*",
|
||||||
|
"guzzlehttp/psr7": "^1.8.4|^2.1.1|^3.0.0",
|
||||||
|
"jean85/pretty-package-versions": "^1.5|^2.0.4",
|
||||||
|
"php": "^7.2|^8.0",
|
||||||
|
"psr/log": "^1.0|^2.0|^3.0",
|
||||||
|
"symfony/options-resolver": "^4.4.30|^5.0.11|^6.0|^7.0|^8.0"
|
||||||
|
},
|
||||||
|
"conflict": {
|
||||||
|
"raven/raven": "*"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"carthage-software/mago": "1.30.0",
|
||||||
|
"friendsofphp/php-cs-fixer": "^3.4",
|
||||||
|
"guzzlehttp/guzzle": "^7.0|^8.0",
|
||||||
|
"guzzlehttp/promises": "^2.0.3|^3.0.0",
|
||||||
|
"monolog/monolog": "^1.6|^2.0|^3.0",
|
||||||
|
"nyholm/psr7": "^1.8",
|
||||||
|
"open-telemetry/api": "^1.1",
|
||||||
|
"open-telemetry/context": "^1.1",
|
||||||
|
"open-telemetry/exporter-otlp": "^1.1",
|
||||||
|
"open-telemetry/gen-otlp-protobuf": "^1.2.1",
|
||||||
|
"open-telemetry/sdk": "^1.1",
|
||||||
|
"open-telemetry/sem-conv": "^1.27",
|
||||||
|
"phpstan/phpstan": "^2.2",
|
||||||
|
"phpunit/phpunit": "^8.5.52|^9.6.34",
|
||||||
|
"spiral/roadrunner-http": "^3.6",
|
||||||
|
"spiral/roadrunner-worker": "^3.6"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"ext-excimer": "Enable Sentry profiling with the Excimer PHP extension.",
|
||||||
|
"monolog/monolog": "Allow sending log messages to Sentry by using the included Monolog handler."
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"files": [
|
||||||
|
"src/functions.php"
|
||||||
|
],
|
||||||
|
"psr-4": {
|
||||||
|
"Sentry\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Sentry",
|
||||||
|
"email": "accounts@sentry.io"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "PHP SDK for Sentry (http://sentry.io)",
|
||||||
|
"homepage": "http://sentry.io",
|
||||||
|
"keywords": [
|
||||||
|
"crash-reporting",
|
||||||
|
"crash-reports",
|
||||||
|
"error-handler",
|
||||||
|
"error-monitoring",
|
||||||
|
"log",
|
||||||
|
"logging",
|
||||||
|
"profiling",
|
||||||
|
"sentry",
|
||||||
|
"tracing"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/getsentry/sentry-php/issues",
|
||||||
|
"source": "https://github.com/getsentry/sentry-php/tree/4.34.0"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://sentry.io/",
|
||||||
|
"type": "custom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://sentry.io/pricing/",
|
||||||
|
"type": "custom"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2026-10-08T11:25:58+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "sentry/sentry-laravel",
|
||||||
|
"version": "4.28.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/getsentry/sentry-laravel.git",
|
||||||
|
"reference": "34bf18fbf99b884a9bfbb46957d0178d2d0b988e"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/getsentry/sentry-laravel/zipball/34bf18fbf99b884a9bfbb46957d0178d2d0b988e",
|
||||||
|
"reference": "34bf18fbf99b884a9bfbb46957d0178d2d0b988e",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"illuminate/support": "^6.0 | ^7.0 | ^8.0 | ^9.0 | ^10.0 | ^11.0 | ^12.0 | ^13.0",
|
||||||
|
"nyholm/psr7": "^1.0",
|
||||||
|
"php": "^7.2 | ^8.0",
|
||||||
|
"sentry/sentry": "^4.31.0",
|
||||||
|
"symfony/psr-http-message-bridge": "^1.0 | ^2.0 | ^6.0 | ^7.0 | ^8.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"friendsofphp/php-cs-fixer": "^3.11",
|
||||||
|
"guzzlehttp/guzzle": "^7.2",
|
||||||
|
"laravel/ai": "^0.1.5",
|
||||||
|
"laravel/folio": "^1.1",
|
||||||
|
"laravel/framework": "^6.0 | ^7.0 | ^8.0 | ^9.0 | ^10.0 | ^11.0 | ^12.0 | ^13.0",
|
||||||
|
"laravel/octane": "^2.15",
|
||||||
|
"laravel/pennant": "^1.0",
|
||||||
|
"livewire/livewire": "^2.0 | ^3.0 | ^4.0",
|
||||||
|
"mockery/mockery": "^1.3",
|
||||||
|
"orchestra/testbench": "^4.7 | ^5.1 | ^6.0 | ^7.0 | ^8.0 | ^9.0 | ^10.0 | ^11.0",
|
||||||
|
"phpstan/phpstan": "^1.10",
|
||||||
|
"phpunit/phpunit": "^8.5 | ^9.6 | ^10.4 | ^11.5"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"laravel": {
|
||||||
|
"aliases": {
|
||||||
|
"Sentry": "Sentry\\Laravel\\Facade"
|
||||||
|
},
|
||||||
|
"providers": [
|
||||||
|
"Sentry\\Laravel\\ServiceProvider",
|
||||||
|
"Sentry\\Laravel\\Tracing\\ServiceProvider"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-0": {
|
||||||
|
"Sentry\\Laravel\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Sentry",
|
||||||
|
"email": "accounts@sentry.io"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Laravel SDK for Sentry (https://sentry.io)",
|
||||||
|
"homepage": "https://sentry.io",
|
||||||
|
"keywords": [
|
||||||
|
"crash-reporting",
|
||||||
|
"crash-reports",
|
||||||
|
"error-handler",
|
||||||
|
"error-monitoring",
|
||||||
|
"laravel",
|
||||||
|
"log",
|
||||||
|
"logging",
|
||||||
|
"profiling",
|
||||||
|
"sentry",
|
||||||
|
"tracing"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/getsentry/sentry-laravel/issues",
|
||||||
|
"source": "https://github.com/getsentry/sentry-laravel/tree/4.28.0"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://sentry.io/",
|
||||||
|
"type": "custom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://sentry.io/pricing/",
|
||||||
|
"type": "custom"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2026-09-23T14:29:19+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "setasign/fpdf",
|
"name": "setasign/fpdf",
|
||||||
"version": "1.9.0",
|
"version": "1.9.0",
|
||||||
|
|
@ -7243,6 +7492,77 @@
|
||||||
],
|
],
|
||||||
"time": "2026-05-23T16:22:37+00:00"
|
"time": "2026-05-23T16:22:37+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "symfony/options-resolver",
|
||||||
|
"version": "v8.1.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/symfony/options-resolver.git",
|
||||||
|
"reference": "88f9c561f678a02d54b897014049fa839e33ff82"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/symfony/options-resolver/zipball/88f9c561f678a02d54b897014049fa839e33ff82",
|
||||||
|
"reference": "88f9c561f678a02d54b897014049fa839e33ff82",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=8.4.1",
|
||||||
|
"symfony/deprecation-contracts": "^2.5|^3"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Symfony\\Component\\OptionsResolver\\": ""
|
||||||
|
},
|
||||||
|
"exclude-from-classmap": [
|
||||||
|
"/Tests/"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Fabien Potencier",
|
||||||
|
"email": "fabien@symfony.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Symfony Community",
|
||||||
|
"homepage": "https://symfony.com/contributors"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Provides an improved replacement for the array_replace PHP function",
|
||||||
|
"homepage": "https://symfony.com",
|
||||||
|
"keywords": [
|
||||||
|
"config",
|
||||||
|
"configuration",
|
||||||
|
"options"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/symfony/options-resolver/tree/v8.1.0"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://symfony.com/sponsor",
|
||||||
|
"type": "custom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/fabpot",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/nicolas-grekas",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||||
|
"type": "tidelift"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2026-05-29T05:06:50+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/polyfill-ctype",
|
"name": "symfony/polyfill-ctype",
|
||||||
"version": "v1.37.0",
|
"version": "v1.37.0",
|
||||||
|
|
@ -9790,66 +10110,6 @@
|
||||||
},
|
},
|
||||||
"time": "2026-06-16T14:57:46+00:00"
|
"time": "2026-06-16T14:57:46+00:00"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "jean85/pretty-package-versions",
|
|
||||||
"version": "2.1.1",
|
|
||||||
"source": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/Jean85/pretty-package-versions.git",
|
|
||||||
"reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a"
|
|
||||||
},
|
|
||||||
"dist": {
|
|
||||||
"type": "zip",
|
|
||||||
"url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/4d7aa5dab42e2a76d99559706022885de0e18e1a",
|
|
||||||
"reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a",
|
|
||||||
"shasum": ""
|
|
||||||
},
|
|
||||||
"require": {
|
|
||||||
"composer-runtime-api": "^2.1.0",
|
|
||||||
"php": "^7.4|^8.0"
|
|
||||||
},
|
|
||||||
"require-dev": {
|
|
||||||
"friendsofphp/php-cs-fixer": "^3.2",
|
|
||||||
"jean85/composer-provided-replaced-stub-package": "^1.0",
|
|
||||||
"phpstan/phpstan": "^2.0",
|
|
||||||
"phpunit/phpunit": "^7.5|^8.5|^9.6",
|
|
||||||
"rector/rector": "^2.0",
|
|
||||||
"vimeo/psalm": "^4.3 || ^5.0"
|
|
||||||
},
|
|
||||||
"type": "library",
|
|
||||||
"extra": {
|
|
||||||
"branch-alias": {
|
|
||||||
"dev-master": "1.x-dev"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"autoload": {
|
|
||||||
"psr-4": {
|
|
||||||
"Jean85\\": "src/"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"notification-url": "https://packagist.org/downloads/",
|
|
||||||
"license": [
|
|
||||||
"MIT"
|
|
||||||
],
|
|
||||||
"authors": [
|
|
||||||
{
|
|
||||||
"name": "Alessandro Lai",
|
|
||||||
"email": "alessandro.lai85@gmail.com"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "A library to get pretty versions strings of installed dependencies",
|
|
||||||
"keywords": [
|
|
||||||
"composer",
|
|
||||||
"package",
|
|
||||||
"release",
|
|
||||||
"versions"
|
|
||||||
],
|
|
||||||
"support": {
|
|
||||||
"issues": "https://github.com/Jean85/pretty-package-versions/issues",
|
|
||||||
"source": "https://github.com/Jean85/pretty-package-versions/tree/2.1.1"
|
|
||||||
},
|
|
||||||
"time": "2025-03-19T14:43:43+00:00"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "laravel/boost",
|
"name": "laravel/boost",
|
||||||
"version": "v1.8.13",
|
"version": "v1.8.13",
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,24 @@ return [
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Browserfehler
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Eigenes GlitchTip-Projekt für JavaScript-Fehler. Die DSN ist öffentlich
|
||||||
|
| (steht im HTML), sie erlaubt nur das Einliefern von Ereignissen. Ohne
|
||||||
|
| DSN wird kein Skript eingebunden. Das SDK liegt selbst gehostet unter
|
||||||
|
| public/js/error-reporting/sentry-browser-<sdk_version>.min.js.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'browser' => [
|
||||||
|
'dsn' => env('SENTRY_BROWSER_DSN'),
|
||||||
|
'sample_rate' => (float) env('SENTRY_BROWSER_SAMPLE_RATE', 1.0),
|
||||||
|
'sdk_version' => '11.5.0',
|
||||||
|
],
|
||||||
|
|
||||||
'scheduler' => [
|
'scheduler' => [
|
||||||
'heartbeat_warn_minutes' => 3,
|
'heartbeat_warn_minutes' => 3,
|
||||||
'heartbeat_critical_minutes' => 10,
|
'heartbeat_critical_minutes' => 10,
|
||||||
|
|
|
||||||
180
config/sentry.php
Normal file
180
config/sentry.php
Normal file
|
|
@ -0,0 +1,180 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Support\Monitoring\ErrorReportScrubber;
|
||||||
|
use App\Support\Monitoring\Release;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sentry Laravel SDK configuration file.
|
||||||
|
*
|
||||||
|
* Ziel ist das selbst gehostete GlitchTip, das das
|
||||||
|
* Sentry-Protokoll spricht. Ohne DSN ist das SDK ein No-op. Abweichend vom
|
||||||
|
* Paket-Standard: kein Personenbezug außer der User-ID, keine
|
||||||
|
* Request-Bodys, keine Log-/Cache-Breadcrumbs (Lognachrichten tragen bei uns
|
||||||
|
* E-Mail-Adressen), keine Metriken (versteht GlitchTip nicht).
|
||||||
|
*
|
||||||
|
* @see https://docs.sentry.io/platforms/php/guides/laravel/configuration/options/
|
||||||
|
*/
|
||||||
|
return [
|
||||||
|
|
||||||
|
// @see https://docs.sentry.io/concepts/key-terms/dsn-explainer/
|
||||||
|
'dsn' => env('SENTRY_LARAVEL_DSN', env('SENTRY_DSN')),
|
||||||
|
|
||||||
|
// @see https://spotlightjs.com/
|
||||||
|
// 'spotlight' => env('SENTRY_SPOTLIGHT', false),
|
||||||
|
|
||||||
|
// @see: https://docs.sentry.io/platforms/php/guides/laravel/configuration/options/#logger
|
||||||
|
// 'logger' => Sentry\Logger\DebugFileLogger::class, // By default this will log to `storage_path('logs/sentry.log')`
|
||||||
|
|
||||||
|
// The release version of your application
|
||||||
|
// Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD'))
|
||||||
|
// Gleiche Kennung wie config('app.version'): SENTRY_RELEASE bzw.
|
||||||
|
// APP_VERSION, sonst die beim Deploy geschriebene Datei RELEASE.
|
||||||
|
'release' => Release::current(env('SENTRY_RELEASE', env('APP_VERSION'))),
|
||||||
|
|
||||||
|
// When left empty or `null` the Laravel environment will be used (usually discovered from `APP_ENV` in your `.env`)
|
||||||
|
'environment' => env('SENTRY_ENVIRONMENT'),
|
||||||
|
|
||||||
|
// Override the organization ID used for trace continuation checks.
|
||||||
|
'org_id' => env('SENTRY_ORG_ID') === null ? null : (int) env('SENTRY_ORG_ID'),
|
||||||
|
|
||||||
|
// @see: https://docs.sentry.io/platforms/php/guides/laravel/configuration/options/#sample_rate
|
||||||
|
'sample_rate' => env('SENTRY_SAMPLE_RATE') === null ? 1.0 : (float) env('SENTRY_SAMPLE_RATE'),
|
||||||
|
|
||||||
|
// @see: https://docs.sentry.io/platforms/php/guides/laravel/configuration/options/#traces_sample_rate
|
||||||
|
'traces_sample_rate' => env('SENTRY_TRACES_SAMPLE_RATE') === null ? null : (float) env('SENTRY_TRACES_SAMPLE_RATE'),
|
||||||
|
|
||||||
|
// @see: https://docs.sentry.io/platforms/php/guides/laravel/configuration/options/#profiles_sample_rate
|
||||||
|
'profiles_sample_rate' => env('SENTRY_PROFILES_SAMPLE_RATE') === null ? null : (float) env('SENTRY_PROFILES_SAMPLE_RATE'),
|
||||||
|
|
||||||
|
// Only continue incoming traces when the organization IDs are compatible with this SDK instance.
|
||||||
|
'strict_trace_continuation' => env('SENTRY_STRICT_TRACE_CONTINUATION', false),
|
||||||
|
|
||||||
|
// @see: https://docs.sentry.io/platforms/php/guides/laravel/configuration/options/#enable_logs
|
||||||
|
'enable_logs' => env('SENTRY_ENABLE_LOGS', false),
|
||||||
|
|
||||||
|
// @see: https://docs.sentry.io/platforms/php/guides/laravel/configuration/options/#enable_metrics
|
||||||
|
'enable_metrics' => env('SENTRY_ENABLE_METRICS', false),
|
||||||
|
|
||||||
|
// @see: https://docs.sentry.io/platforms/php/guides/laravel/configuration/options/#log_flush_threshold
|
||||||
|
'log_flush_threshold' => env('SENTRY_LOG_FLUSH_THRESHOLD') === null ? null : (int) env('SENTRY_LOG_FLUSH_THRESHOLD'),
|
||||||
|
|
||||||
|
// The minimum log level that will be sent to Sentry as logs using the `sentry_logs` logging channel
|
||||||
|
'logs_channel_level' => env('SENTRY_LOG_LEVEL', env('SENTRY_LOGS_LEVEL', env('LOG_LEVEL', 'debug'))),
|
||||||
|
|
||||||
|
// @see: https://docs.sentry.io/platforms/php/guides/laravel/configuration/options/#send_default_pii
|
||||||
|
'send_default_pii' => false,
|
||||||
|
|
||||||
|
// Formular- und JSON-Inhalte verlassen den Server nie.
|
||||||
|
'max_request_body_size' => 'never',
|
||||||
|
|
||||||
|
// User-ID statt Person, Tokens aus URL/Query/Referer, E-Mails aus Meldungen.
|
||||||
|
'before_send' => [ErrorReportScrubber::class, 'beforeSend'],
|
||||||
|
|
||||||
|
// @see: https://docs.sentry.io/platforms/php/guides/laravel/configuration/options/#ignore_exceptions
|
||||||
|
// 'ignore_exceptions' => [],
|
||||||
|
|
||||||
|
// @see: https://docs.sentry.io/platforms/php/guides/laravel/configuration/options/#ignore_transactions
|
||||||
|
'ignore_transactions' => [
|
||||||
|
// Ignore Laravel's default health URL
|
||||||
|
'/up',
|
||||||
|
'/health',
|
||||||
|
],
|
||||||
|
|
||||||
|
// Breadcrumb specific configuration
|
||||||
|
'breadcrumbs' => [
|
||||||
|
// Capture Laravel logs as breadcrumbs
|
||||||
|
'logs' => env('SENTRY_BREADCRUMBS_LOGS_ENABLED', false),
|
||||||
|
|
||||||
|
// Capture Laravel cache events (hits, writes etc.) as breadcrumbs
|
||||||
|
'cache' => env('SENTRY_BREADCRUMBS_CACHE_ENABLED', false),
|
||||||
|
|
||||||
|
// Capture Livewire components like routes as breadcrumbs
|
||||||
|
'livewire' => env('SENTRY_BREADCRUMBS_LIVEWIRE_ENABLED', true),
|
||||||
|
|
||||||
|
// Capture SQL queries as breadcrumbs
|
||||||
|
'sql_queries' => env('SENTRY_BREADCRUMBS_SQL_QUERIES_ENABLED', true),
|
||||||
|
|
||||||
|
// Capture SQL query bindings (parameters) in SQL query breadcrumbs
|
||||||
|
'sql_bindings' => env('SENTRY_BREADCRUMBS_SQL_BINDINGS_ENABLED', false),
|
||||||
|
|
||||||
|
// Capture queue job information as breadcrumbs
|
||||||
|
'queue_info' => env('SENTRY_BREADCRUMBS_QUEUE_INFO_ENABLED', true),
|
||||||
|
|
||||||
|
// Capture command information as breadcrumbs
|
||||||
|
'command_info' => env('SENTRY_BREADCRUMBS_COMMAND_JOBS_ENABLED', true),
|
||||||
|
|
||||||
|
// Capture HTTP client request information as breadcrumbs
|
||||||
|
'http_client_requests' => env('SENTRY_BREADCRUMBS_HTTP_CLIENT_REQUESTS_ENABLED', true),
|
||||||
|
|
||||||
|
// Capture send notifications as breadcrumbs
|
||||||
|
'notifications' => env('SENTRY_BREADCRUMBS_NOTIFICATIONS_ENABLED', true),
|
||||||
|
],
|
||||||
|
|
||||||
|
// Performance monitoring specific configuration
|
||||||
|
'tracing' => [
|
||||||
|
// Trace queue jobs as their own transactions (this enables tracing for queue jobs)
|
||||||
|
'queue_job_transactions' => env('SENTRY_TRACE_QUEUE_ENABLED', true),
|
||||||
|
|
||||||
|
// Capture queue jobs as spans when executed on the sync driver
|
||||||
|
'queue_jobs' => env('SENTRY_TRACE_QUEUE_JOBS_ENABLED', true),
|
||||||
|
|
||||||
|
// Capture SQL queries as spans
|
||||||
|
'sql_queries' => env('SENTRY_TRACE_SQL_QUERIES_ENABLED', true),
|
||||||
|
|
||||||
|
// Capture SQL query bindings (parameters) in SQL query spans
|
||||||
|
'sql_bindings' => env('SENTRY_TRACE_SQL_BINDINGS_ENABLED', false),
|
||||||
|
|
||||||
|
// Capture where the SQL query originated from on the SQL query spans
|
||||||
|
'sql_origin' => env('SENTRY_TRACE_SQL_ORIGIN_ENABLED', true),
|
||||||
|
|
||||||
|
// Define a threshold in milliseconds for SQL queries to resolve their origin
|
||||||
|
'sql_origin_threshold_ms' => env('SENTRY_TRACE_SQL_ORIGIN_THRESHOLD_MS', 100),
|
||||||
|
|
||||||
|
// Capture views rendered as spans
|
||||||
|
'views' => env('SENTRY_TRACE_VIEWS_ENABLED', true),
|
||||||
|
|
||||||
|
// Capture Livewire components as spans
|
||||||
|
'livewire' => env('SENTRY_TRACE_LIVEWIRE_ENABLED', true),
|
||||||
|
|
||||||
|
// Capture HTTP client requests as spans
|
||||||
|
'http_client_requests' => env('SENTRY_TRACE_HTTP_CLIENT_REQUESTS_ENABLED', true),
|
||||||
|
|
||||||
|
// Capture Laravel cache events (hits, writes etc.) as spans
|
||||||
|
'cache' => env('SENTRY_TRACE_CACHE_ENABLED', true),
|
||||||
|
|
||||||
|
// Capture Redis operations as spans (this enables Redis events in Laravel)
|
||||||
|
'redis_commands' => env('SENTRY_TRACE_REDIS_COMMANDS', false),
|
||||||
|
|
||||||
|
// Capture where the Redis command originated from on the Redis command spans
|
||||||
|
'redis_origin' => env('SENTRY_TRACE_REDIS_ORIGIN_ENABLED', true),
|
||||||
|
|
||||||
|
// Capture send notifications as spans
|
||||||
|
'notifications' => env('SENTRY_TRACE_NOTIFICATIONS_ENABLED', true),
|
||||||
|
|
||||||
|
// Enable tracing for requests without a matching route (404's)
|
||||||
|
'missing_routes' => env('SENTRY_TRACE_MISSING_ROUTES_ENABLED', false),
|
||||||
|
|
||||||
|
// Configures if the performance trace should continue after the response has been sent to the user until the application terminates
|
||||||
|
// This is required to capture any spans that are created after the response has been sent like queue jobs dispatched using `dispatch(...)->afterResponse()` for example
|
||||||
|
'continue_after_response' => env('SENTRY_TRACE_CONTINUE_AFTER_RESPONSE', true),
|
||||||
|
|
||||||
|
// Capture AI agent interactions as spans (requires laravel/ai)
|
||||||
|
'gen_ai' => env('SENTRY_TRACE_GEN_AI_ENABLED', true),
|
||||||
|
|
||||||
|
// Capture AI invoke_agent spans
|
||||||
|
'gen_ai_invoke_agent' => env('SENTRY_TRACE_GEN_AI_INVOKE_AGENT_ENABLED', true),
|
||||||
|
|
||||||
|
// Capture AI chat spans
|
||||||
|
'gen_ai_chat' => env('SENTRY_TRACE_GEN_AI_CHAT_ENABLED', true),
|
||||||
|
|
||||||
|
// Capture AI execute_tool spans
|
||||||
|
'gen_ai_execute_tool' => env('SENTRY_TRACE_GEN_AI_EXECUTE_TOOL_ENABLED', true),
|
||||||
|
|
||||||
|
// Capture AI embeddings spans
|
||||||
|
'gen_ai_embeddings' => env('SENTRY_TRACE_GEN_AI_EMBEDDINGS_ENABLED', true),
|
||||||
|
|
||||||
|
// Enable the tracing integrations supplied by Sentry (recommended)
|
||||||
|
'default_integrations' => env('SENTRY_TRACE_DEFAULT_INTEGRATIONS_ENABLED', true),
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
|
|
@ -245,6 +245,11 @@ wirklich läuft. Steht er nach dem Deploy auf „Kritisch“, ist in Ploi kein S
|
||||||
|
|
||||||
Schwellen in `.env` (optional): `MONITORING_QUEUE_BACKLOG_SECONDS=300`, `MONITORING_DISK_MIN_FREE_PERCENT=10`.
|
Schwellen in `.env` (optional): `MONITORING_QUEUE_BACKLOG_SECONDS=300`, `MONITORING_DISK_MIN_FREE_PERCENT=10`.
|
||||||
|
|
||||||
|
**Fehlerprotokoll GlitchTip:** Exceptions, Fehlersignale (toter interner Link, 403/429, 419) und
|
||||||
|
Browserfehler — Einrichtung, Werte und Auswertung in [`dev/glitch-tip/README.md`](../glitch-tip/README.md).
|
||||||
|
Live-`.env`: `SENTRY_LARAVEL_DSN`, `SENTRY_BROWSER_DSN`, `SENTRY_ENVIRONMENT=production`,
|
||||||
|
`MONITORING_NTFY_URL`, `MONITORING_NTFY_TOPIC`, `MONITORING_NTFY_TOKEN`.
|
||||||
|
|
||||||
**Alarm-Mails** (`App\Support\OperationalAlert`): Schlägt eine geplante Aufgabe fehl (Exit-Code ≠ 0,
|
**Alarm-Mails** (`App\Support\OperationalAlert`): Schlägt eine geplante Aufgabe fehl (Exit-Code ≠ 0,
|
||||||
z. B. `business:store-optimized`, `user:make_abo_order`) oder ein Queue-Job endgültig, geht eine Mail an
|
z. B. `business:store-optimized`, `user:make_abo_order`) oder ein Queue-Job endgültig, geht eine Mail an
|
||||||
`MONITORING_ALERT_EMAIL` (Standard: `EXCEPTION_MAIL`). Gleiche Meldungen höchstens einmal pro Stunde
|
`MONITORING_ALERT_EMAIL` (Standard: `EXCEPTION_MAIL`). Gleiche Meldungen höchstens einmal pro Stunde
|
||||||
|
|
|
||||||
90
dev/glitch-tip/README.md
Normal file
90
dev/glitch-tip/README.md
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
# GlitchTip — Fehlerprotokoll mivita.care
|
||||||
|
|
||||||
|
**Stand:** 08.10.2026 · Hintergrund und Regeln: [`dev/git-deploy-monitoring/02-fehlerprotokoll-glitchtip.md`](../git-deploy-monitoring/02-fehlerprotokoll-glitchtip.md)
|
||||||
|
|
||||||
|
| | Wert |
|
||||||
|
|---|---|
|
||||||
|
| Instanz | `https://errors.adametz.media` (selbst gehostet, gemeinsam mit anderen Projekten) |
|
||||||
|
| Organisation | `adametz-media` |
|
||||||
|
| Projekt Server (PHP/Laravel) | `server-mivitacare`, ID **4** → `SENTRY_LARAVEL_DSN` |
|
||||||
|
| Projekt Browser (JavaScript) | `browser-mivitacare`, ID **3** → `SENTRY_BROWSER_DSN` |
|
||||||
|
| Umgebungen | `production` (Live), `local` (Entwicklung, nur zum Testen) |
|
||||||
|
| Push (ntfy) | `https://push.adametz.media`, Topic in `MONITORING_NTFY_TOPIC` |
|
||||||
|
|
||||||
|
## Was gemeldet wird
|
||||||
|
|
||||||
|
| Quelle | Wie |
|
||||||
|
|---|---|
|
||||||
|
| Exceptions (Request, Job, Command) | `App\Exceptions\Handler::report()` → `Integration::captureUnhandledException()`; Laravels `dontReport` gilt (404, Validierung, Auth), zusätzlich `PayoneTransportException`, `CommandNotFoundException` |
|
||||||
|
| Fehler ohne Exception | `App\Http\Middleware\ReportErrorSignals` (global): 404 mit eigenem Referrer (nach **Pfad**, nicht Route — wegen der Catch-all-Route), 403/429 für Angemeldete, 419; höchstens 5 je 10 Min. und Ort |
|
||||||
|
| Browserfehler | `public/js/error-reporting/error-reporting.js` über `<x-monitoring.browser-errors area="…" />` in 7 Layouts (`crm`, `crm-auth`, `portal`, `portal-auth`, `portal-public`, `web`, `shop`) |
|
||||||
|
| Betriebs-Alarme | `App\Support\OperationalAlert` → Log, Mail, ntfy (siehe `dev/betrieb/README.md`, Abschnitt G) |
|
||||||
|
|
||||||
|
Datenschutz: `App\Support\Monitoring\ErrorReportScrubber` (Server) und dieselben Regeln in
|
||||||
|
`error-reporting.js` (Browser): nur User-ID, keine Request-Bodys/Cookies, Tokens und nicht erlaubte
|
||||||
|
Query-Parameter als `[Filtered]`, E-Mail-Adressen als `[email]`. Erlaubte Query-Parameter:
|
||||||
|
`page, q, tab, sort, lang, month, year` (+ `utm_*`) — Liste an beiden Stellen gleich halten.
|
||||||
|
|
||||||
|
Tests melden nie an das echte GlitchTip/ntfy (`phpunit.xml`, Wächtertest in `ErrorReportingTest`).
|
||||||
|
|
||||||
|
## Browser-SDK (selbst gehostet)
|
||||||
|
|
||||||
|
Kein Drittanbieter-CDN (sonst ginge die IP jedes Besuchers an Sentry). `public/js/error-reporting/`:
|
||||||
|
|
||||||
|
- `error-reporting.js` — Listener, lädt das SDK erst beim ersten Fehler; ES2018, **ohne Regex-Lookbehind**
|
||||||
|
(ältere Safari-Versionen brechen sonst beim Parsen ab — Test prüft das)
|
||||||
|
- `sentry-browser-<version>.min.js` — Version in `config('monitoring.browser.sdk_version')`
|
||||||
|
|
||||||
|
Neu bauen (z. B. Update auf eine neue SDK-Version), außerhalb des Projekts:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir /tmp/sentry-build && cd /tmp/sentry-build && npm init -y >/dev/null
|
||||||
|
npm install --ignore-scripts --save-exact @sentry/browser@<version> esbuild
|
||||||
|
echo "export { captureException, init, setTag, setUser } from '@sentry/browser';" > entry.js
|
||||||
|
npx esbuild entry.js --bundle --minify --format=iife --global-name=MivitaSentry --target=es2018 \
|
||||||
|
--legal-comments=none --outfile=sentry-browser-<version>.min.js
|
||||||
|
```
|
||||||
|
|
||||||
|
Datei nach `public/js/error-reporting/` kopieren, `sdk_version` in `config/monitoring.php` anpassen,
|
||||||
|
alte Datei löschen.
|
||||||
|
|
||||||
|
## Auswerten per API
|
||||||
|
|
||||||
|
Lese-Token (`project:read`, `event:read`) liegt lokal als `GLITCHTIP_API_TOKEN` in der `.env` — nie
|
||||||
|
ausgeben, nie committen.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
T=$(grep -E "^GLITCHTIP_API_TOKEN=" .env | cut -d= -f2- | tr -d "'\"")
|
||||||
|
B=https://errors.adametz.media/api/0
|
||||||
|
|
||||||
|
# Offene Einträge, nur Live (project=4 Server, project=3 Browser)
|
||||||
|
curl -s -H "Authorization: Bearer $T" \
|
||||||
|
"$B/organizations/adametz-media/issues/?project=4&query=is:unresolved&environment=production&limit=100" \
|
||||||
|
| python3 -c "
|
||||||
|
import json,sys
|
||||||
|
for i in json.load(sys.stdin):
|
||||||
|
print(i['id'], i['count'], i['firstSeen'][:16], i['lastSeen'][:16], '|', (i['title'] or '')[:100])"
|
||||||
|
|
||||||
|
# Letztes Ereignis eines Eintrags (Umgebung, Release, Bereich, Stacktrace)
|
||||||
|
curl -s -H "Authorization: Bearer $T" "$B/issues/<ID>/events/latest/"
|
||||||
|
```
|
||||||
|
|
||||||
|
Einordnen und Vorgehen: Abschnitt 7 in `02-fehlerprotokoll-glitchtip.md`. Auflösen in GlitchTip von
|
||||||
|
Hand (Token ist nur lesend).
|
||||||
|
|
||||||
|
## Testen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Server (Umgebung laut SENTRY_ENVIRONMENT)
|
||||||
|
php artisan tinker --execute 'report(new RuntimeException("GlitchTip-Test"));'
|
||||||
|
# Alarm (Mail + ntfy)
|
||||||
|
php artisan tinker --execute 'App\Support\OperationalAlert::send("Test-Alarm");'
|
||||||
|
```
|
||||||
|
|
||||||
|
Browser (Konsole auf einer Seite der App, z. B. CRM):
|
||||||
|
|
||||||
|
```js
|
||||||
|
setTimeout(() => { throw new Error('Browser-Test test@example.com') });
|
||||||
|
```
|
||||||
|
|
||||||
|
→ erscheint im Projekt `browser-mivitacare`, E-Mail als `[email]`.
|
||||||
|
|
@ -27,6 +27,8 @@
|
||||||
<env name="APP_MAINTENANCE_DRIVER" value="file"/>
|
<env name="APP_MAINTENANCE_DRIVER" value="file"/>
|
||||||
<env name="BCRYPT_ROUNDS" value="4"/>
|
<env name="BCRYPT_ROUNDS" value="4"/>
|
||||||
<env name="CACHE_STORE" value="array"/>
|
<env name="CACHE_STORE" value="array"/>
|
||||||
|
<!-- config/cache.php liest CACHE_DRIVER — sonst schreiben Tests in den lokalen Datei-Cache -->
|
||||||
|
<env name="CACHE_DRIVER" value="array" force="true"/>
|
||||||
<env name="DB_CONNECTION" value="sqlite" force="true"/>
|
<env name="DB_CONNECTION" value="sqlite" force="true"/>
|
||||||
<env name="DB_DATABASE" value=":memory:" force="true"/>
|
<env name="DB_DATABASE" value=":memory:" force="true"/>
|
||||||
<server name="DB_CONNECTION" value="sqlite" force="true"/>
|
<server name="DB_CONNECTION" value="sqlite" force="true"/>
|
||||||
|
|
@ -38,6 +40,11 @@
|
||||||
<env name="TELESCOPE_ENABLED" value="false"/>
|
<env name="TELESCOPE_ENABLED" value="false"/>
|
||||||
<env name="VIES_ENABLED" value="false"/>
|
<env name="VIES_ENABLED" value="false"/>
|
||||||
<env name="BASIC_AUTH_ENABLED" value="false"/>
|
<env name="BASIC_AUTH_ENABLED" value="false"/>
|
||||||
|
<!-- Tests provozieren Fehler absichtlich — nie an das echte GlitchTip -->
|
||||||
|
<env name="SENTRY_LARAVEL_DSN" value="" force="true"/>
|
||||||
|
<env name="SENTRY_DSN" value="" force="true"/>
|
||||||
|
<env name="SENTRY_BROWSER_DSN" value="" force="true"/>
|
||||||
|
<env name="MONITORING_NTFY_URL" value="" force="true"/>
|
||||||
<env name="PAYONE_KEY_HASH" value="698fb2555f8b2efc74f60b2121421f45"/>
|
<env name="PAYONE_KEY_HASH" value="698fb2555f8b2efc74f60b2121421f45"/>
|
||||||
<env name="STRIPE_SECRET" value="sk_test_fake_suite_key"/>
|
<env name="STRIPE_SECRET" value="sk_test_fake_suite_key"/>
|
||||||
<env name="STRIPE_KEY" value="pk_test_fake_suite_key"/>
|
<env name="STRIPE_KEY" value="pk_test_fake_suite_key"/>
|
||||||
|
|
|
||||||
155
public/js/error-reporting/error-reporting.js
Normal file
155
public/js/error-reporting/error-reporting.js
Normal file
|
|
@ -0,0 +1,155 @@
|
||||||
|
/**
|
||||||
|
* 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));
|
||||||
|
});
|
||||||
|
})();
|
||||||
15
public/js/error-reporting/sentry-browser-11.5.0.min.js
vendored
Normal file
15
public/js/error-reporting/sentry-browser-11.5.0.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,15 @@
|
||||||
|
@props(['area' => 'web'])
|
||||||
|
{{-- Browserfehler an GlitchTip. Ohne SENTRY_BROWSER_DSN wird nichts eingebunden;
|
||||||
|
das SDK selbst lädt erst beim ersten Fehler (public/js/error-reporting/). --}}
|
||||||
|
@if (filled(config('monitoring.browser.dsn')))
|
||||||
|
<meta name="mivita-error-reporting" content="{{ json_encode([
|
||||||
|
'dsn' => config('monitoring.browser.dsn'),
|
||||||
|
'sdk' => asset('js/error-reporting/sentry-browser-'.config('monitoring.browser.sdk_version').'.min.js'),
|
||||||
|
'release' => config('sentry.release'),
|
||||||
|
'environment' => config('sentry.environment') ?? app()->environment(),
|
||||||
|
'sampleRate' => config('monitoring.browser.sample_rate'),
|
||||||
|
'userId' => auth()->id(),
|
||||||
|
'area' => $area,
|
||||||
|
]) }}">
|
||||||
|
<script src="{{ asset('js/error-reporting/error-reporting.js') }}?v={{ config('app.version') ?? config('monitoring.browser.sdk_version') }}" defer></script>
|
||||||
|
@endif
|
||||||
|
|
@ -54,6 +54,7 @@
|
||||||
<link rel="stylesheet" type="text/css" href="{{ asset('/css/cookieconsent.min.css') }}" />
|
<link rel="stylesheet" type="text/css" href="{{ asset('/css/cookieconsent.min.css') }}" />
|
||||||
<script src="{{ asset('/js/jquery.min.js') }}"></script>
|
<script src="{{ asset('/js/jquery.min.js') }}"></script>
|
||||||
|
|
||||||
|
<x-monitoring.browser-errors area="crm" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,7 @@
|
||||||
})});
|
})});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<x-monitoring.browser-errors area="crm-auth" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,7 @@
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<x-monitoring.browser-errors area="portal" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,7 @@
|
||||||
})});
|
})});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<x-monitoring.browser-errors area="portal-auth" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@
|
||||||
|
|
||||||
<script src="{{ asset('/vendor/js/layout-helpers.js') }}"></script>
|
<script src="{{ asset('/vendor/js/layout-helpers.js') }}"></script>
|
||||||
<script src="{{ asset('/js/jquery.min.js') }}"></script>
|
<script src="{{ asset('/js/jquery.min.js') }}"></script>
|
||||||
|
<x-monitoring.browser-errors area="portal-public" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|
|
||||||
|
|
@ -88,6 +88,7 @@
|
||||||
color: #fff !important;
|
color: #fff !important;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
<x-monitoring.browser-errors area="web" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="smoothscroll enable-animation">
|
<body class="smoothscroll enable-animation">
|
||||||
|
|
|
||||||
|
|
@ -119,6 +119,7 @@
|
||||||
border-color: #989a9c;
|
border-color: #989a9c;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
<x-monitoring.browser-errors area="shop" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="smoothscroll enable-animation">
|
<body class="smoothscroll enable-animation">
|
||||||
|
|
|
||||||
60
tests/Feature/Monitoring/BrowserErrorReportingTest.php
Normal file
60
tests/Feature/Monitoring/BrowserErrorReportingTest.php
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\User;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Illuminate\Support\Facades\Blade;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
uses(TestCase::class, RefreshDatabase::class);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Browserfehler: Meta-Konfiguration und Skript nur mit DSN; die User-ID ist
|
||||||
|
* der einzige Personenbezug. Das SDK ist selbst gehostet (kein Drittanbieter).
|
||||||
|
*/
|
||||||
|
test('nothing is rendered without a browser dsn', function () {
|
||||||
|
config()->set('monitoring.browser.dsn', null);
|
||||||
|
|
||||||
|
expect(trim(Blade::render('<x-monitoring.browser-errors />')))->toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the configuration carries dsn, release, sdk and the user id only', function () {
|
||||||
|
config()->set('monitoring.browser.dsn', 'https://public@glitchtip.test/2');
|
||||||
|
config()->set('sentry.release', 'v1.3.0');
|
||||||
|
$user = User::forceCreate(['email' => 'kunde@example.com', 'password' => bcrypt('secret'), 'lang' => 'de']);
|
||||||
|
$this->actingAs($user);
|
||||||
|
|
||||||
|
$html = Blade::render('<x-monitoring.browser-errors area="portal" />');
|
||||||
|
|
||||||
|
preg_match('/<meta name="mivita-error-reporting" content="([^"]+)"/', $html, $match);
|
||||||
|
$config = json_decode(html_entity_decode($match[1]), true);
|
||||||
|
|
||||||
|
expect($config)->toMatchArray([
|
||||||
|
'dsn' => 'https://public@glitchtip.test/2',
|
||||||
|
'release' => 'v1.3.0',
|
||||||
|
'userId' => $user->id,
|
||||||
|
'area' => 'portal',
|
||||||
|
])
|
||||||
|
->and($config['sdk'])->toEndWith('/js/error-reporting/sentry-browser-'.config('monitoring.browser.sdk_version').'.min.js')
|
||||||
|
->and($html)->toContain('js/error-reporting/error-reporting.js')
|
||||||
|
->and($html)->not->toContain('kunde@example.com');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the self-hosted sdk and the loader exist and the loader avoids regex lookbehind', function () {
|
||||||
|
$loader = file_get_contents(public_path('js/error-reporting/error-reporting.js'));
|
||||||
|
|
||||||
|
expect(public_path('js/error-reporting/sentry-browser-'.config('monitoring.browser.sdk_version').'.min.js'))->toBeFile()
|
||||||
|
->and($loader)->not->toContain('(?<=')
|
||||||
|
->and($loader)->not->toContain('(?<!');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('every page layout includes the browser error reporting', function (string $layout) {
|
||||||
|
expect(file_get_contents(resource_path("views/{$layout}.blade.php")))->toContain('<x-monitoring.browser-errors');
|
||||||
|
})->with([
|
||||||
|
'layouts/application',
|
||||||
|
'layouts/auth',
|
||||||
|
'portal/layouts/application',
|
||||||
|
'portal/layouts/auth',
|
||||||
|
'portal/layouts/public',
|
||||||
|
'web/layouts/application',
|
||||||
|
'web/user/layouts/application',
|
||||||
|
]);
|
||||||
104
tests/Feature/Monitoring/ErrorReportingTest.php
Normal file
104
tests/Feature/Monitoring/ErrorReportingTest.php
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Support\Monitoring\ErrorReportScrubber;
|
||||||
|
use App\User;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
use Sentry\Event;
|
||||||
|
use Sentry\ExceptionDataBag;
|
||||||
|
use Sentry\SentrySdk;
|
||||||
|
use Symfony\Component\Console\Exception\CommandNotFoundException;
|
||||||
|
use Tests\Support\FakeGlitchTip;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
uses(TestCase::class, RefreshDatabase::class);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GlitchTip-Anbindung: gemeldete Exceptions gehen raus, aber nur mit
|
||||||
|
* User-ID, ohne Tokens und ohne E-Mail-Adressen.
|
||||||
|
*/
|
||||||
|
beforeEach(function () {
|
||||||
|
config()->set('app.exception_mail', null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('reported exceptions reach glitchtip with the user id only', function () {
|
||||||
|
$glitchTip = FakeGlitchTip::install();
|
||||||
|
$user = User::forceCreate(['email' => 'kunde@example.com', 'password' => bcrypt('secret'), 'lang' => 'de']);
|
||||||
|
$this->actingAs($user);
|
||||||
|
|
||||||
|
report(new RuntimeException('Import für kunde@example.com fehlgeschlagen'));
|
||||||
|
|
||||||
|
expect($glitchTip->events)->toHaveCount(1);
|
||||||
|
|
||||||
|
$event = $glitchTip->events[0];
|
||||||
|
expect($event->getUser()?->getId())->toBe($user->id)
|
||||||
|
->and($event->getUser()?->getEmail())->toBeNull()
|
||||||
|
->and($event->getExceptions()[0]->getValue())->toBe('Import für [email] fehlgeschlagen');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('exceptions laravel does not report stay out of glitchtip', function () {
|
||||||
|
$glitchTip = FakeGlitchTip::install();
|
||||||
|
|
||||||
|
$this->get('/diese/seite/gibt-es/nicht')->assertNotFound();
|
||||||
|
|
||||||
|
expect($glitchTip->events)->toBe([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a mistyped artisan command is not reported', function () {
|
||||||
|
$glitchTip = FakeGlitchTip::install();
|
||||||
|
|
||||||
|
report(new CommandNotFoundException('Command "config:cachels" is not defined.'));
|
||||||
|
|
||||||
|
expect($glitchTip->events)->toBe([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('request data loses tokens, signatures and bodies', function () {
|
||||||
|
$scrubbed = ErrorReportScrubber::scrubRequest([
|
||||||
|
'url' => 'https://my.mivita.care/einladung/5?signature=abc123&expires=1700000000&page=2',
|
||||||
|
'query_string' => 'signature=abc123&expires=1700000000&page=2&utm_source=nl',
|
||||||
|
'data' => ['password' => 'geheim'],
|
||||||
|
'cookies' => ['mivita_session' => 'x'],
|
||||||
|
'headers' => ['referer' => ['https://my.mivita.care/password/reset/'.str_repeat('a', 64).'?email=kunde@example.com']],
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect($scrubbed['url'])->toBe('https://my.mivita.care/einladung/5?signature=%5BFiltered%5D&expires=%5BFiltered%5D&page=2')
|
||||||
|
->and($scrubbed['query_string'])->toBe('signature=%5BFiltered%5D&expires=%5BFiltered%5D&page=2&utm_source=nl')
|
||||||
|
->and($scrubbed)->not->toHaveKeys(['data', 'cookies'])
|
||||||
|
->and($scrubbed['headers']['referer'][0])->toBe('https://my.mivita.care/password/reset/[Filtered]?email=%5BFiltered%5D');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('route parameters named like secrets are removed from the url', function () {
|
||||||
|
Route::middleware('web')->get('/_test/monitoring/magic/{token}', function () {
|
||||||
|
$event = Event::createEvent();
|
||||||
|
$event->setRequest(['url' => request()->fullUrl()]);
|
||||||
|
|
||||||
|
return ErrorReportScrubber::beforeSend($event)?->getRequest()['url'];
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->get('/_test/monitoring/magic/kurz123')
|
||||||
|
->assertSuccessful()
|
||||||
|
->assertSee('/_test/monitoring/magic/[Filtered]', false)
|
||||||
|
->assertDontSee('kurz123');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('messages and exception values lose e-mail addresses', function () {
|
||||||
|
$event = Event::createEvent();
|
||||||
|
$event->setMessage('Bounce von info@firma.de', [], 'Bounce von info@firma.de');
|
||||||
|
$event->setExceptions([new ExceptionDataBag(new RuntimeException('Duplicate entry \'a.b@c.org\''))]);
|
||||||
|
|
||||||
|
$scrubbed = ErrorReportScrubber::beforeSend($event);
|
||||||
|
|
||||||
|
expect($scrubbed?->getMessage())->toBe('Bounce von [email]')
|
||||||
|
->and($scrubbed?->getMessageFormatted())->toBe('Bounce von [email]')
|
||||||
|
->and($scrubbed?->getExceptions()[0]->getValue())->toBe('Duplicate entry \'[email]\'')
|
||||||
|
->and($scrubbed?->getUser())->toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the test suite never reports to the real glitchtip or ntfy', function () {
|
||||||
|
// Ohne diese Absicherung landeten absichtlich provozierte Fehler
|
||||||
|
// („Queue weg“, „Datenbank nicht erreichbar“) in GlitchTip bzw. als Push.
|
||||||
|
expect(config('sentry.dsn'))->toBeEmpty()
|
||||||
|
->and(config('monitoring.browser.dsn'))->toBeEmpty()
|
||||||
|
->and(config('monitoring.alerts.ntfy.url'))->toBeEmpty()
|
||||||
|
->and(SentrySdk::getCurrentHub()->getClient()?->getOptions()->getDsn())->toBeNull();
|
||||||
|
});
|
||||||
73
tests/Feature/Monitoring/ErrorSignalsTest.php
Normal file
73
tests/Feature/Monitoring/ErrorSignalsTest.php
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Support\Monitoring\ErrorSignal;
|
||||||
|
use App\User;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
use Tests\Support\FakeGlitchTip;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
uses(TestCase::class, RefreshDatabase::class);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fehler ohne Exception: tote interne Links, 403/429 für Angemeldete und
|
||||||
|
* 419 werden als GlitchTip-Warnung gemeldet. Testrouten mit vier Segmenten,
|
||||||
|
* sonst greift die Catch-all-Route der Haupt-Domain.
|
||||||
|
*/
|
||||||
|
beforeEach(function () {
|
||||||
|
config()->set('cache.default', 'array');
|
||||||
|
$this->withoutDefer();
|
||||||
|
$this->glitchTip = FakeGlitchTip::install();
|
||||||
|
|
||||||
|
Route::middleware('web')->get('/_test/monitoring/signal/forbidden', fn () => abort(403))->name('test.forbidden');
|
||||||
|
Route::middleware('web')->get('/_test/monitoring/signal/expired', fn () => abort(419))->name('test.expired');
|
||||||
|
Route::middleware('web')->get('/_test/monitoring/signal/throttled', fn () => abort(429))->name('test.throttled');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a 404 reached from one of our own pages is reported as a dead internal link', function () {
|
||||||
|
$this->get('/gibt/es/nicht/mehr', ['referer' => url('/produkte/aloe')])->assertNotFound();
|
||||||
|
|
||||||
|
expect($this->glitchTip->messages())->toBe(['Toter interner Link: /gibt/es/nicht/mehr']);
|
||||||
|
|
||||||
|
$event = $this->glitchTip->events[0];
|
||||||
|
expect($event->getTags())->toMatchArray(['signal' => ErrorSignal::DeadInternalLink])
|
||||||
|
->and($event->getFingerprint())->toBe(['error-signal', ErrorSignal::DeadInternalLink, '/gibt/es/nicht/mehr'])
|
||||||
|
->and($event->getContexts()['signal']['referer'])->toBe('/produkte/aloe');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a 404 without referer or from another site is not reported', function () {
|
||||||
|
$this->get('/gibt/es/nicht/mehr')->assertNotFound();
|
||||||
|
$this->get('/gibt/es/nicht/mehr', ['referer' => 'https://google.com/search'])->assertNotFound();
|
||||||
|
|
||||||
|
expect($this->glitchTip->events)->toBe([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('403 and 429 are reported only for signed-in users', function () {
|
||||||
|
$this->get('/_test/monitoring/signal/forbidden')->assertForbidden();
|
||||||
|
$this->get('/_test/monitoring/signal/throttled')->assertTooManyRequests();
|
||||||
|
|
||||||
|
expect($this->glitchTip->events)->toBe([]);
|
||||||
|
|
||||||
|
$this->actingAs(User::forceCreate(['email' => 'berater@example.com', 'password' => bcrypt('secret'), 'lang' => 'de']));
|
||||||
|
$this->get('/_test/monitoring/signal/forbidden')->assertForbidden();
|
||||||
|
$this->get('/_test/monitoring/signal/throttled')->assertTooManyRequests();
|
||||||
|
|
||||||
|
expect($this->glitchTip->messages())->toBe([
|
||||||
|
'Zugriff verweigert (angemeldet): test.forbidden',
|
||||||
|
'Drosselung trifft angemeldeten Nutzer: test.throttled',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('an expired page is always reported', function () {
|
||||||
|
$this->get('/_test/monitoring/signal/expired')->assertStatus(419);
|
||||||
|
|
||||||
|
expect($this->glitchTip->messages())->toBe(['Seite abgelaufen (419): test.expired']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('each signal group is capped at five reports per window', function () {
|
||||||
|
foreach (range(1, 7) as $attempt) {
|
||||||
|
$this->get('/_test/monitoring/signal/expired');
|
||||||
|
}
|
||||||
|
|
||||||
|
expect($this->glitchTip->events)->toHaveCount(5);
|
||||||
|
});
|
||||||
|
|
@ -57,6 +57,7 @@ function statusPageUser(int $admin): User
|
||||||
|
|
||||||
test('system admins see the checks, the version and the menu entry', function () {
|
test('system admins see the checks, the version and the menu entry', function () {
|
||||||
config()->set('app.version', 'v1.0.2');
|
config()->set('app.version', 'v1.0.2');
|
||||||
|
config()->set('monitoring.health.token', null);
|
||||||
app(HealthProbe::class)->recordHeartbeat();
|
app(HealthProbe::class)->recordHeartbeat();
|
||||||
|
|
||||||
$this->actingAs(statusPageUser(4))
|
$this->actingAs(statusPageUser(4))
|
||||||
|
|
|
||||||
63
tests/Support/FakeGlitchTip.php
Normal file
63
tests/Support/FakeGlitchTip.php
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Support;
|
||||||
|
|
||||||
|
use Sentry\ClientBuilder;
|
||||||
|
use Sentry\Event;
|
||||||
|
use Sentry\SentrySdk;
|
||||||
|
use Sentry\Transport\Result;
|
||||||
|
use Sentry\Transport\ResultStatus;
|
||||||
|
use Sentry\Transport\TransportInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fängt Sentry-Events ab, statt sie an GlitchTip zu senden. Der Client wird
|
||||||
|
* mit den Datenschutz-Optionen aus config/sentry.php gebaut, damit Tests den
|
||||||
|
* echten `before_send`-Weg sehen.
|
||||||
|
*
|
||||||
|
* Nutzung: $glitchTip = FakeGlitchTip::install(); … $glitchTip->events
|
||||||
|
*/
|
||||||
|
class FakeGlitchTip implements TransportInterface
|
||||||
|
{
|
||||||
|
/** @var list<Event> */
|
||||||
|
public array $events = [];
|
||||||
|
|
||||||
|
public static function install(): self
|
||||||
|
{
|
||||||
|
$transport = new self;
|
||||||
|
|
||||||
|
$client = ClientBuilder::create([
|
||||||
|
'dsn' => 'https://public@glitchtip.test/1',
|
||||||
|
'default_integrations' => false,
|
||||||
|
'send_default_pii' => config('sentry.send_default_pii'),
|
||||||
|
'max_request_body_size' => config('sentry.max_request_body_size'),
|
||||||
|
'before_send' => config('sentry.before_send'),
|
||||||
|
])->setTransport($transport)->getClient();
|
||||||
|
|
||||||
|
SentrySdk::getCurrentHub()->bindClient($client);
|
||||||
|
|
||||||
|
return $transport;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function send(Event $event): Result
|
||||||
|
{
|
||||||
|
$this->events[] = $event;
|
||||||
|
|
||||||
|
return new Result(ResultStatus::success(), $event);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function close(?int $timeout = null): Result
|
||||||
|
{
|
||||||
|
return new Result(ResultStatus::success());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
public function messages(): array
|
||||||
|
{
|
||||||
|
return array_values(array_map(
|
||||||
|
static fn (Event $event): string => (string) ($event->getMessage() ?? $event->getExceptions()[0]?->getValue()),
|
||||||
|
$this->events,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue