diff --git a/bootstrap/app.php b/bootstrap/app.php index b229b5be..f175f80d 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -20,6 +20,7 @@ use Illuminate\Support\Facades\Route; use Laravel\Sanctum\Http\Middleware\CheckAbilities; use Laravel\Sanctum\Http\Middleware\CheckForAnyAbility; use Sentry\Laravel\Integration; +use Symfony\Component\Console\Exception\CommandNotFoundException; return Application::configure(basePath: dirname(__DIR__)) ->withRouting( @@ -162,4 +163,8 @@ return Application::configure(basePath: dirname(__DIR__)) // Validierung, Auth) gilt weiter — diese Fälle meldet // ReportErrorSignals gezielt als Warnung. Integration::handles($exceptions); + + // Vertippte artisan-Befehle sind kein Anwendungsfehler (GlitchTip + // server-pressekonto, Issue 9: „config:cachels“ auf Live). + $exceptions->dontReport(CommandNotFoundException::class); })->create(); diff --git a/resources/js/error-reporting.js b/resources/js/error-reporting.js index 644ecab1..d2b474e6 100644 --- a/resources/js/error-reporting.js +++ b/resources/js/error-reporting.js @@ -87,6 +87,27 @@ function loadSdk() { return sdk; } +/** + * Promise-Ablehnungen mit Objekt statt Error kamen als „Error: [object Object]“ + * an (GlitchTip browser-pressekonto, Issue 10) — Typ und Inhalt mitschicken, + * damit die Quelle erkennbar ist. + */ +function rejectionToError(reason) { + if (reason instanceof Error) { + return reason; + } + + const kind = reason?.constructor?.name ?? typeof reason; + let text; + try { + text = typeof reason === 'string' ? reason : JSON.stringify(reason); + } catch { + text = String(reason); + } + + return new Error(`Unbehandelte Ablehnung (${kind}): ${String(text ?? '').slice(0, 300)}`); +} + function capture(error) { loadSdk() .then((Sentry) => Sentry.captureException(error)) @@ -102,6 +123,6 @@ if (config?.dsn) { }); window.addEventListener('unhandledrejection', (event) => { - capture(event.reason instanceof Error ? event.reason : new Error(String(event.reason))); + capture(rejectionToError(event.reason)); }); } diff --git a/resources/views/livewire/auth/register.blade.php b/resources/views/livewire/auth/register.blade.php index 341d591e..50f62d22 100644 --- a/resources/views/livewire/auth/register.blade.php +++ b/resources/views/livewire/auth/register.blade.php @@ -88,7 +88,7 @@ new #[Layout('components.layouts.auth.pressekonto', ['heading' => 'Konto erstell autofocus autocomplete="name" class="field-input" - :placeholder="__('Vor- und Nachname')" + placeholder="{{ __('Vor- und Nachname') }}" @error('name') aria-invalid="true" @enderror /> @error('name') diff --git a/tests/Feature/Auth/RegistrationTest.php b/tests/Feature/Auth/RegistrationTest.php index 87c1db8f..040264f3 100644 --- a/tests/Feature/Auth/RegistrationTest.php +++ b/tests/Feature/Auth/RegistrationTest.php @@ -11,7 +11,11 @@ test('registration screen can be rendered', function () { /** @var TestCase $this */ $response = $this->get('/register'); - $response->assertStatus(200); + // Platzhalter serverseitig übersetzt, nicht als Alpine-Ausdruck + // (ReferenceError „__ is not defined“, GlitchTip 06.10.2026). + $response->assertStatus(200) + ->assertSee('placeholder="Vor- und Nachname"', false) + ->assertDontSee(':placeholder="__(', false); }); test('new users register inactive and are sent to the verification notice', function () { diff --git a/tests/Feature/BladeAlpineTranslationGuardTest.php b/tests/Feature/BladeAlpineTranslationGuardTest.php new file mode 100644 index 00000000..4090e907 --- /dev/null +++ b/tests/Feature/BladeAlpineTranslationGuardTest.php @@ -0,0 +1,46 @@ +`, ``) + * ein PHP-Ausdruck. An einem normalen HTML-Element reicht Blade das + * Attribut unverändert an Alpine durch, und der Browser wirft + * „ReferenceError: __ is not defined“ — so geschehen auf /register + * (GlitchTip browser-pressekonto, Issue 15, 06.10.2026). Für HTML-Elemente + * gilt: `attr="{{ __('…') }}"`. + */ +function alpineTranslationBindingPattern(): string +{ + // HTML-Tag (keine Komponente), beliebige Attribute auch über mehrere + // Zeilen, dann ein Alpine-Binding, dessen Ausdruck mit __( beginnt. + return '/<(?!x-|flux:|livewire:|\/)([a-z][a-z0-9-]*)\b((?:[^>"]|"[^"]*")*?)\s(:[a-zA-Z.-]+|x-bind:[a-zA-Z.-]+)="\s*__\(/s'; +} + +test('no html element binds an alpine attribute to the php translation helper', function () { + $pattern = alpineTranslationBindingPattern(); + $offenders = []; + + foreach (File::allFiles(resource_path('views')) as $view) { + if (! str_ends_with($view->getFilename(), '.blade.php')) { + continue; + } + + $contents = File::get($view->getRealPath()); + + preg_match_all($pattern, $contents, $matches, PREG_OFFSET_CAPTURE); + + foreach ($matches[3] as [$attribute, $offset]) { + $offenders[] = sprintf('%s:%d %s', $view->getRelativePathname(), substr_count($contents, "\n", 0, $offset) + 1, $attribute); + } + } + + expect($offenders)->toBe([], "Alpine-Binding mit __() an einem HTML-Element — auf attr=\"{{ __('…') }}\" umstellen:\n".implode("\n", $offenders)); +}); + +test('the guard catches the pattern that broke the registration', function () { + $pattern = alpineTranslationBindingPattern(); + + expect(preg_match($pattern, ""))->toBe(1) + ->and(preg_match($pattern, ""))->toBe(0); +}); diff --git a/tests/Feature/Monitoring/ErrorReportingTest.php b/tests/Feature/Monitoring/ErrorReportingTest.php index 67f546c3..9b2b8c1a 100644 --- a/tests/Feature/Monitoring/ErrorReportingTest.php +++ b/tests/Feature/Monitoring/ErrorReportingTest.php @@ -6,6 +6,7 @@ use Illuminate\Support\Facades\Route; use Sentry\Event; use Sentry\ExceptionDataBag; use Sentry\SentrySdk; +use Symfony\Component\Console\Exception\CommandNotFoundException; use Tests\Support\FakeGlitchTip; /** @@ -35,6 +36,14 @@ test('exceptions laravel does not report stay out of glitchtip', function () { 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://pressekonto.test/newsroom/claim/5?signature=abc123&expires=1700000000&src=mail_publish',