# Sentry BE wire-up — 2026-05-11

Task 2.5 BE — autonomous single-commit run on `master` (local only).

## What landed

| Change | File |
| --- | --- |
| Composer dep `sentry/sentry-laravel ^4.25` | `composer.json`, `composer.lock` |
| Published config | `config/sentry.php` |
| Temporary smoke route | `routes/api.php` — `GET /api/_sentry-test` |

The package auto-registers via Laravel's package discovery (no
`bootstrap/providers.php` edit needed).

## Test route

```
GET https://mathapi.allgifted.com/api/_sentry-test
```

(Local equivalent: `GET http://localhost:8000/api/_sentry-test`.)

The route throws `new \Exception('Sentry test')`. Once `SENTRY_LARAVEL_DSN`
is set in production `.env`, hitting this URL produces a captured event
in the Sentry dashboard, proving the wire-up. **Remove the route after
the first event lands** — comment in `routes/api.php` already says so.

## DSN status

`SENTRY_LARAVEL_DSN` is **not** set in `.env` yet. `config/sentry.php`
reads it via `env('SENTRY_LARAVEL_DSN', env('SENTRY_DSN'))`, so the
package boots with a null DSN and silently no-ops — no errors, no
captures. Pam will set the real DSN during deploy:

```bash
# On the production droplet, after git pull:
echo "SENTRY_LARAVEL_DSN=https://<key>@oXXXX.ingest.sentry.io/<project>" >> .env
php artisan config:clear
php artisan config:cache
systemctl restart apache2
# Then: curl https://mathapi.allgifted.com/api/_sentry-test
```

## Test-suite state

`php artisan test` was run twice — the first time picked up a stale
config cache that `composer require`'s post-install `php artisan
optimize` had written (32 failed). After `php artisan config:clear`
the second run settled on the **pre-existing baseline** for this
Windows dev box:

| Test class                          | Status | Notes |
| ----------------------------------- | ------ | ----- |
| `Tests\Unit\ExampleTest`            | PASS   | |
| `Tests\Unit\MaxileServiceTest`      | FAIL   | pre-existing — pdo_sqlite not loaded |
| `Tests\Feature\AnswerEndpointAuthValidationTest` | PASS | |
| `Tests\Feature\AnswerGradingTest`   | FAIL   | pre-existing — pdo_sqlite not loaded |
| `Tests\Feature\DiagnosticSubmitEndpointTest` | PASS | |
| `Tests\Feature\ExampleTest`         | FAIL   | pre-existing — skeleton test hits `/`, no route defined |
| `Tests\Feature\HealthEndpointTest`  | PASS   | |
| `Tests\Feature\IdempotencyMiddlewareTest` | PASS | |
| `Tests\Feature\LivesPurchaseEndpointTest` | PASS | |
| `Tests\Feature\LivesRegenerationTest` | FAIL | pre-existing — pdo_sqlite not loaded |
| `Tests\Feature\OtpSignupRoleAssignmentTest` | FAIL | pre-existing — pdo_sqlite not loaded |
| `Tests\Feature\RateLimitTest`       | PASS   | |

**No regressions.** Every class that passed before Sentry still passes.
Every class that fails fails for one of two pre-existing reasons:
`pdo_sqlite` is not loaded on this PHP install (errors with "could not
find driver"), or the default Laravel skeleton `Feature\ExampleTest`
asserts 200 on `/` which has no route. Both predate Task 2.5.

Verified passing subset (no-DB classes only):

```
Tests:    23 passed, 1 failed (87 assertions)
```

— the single failure is `Feature\ExampleTest` (the `/` skeleton),
which Sentry cannot influence.

On CI / the production server (where `pdo_sqlite` is installed) the
full suite is expected to pass apart from the same `Feature\ExampleTest`
skeleton — which is a separate cleanup, not a launch blocker.

## Decisions defaulted on

1. **`php artisan sentry:publish` was abandoned in favour of
   `vendor:publish --provider="Sentry\Laravel\ServiceProvider"`.**
   `sentry:publish` insists on sending a test event during config
   publish; without a real DSN it errors before writing
   `config/sentry.php`. `vendor:publish` is the same operation minus
   the test event, which is what we want when the DSN lands later.

2. **DSN intentionally left unset in `.env`.** The brief said
   "it picks up .env if DSN set" — no DSN was set in `$SENTRY_LARAVEL_DSN`
   on this machine, and `CLAUDE.md` forbids `.env` edits from this
   working directory. Pam adds the DSN during deploy (template above).

3. **`config:clear` + `cache:clear` were run after `composer require`.**
   The composer post-install script triggered `php artisan optimize`,
   which cached the config in MySQL mode and bypassed `phpunit.xml`'s
   `DB_CONNECTION=sqlite` force flag, causing the first `php artisan test`
   run to report a flood of MySQL connection errors. Clearing the cache
   restored phpunit's env overrides.

4. **Test route is unauthenticated and unthrottled** — it's a smoke
   probe meant to be hit once with `curl`, then deleted. Auth would
   only obscure whether Sentry caught the throw.

## Commit

`feat(observability): wire Sentry for Laravel BE (Task 2.5 BE)`

(SHA backfilled after commit lands.)
