From f38c7ece972598540bb887eb0b3588d2ec53dbea Mon Sep 17 00:00:00 2001 From: "bert.hausmans" Date: Sat, 25 Apr 2026 04:03:31 +0200 Subject: [PATCH] chore: install laravel telescope as dev-only debugging dashboard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Installs laravel/telescope ^5.0 (v5.12.5) as a dev-dependency. Three-layer production safety adapted to Laravel 11 layout (no Kernel.php; routing/schedule in bootstrap/app.php + routes/console.php): 1. composer.json `extra.laravel.dont-discover` lists laravel/telescope. After editing, `php artisan package:discover` regenerates bootstrap/cache/packages.php — without this step the auto-discovery cache still registers the vendor provider. 2. AppServiceProvider::register() gates registration to local + testing environments. Registers BOTH the vendor Laravel\Telescope\TelescopeServiceProvider (routes, migrations, publishing) AND the project's App\Providers\TelescopeService Provider (gate + filter) — they're sibling classes that extend ServiceProvider independently, not parent/child, so both must register for the dashboard to work. bootstrap/providers.php deliberately does NOT list either Telescope provider. 3. .env TELESCOPE_ENABLED flag (false in .env.example). Runtime toggle that disables Telescope even when the providers are registered. Production safety verified via simulated APP_ENV=production check: confirms no Telescope-* providers are loaded. Authorization: viewTelescope gate restricts dashboard to users with the super_admin Spatie Permission role. Even in local environments, only super_admin can view. Default was an email allow-list stub — replaced with `$user->hasRole('super_admin')`. Pruning: Schedule::command('telescope:prune --hours=48') added in routes/console.php (Laravel 11's schedule location), environment- gated to local + testing only. Documentation: /dev-docs/TELESCOPE.md added; CLAUDE.md gets a Development-tooling section. The doc explicitly calls out the dual-provider registration (vendor + app) which differs from the single-provider pattern in older Laravel versions. Migrations applied: telescope_entries, telescope_entries_tags, telescope_monitoring tables. Route registration verified in local (42 telescope.* routes). Tests: 1208/1208 passing — Telescope loads in the testing environment as well, so the suite exercised it without issues. Deployment note (flag for separate docs): a production operator who runs `php artisan migrate` manually will still apply the Telescope migrations — but because the providers never register in production, the tables stay empty. Co-Authored-By: Claude Opus 4.7 (1M context) --- CLAUDE.md | 6 + api/.env.example | 7 + api/app/Providers/AppServiceProvider.php | 13 ++ .../Providers/TelescopeServiceProvider.php | 63 ++++++ api/bootstrap/providers.php | 3 + api/composer.json | 5 +- api/composer.lock | 127 ++++++++++- api/config/telescope.php | 212 ++++++++++++++++++ ..._015838_create_telescope_entries_table.php | 70 ++++++ api/routes/console.php | 7 + dev-docs/TELESCOPE.md | 101 +++++++++ 11 files changed, 612 insertions(+), 2 deletions(-) create mode 100644 api/app/Providers/TelescopeServiceProvider.php create mode 100644 api/config/telescope.php create mode 100644 api/database/migrations/2026_04_25_015838_create_telescope_entries_table.php create mode 100644 dev-docs/TELESCOPE.md diff --git a/CLAUDE.md b/CLAUDE.md index 15531863..a4d8b472 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -26,6 +26,12 @@ Design document: `/dev-docs/design-document.md` See `/dev-docs/FRONTEND-TOOLING.md`. New TypeScript code adheres to ts-reset's stricter types automatically. +## Development tooling + +- Laravel Telescope at `/telescope` — queries, jobs, mails, redis, + events. Local + testing only, never production. super_admin role + required to view. See `/dev-docs/TELESCOPE.md`. + ## Repository layout - `api/` — Laravel backend diff --git a/api/.env.example b/api/.env.example index 03222877..c578a3ff 100644 --- a/api/.env.example +++ b/api/.env.example @@ -71,3 +71,10 @@ SANCTUM_STATEFUL_DOMAINS=localhost:5174,localhost:5175 # FRONTEND_APP_URL=https://crewli.app # FRONTEND_PORTAL_URL=https://portal.crewli.app # SANCTUM_STATEFUL_DOMAINS=crewli.app,portal.crewli.app + +# Laravel Telescope — dev-only debugging dashboard at /telescope. +# Flip to true in your local .env. Production MUST keep this false; +# the three-layer safety (composer dont-discover + AppServiceProvider +# env-gate + this flag) keeps Telescope out even if one layer is +# breached. See /dev-docs/TELESCOPE.md. +TELESCOPE_ENABLED=false diff --git a/api/app/Providers/AppServiceProvider.php b/api/app/Providers/AppServiceProvider.php index 43f219a0..d66eccac 100644 --- a/api/app/Providers/AppServiceProvider.php +++ b/api/app/Providers/AppServiceProvider.php @@ -84,6 +84,19 @@ class AppServiceProvider extends ServiceProvider ); $this->app->singleton(PurposeRegistry::class); + + // Telescope is a dev-only debugging dashboard. Three-layer + // defense keeps it out of production: composer `dont-discover` + // suppresses auto-registration, this block gates manual + // registration to local/testing, and TELESCOPE_ENABLED in .env + // is the runtime toggle (see /dev-docs/TELESCOPE.md). The vendor + // service provider registers routes/migrations/publishing; + // App\Providers\TelescopeServiceProvider attaches the gate + + // filter. Both must register for the dashboard to work. + if ($this->app->environment('local', 'testing')) { + $this->app->register(\Laravel\Telescope\TelescopeServiceProvider::class); + $this->app->register(\App\Providers\TelescopeServiceProvider::class); + } } public function boot(): void diff --git a/api/app/Providers/TelescopeServiceProvider.php b/api/app/Providers/TelescopeServiceProvider.php new file mode 100644 index 00000000..a53701ea --- /dev/null +++ b/api/app/Providers/TelescopeServiceProvider.php @@ -0,0 +1,63 @@ +hideSensitiveRequestDetails(); + + $isLocal = $this->app->environment('local'); + + Telescope::filter(function (IncomingEntry $entry) use ($isLocal) { + return $isLocal || + $entry->isReportableException() || + $entry->isFailedRequest() || + $entry->isFailedJob() || + $entry->isScheduledTask() || + $entry->hasMonitoredTag(); + }); + } + + /** + * Prevent sensitive request details from being logged by Telescope. + */ + protected function hideSensitiveRequestDetails(): void + { + if ($this->app->environment('local')) { + return; + } + + Telescope::hideRequestParameters(['_token']); + + Telescope::hideRequestHeaders([ + 'cookie', + 'x-csrf-token', + 'x-xsrf-token', + ]); + } + + /** + * Register the Telescope gate. + * + * This gate determines who can access Telescope in non-local environments. + */ + protected function gate(): void + { + Gate::define('viewTelescope', function (?User $user) { + return $user !== null && $user->hasRole('super_admin'); + }); + } +} diff --git a/api/bootstrap/providers.php b/api/bootstrap/providers.php index 38b258d1..23ac8540 100644 --- a/api/bootstrap/providers.php +++ b/api/bootstrap/providers.php @@ -1,5 +1,8 @@ env('TELESCOPE_ENABLED', true), + + /* + |-------------------------------------------------------------------------- + | Telescope Domain + |-------------------------------------------------------------------------- + | + | This is the subdomain where Telescope will be accessible from. If the + | setting is null, Telescope will reside under the same domain as the + | application. Otherwise, this value will be used as the subdomain. + | + */ + + 'domain' => env('TELESCOPE_DOMAIN'), + + /* + |-------------------------------------------------------------------------- + | Telescope Path + |-------------------------------------------------------------------------- + | + | This is the URI path where Telescope will be accessible from. Feel free + | to change this path to anything you like. Note that the URI will not + | affect the paths of its internal API that aren't exposed to users. + | + */ + + 'path' => env('TELESCOPE_PATH', 'telescope'), + + /* + |-------------------------------------------------------------------------- + | Telescope Storage Driver + |-------------------------------------------------------------------------- + | + | This configuration options determines the storage driver that will + | be used to store Telescope's data. In addition, you may set any + | custom options as needed by the particular driver you choose. + | + */ + + 'driver' => env('TELESCOPE_DRIVER', 'database'), + + 'storage' => [ + 'database' => [ + 'connection' => env('DB_CONNECTION', 'mysql'), + 'chunk' => 1000, + ], + ], + + /* + |-------------------------------------------------------------------------- + | Telescope Queue + |-------------------------------------------------------------------------- + | + | This configuration options determines the queue connection and queue + | which will be used to process ProcessPendingUpdate jobs. This can + | be changed if you would prefer to use a non-default connection. + | + */ + + 'queue' => [ + 'connection' => env('TELESCOPE_QUEUE_CONNECTION'), + 'queue' => env('TELESCOPE_QUEUE'), + 'delay' => env('TELESCOPE_QUEUE_DELAY', 10), + ], + + /* + |-------------------------------------------------------------------------- + | Telescope Route Middleware + |-------------------------------------------------------------------------- + | + | These middleware will be assigned to every Telescope route, giving you + | the chance to add your own middleware to this list or change any of + | the existing middleware. Or, you can simply stick with this list. + | + */ + + 'middleware' => [ + 'web', + Authorize::class, + ], + + /* + |-------------------------------------------------------------------------- + | Allowed / Ignored Paths & Commands + |-------------------------------------------------------------------------- + | + | The following array lists the URI paths and Artisan commands that will + | not be watched by Telescope. In addition to this list, some Laravel + | commands, like migrations and queue commands, are always ignored. + | + */ + + 'only_paths' => [ + // 'api/*' + ], + + 'ignore_paths' => [ + 'livewire*', + 'nova-api*', + 'pulse*', + '_boost*', + '.well-known*', + ], + + 'ignore_commands' => [ + // + ], + + /* + |-------------------------------------------------------------------------- + | Telescope Watchers + |-------------------------------------------------------------------------- + | + | The following array lists the "watchers" that will be registered with + | Telescope. The watchers gather the application's profile data when + | a request or task is executed. Feel free to customize this list. + | + */ + + 'watchers' => [ + Watchers\BatchWatcher::class => env('TELESCOPE_BATCH_WATCHER', true), + + Watchers\CacheWatcher::class => [ + 'enabled' => env('TELESCOPE_CACHE_WATCHER', true), + 'hidden' => [], + 'ignore' => [], + ], + + Watchers\ClientRequestWatcher::class => [ + 'enabled' => env('TELESCOPE_CLIENT_REQUEST_WATCHER', true), + 'ignore_hosts' => [], + ], + + Watchers\CommandWatcher::class => [ + 'enabled' => env('TELESCOPE_COMMAND_WATCHER', true), + 'ignore' => [], + ], + + Watchers\DumpWatcher::class => [ + 'enabled' => env('TELESCOPE_DUMP_WATCHER', true), + 'always' => env('TELESCOPE_DUMP_WATCHER_ALWAYS', false), + ], + + Watchers\EventWatcher::class => [ + 'enabled' => env('TELESCOPE_EVENT_WATCHER', true), + 'ignore' => [], + ], + + Watchers\ExceptionWatcher::class => env('TELESCOPE_EXCEPTION_WATCHER', true), + + Watchers\GateWatcher::class => [ + 'enabled' => env('TELESCOPE_GATE_WATCHER', true), + 'ignore_abilities' => [], + 'ignore_packages' => true, + 'ignore_paths' => [], + ], + + Watchers\JobWatcher::class => env('TELESCOPE_JOB_WATCHER', true), + + Watchers\LogWatcher::class => [ + 'enabled' => env('TELESCOPE_LOG_WATCHER', true), + 'level' => 'error', + ], + + Watchers\MailWatcher::class => env('TELESCOPE_MAIL_WATCHER', true), + + Watchers\ModelWatcher::class => [ + 'enabled' => env('TELESCOPE_MODEL_WATCHER', true), + 'events' => ['eloquent.*'], + 'hydrations' => true, + ], + + Watchers\NotificationWatcher::class => env('TELESCOPE_NOTIFICATION_WATCHER', true), + + Watchers\QueryWatcher::class => [ + 'enabled' => env('TELESCOPE_QUERY_WATCHER', true), + 'ignore_packages' => true, + 'ignore_paths' => [], + 'slow' => 100, + ], + + Watchers\RedisWatcher::class => env('TELESCOPE_REDIS_WATCHER', true), + + Watchers\RequestWatcher::class => [ + 'enabled' => env('TELESCOPE_REQUEST_WATCHER', true), + 'size_limit' => env('TELESCOPE_RESPONSE_SIZE_LIMIT', 64), + 'ignore_http_methods' => [], + 'ignore_status_codes' => [], + ], + + Watchers\ScheduleWatcher::class => env('TELESCOPE_SCHEDULE_WATCHER', true), + Watchers\ViewWatcher::class => env('TELESCOPE_VIEW_WATCHER', true), + ], +]; diff --git a/api/database/migrations/2026_04_25_015838_create_telescope_entries_table.php b/api/database/migrations/2026_04_25_015838_create_telescope_entries_table.php new file mode 100644 index 00000000..031b6f47 --- /dev/null +++ b/api/database/migrations/2026_04_25_015838_create_telescope_entries_table.php @@ -0,0 +1,70 @@ +getConnection()); + + $schema->create('telescope_entries', function (Blueprint $table) { + $table->bigIncrements('sequence'); + $table->uuid('uuid'); + $table->uuid('batch_id'); + $table->string('family_hash')->nullable(); + $table->boolean('should_display_on_index')->default(true); + $table->string('type', 20); + $table->longText('content'); + $table->dateTime('created_at')->nullable(); + + $table->unique('uuid'); + $table->index('batch_id'); + $table->index('family_hash'); + $table->index('created_at'); + $table->index(['type', 'should_display_on_index']); + }); + + $schema->create('telescope_entries_tags', function (Blueprint $table) { + $table->uuid('entry_uuid'); + $table->string('tag'); + + $table->primary(['entry_uuid', 'tag']); + $table->index('tag'); + + $table->foreign('entry_uuid') + ->references('uuid') + ->on('telescope_entries') + ->cascadeOnDelete(); + }); + + $schema->create('telescope_monitoring', function (Blueprint $table) { + $table->string('tag')->primary(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + $schema = Schema::connection($this->getConnection()); + + $schema->dropIfExists('telescope_entries_tags'); + $schema->dropIfExists('telescope_entries'); + $schema->dropIfExists('telescope_monitoring'); + } +}; diff --git a/api/routes/console.php b/api/routes/console.php index 1eee6eb8..2713d22a 100644 --- a/api/routes/console.php +++ b/api/routes/console.php @@ -9,3 +9,10 @@ Artisan::command('inspire', function () { })->purpose('Display an inspiring quote'); Schedule::command('invitations:expire')->daily(); + +// Telescope retention — dev-only (mirrors AppServiceProvider's +// environment gate). 48h is enough for debugging without filling the +// dev database. +Schedule::command('telescope:prune --hours=48') + ->daily() + ->environments(['local', 'testing']); diff --git a/dev-docs/TELESCOPE.md b/dev-docs/TELESCOPE.md new file mode 100644 index 00000000..e1fc18e1 --- /dev/null +++ b/dev-docs/TELESCOPE.md @@ -0,0 +1,101 @@ +# Laravel Telescope (dev-only) + +Local debugging dashboard for queries, jobs, mails, redis, events, +exceptions. **Never registered in production.** + +## Access + +- Local URL: +- Auth requirement: authenticated user with `super_admin` role + (Spatie Permission). The `viewTelescope` gate is the access + control; even in `local`, only super_admin can view. + +## Production safety — three-layer defense + +Crewli runs Laravel 11 (no `Kernel.php`; routing/schedule live in +`bootstrap/app.php` + `routes/console.php`). The defense layers +adapted to that layout are: + +1. **`composer.json` `extra.laravel.dont-discover`** lists + `laravel/telescope`, so Laravel's auto-discovery never registers + the vendor `Laravel\Telescope\TelescopeServiceProvider`. After + editing this list, run `php artisan package:discover` once to + refresh `bootstrap/cache/packages.php`. +2. **`AppServiceProvider::register()`** gates manual registration + to `local` + `testing` only, and registers BOTH the vendor + provider (routes/migrations/publishing) and the project's + `App\Providers\TelescopeServiceProvider` (gate + filter): + ```php + if ($this->app->environment('local', 'testing')) { + $this->app->register(\Laravel\Telescope\TelescopeServiceProvider::class); + $this->app->register(\App\Providers\TelescopeServiceProvider::class); + } + ``` + `bootstrap/providers.php` deliberately does NOT list either + Telescope provider — both registrations live behind this + environment gate. +3. **`.env` `TELESCOPE_ENABLED` flag.** `false` in `.env.example`. + Runtime toggle that disables Telescope even when the providers + are registered (e.g. to silence Telescope locally during a + profiling session). + +If you ever see Telescope on production: revert immediately, +audit the three layers above, and treat as a security incident. +The dashboard exposes every query, payload, and job — including +secrets in payloads. + +### Verifying production safety + +```bash +APP_ENV=production php -r " +require 'vendor/autoload.php'; +\$app = require 'bootstrap/app.php'; +\$app->make('Illuminate\Contracts\Console\Kernel')->bootstrap(); +foreach (array_keys(\$app->getLoadedProviders()) as \$p) { + if (stripos(\$p, 'telescope') !== false) echo 'LOADED: ' . \$p . PHP_EOL; +} +" +``` + +Expected output: nothing. Any line means a layer is breached. + +## Pruning + +Scheduled in `routes/console.php` (Laravel 11 layout): + +```php +Schedule::command('telescope:prune --hours=48') + ->daily() + ->environments(['local', 'testing']); +``` + +48-hour retention. Adjust if dev DB is filling up. + +## What Telescope captures + +- All Eloquent queries with bindings + execution time (N+1 detection) +- Background jobs with payload + status + retry history +- Sent mails with rendered HTML + plaintext + recipient +- Redis commands +- Cache reads/writes +- Events fired with listeners +- Exceptions with stack traces +- HTTP requests with headers + payload + +## What NOT to use Telescope for + +- Production debugging — never enabled there +- Long-term audit trails — Spatie ActivityLog handles that + (see ARCH-FORM-BUILDER §17.1) +- Performance benchmarking — use APM tools designed for it + +## Disabling temporarily + +If Telescope is slowing down a specific dev workflow: + +```bash +TELESCOPE_ENABLED=false php artisan serve +``` + +Or set globally in `.env` and restart your server. Re-enable when +done.