diff --git a/CHANGELOG.md b/CHANGELOG.md index 544a75ad..809122ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,21 @@ Changelog ========= +## v2.25.1 (2023-01-17) + +### Enhancements + +* Ensure events are sent from queues when using Laravel Vapor + [#511](https://github.com/bugsnag/bugsnag-laravel/pull/511) + +### Bug fixes + +* Fix events from CLI commands always being handled when using the `NunoMaduro\Collision` package + [#503](https://github.com/bugsnag/bugsnag-laravel/pull/503) + +* Fix breadcrumbs leaking between queued jobs when using Laravel Vapor + [#511](https://github.com/bugsnag/bugsnag-laravel/pull/511) + ## 2.25.0 (2022-10-25) ### Enhancements diff --git a/Gemfile b/Gemfile index 23641092..23e2e699 100644 --- a/Gemfile +++ b/Gemfile @@ -1,3 +1,3 @@ source 'https://rubygems.org' -gem 'bugsnag-maze-runner', git: 'https://github.com/bugsnag/maze-runner', tag: 'v7.6.0' +gem 'bugsnag-maze-runner', git: 'https://github.com/bugsnag/maze-runner', tag: 'v7.9.0' diff --git a/features/fixtures/docker-compose.yml b/features/fixtures/docker-compose.yml index bfe17826..03a4acf9 100644 --- a/features/fixtures/docker-compose.yml +++ b/features/fixtures/docker-compose.yml @@ -15,6 +15,7 @@ services: - BUGSNAG_REGISTER_OOM_BOOTSTRAPPER - BUGSNAG_DISCARD_CLASSES - BUGSNAG_REDACTED_KEYS + - BUGSNAG_QUERY restart: "no" ports: - target: 8000 @@ -34,6 +35,7 @@ services: - BUGSNAG_REGISTER_OOM_BOOTSTRAPPER - BUGSNAG_DISCARD_CLASSES - BUGSNAG_REDACTED_KEYS + - BUGSNAG_QUERY restart: "no" ports: - target: 8000 @@ -53,6 +55,7 @@ services: - BUGSNAG_REGISTER_OOM_BOOTSTRAPPER - BUGSNAG_DISCARD_CLASSES - BUGSNAG_REDACTED_KEYS + - BUGSNAG_QUERY restart: "no" ports: - target: 8000 @@ -72,13 +75,13 @@ services: - BUGSNAG_REGISTER_OOM_BOOTSTRAPPER - BUGSNAG_DISCARD_CLASSES - BUGSNAG_REDACTED_KEYS + - BUGSNAG_QUERY restart: "no" ports: - target: 8000 published: 61266 laravel8: - init: true build: context: laravel8 args: @@ -92,13 +95,13 @@ services: - BUGSNAG_REGISTER_OOM_BOOTSTRAPPER - BUGSNAG_DISCARD_CLASSES - BUGSNAG_REDACTED_KEYS + - BUGSNAG_QUERY restart: "no" ports: - target: 8000 published: 61280 laravel9: - init: true build: context: laravel9 args: @@ -112,6 +115,7 @@ services: - BUGSNAG_REGISTER_OOM_BOOTSTRAPPER - BUGSNAG_DISCARD_CLASSES - BUGSNAG_REDACTED_KEYS + - BUGSNAG_QUERY restart: "no" ports: - target: 8000 diff --git a/features/fixtures/laravel51/.env b/features/fixtures/laravel51/.env index cebc8926..cf3794c3 100644 --- a/features/fixtures/laravel51/.env +++ b/features/fixtures/laravel51/.env @@ -2,15 +2,11 @@ APP_ENV=local APP_DEBUG=true APP_KEY=SomeRandomString -DB_CONNECTION=mysql -DB_HOST=127.0.0.1 -DB_DATABASE=homestead -DB_USERNAME=homestead -DB_PASSWORD=secret +DB_CONNECTION=sqlite CACHE_DRIVER=file SESSION_DRIVER=file -QUEUE_DRIVER=sync +QUEUE_DRIVER=database REDIS_HOST=127.0.0.1 REDIS_PASSWORD=null diff --git a/features/fixtures/laravel51/Dockerfile b/features/fixtures/laravel51/Dockerfile index c0bba8aa..b5551f8c 100644 --- a/features/fixtures/laravel51/Dockerfile +++ b/features/fixtures/laravel51/Dockerfile @@ -16,4 +16,7 @@ COPY --from=composer:2.2 /usr/bin/composer /usr/local/bin/composer RUN composer install --no-dev RUN php artisan key:generate +# create database & apply migrations +RUN touch database/database.sqlite && php artisan migrate --no-interaction + CMD php artisan serve --port=8000 --host=0.0.0.0 diff --git a/features/fixtures/laravel51/app/Http/routes.php b/features/fixtures/laravel51/app/Http/routes.php index dfc4b2d1..f1e8cbb5 100644 --- a/features/fixtures/laravel51/app/Http/routes.php +++ b/features/fixtures/laravel51/app/Http/routes.php @@ -61,6 +61,14 @@ return view('handlederror'); }); +Route::get('/queue/unhandled', function () { + Queue::push(new \App\Jobs\UnhandledJob()); +}); + +Route::get('/queue/handled', function () { + Queue::push(new \App\Jobs\HandledJob()); +}); + /** * Return some diagnostics if an OOM did not happen when it should have. * diff --git a/features/fixtures/laravel51/app/Jobs/HandledJob.php b/features/fixtures/laravel51/app/Jobs/HandledJob.php new file mode 100644 index 00000000..1e3c8ec4 --- /dev/null +++ b/features/fixtures/laravel51/app/Jobs/HandledJob.php @@ -0,0 +1,24 @@ +bigIncrements('id'); + $table->string('queue'); + $table->longText('payload'); + $table->tinyInteger('attempts')->unsigned(); + $table->tinyInteger('reserved')->unsigned(); + $table->unsignedInteger('reserved_at')->nullable(); + $table->unsignedInteger('available_at'); + $table->unsignedInteger('created_at'); + $table->index(['queue', 'reserved', 'reserved_at']); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::drop('jobs'); + } +} diff --git a/features/fixtures/laravel51/database/migrations/2022_12_06_094156_create_failed_jobs_table.php b/features/fixtures/laravel51/database/migrations/2022_12_06_094156_create_failed_jobs_table.php new file mode 100644 index 00000000..3d733b6b --- /dev/null +++ b/features/fixtures/laravel51/database/migrations/2022_12_06_094156_create_failed_jobs_table.php @@ -0,0 +1,33 @@ +increments('id'); + $table->text('connection'); + $table->text('queue'); + $table->longText('payload'); + $table->timestamp('failed_at')->useCurrent(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::drop('failed_jobs'); + } +} diff --git a/features/fixtures/laravel56/.env b/features/fixtures/laravel56/.env index ec44a125..7e2d4a2a 100644 --- a/features/fixtures/laravel56/.env +++ b/features/fixtures/laravel56/.env @@ -6,18 +6,13 @@ APP_URL=http://localhost LOG_CHANNEL=stack -DB_CONNECTION=mysql -DB_HOST=127.0.0.1 -DB_PORT=3306 -DB_DATABASE=homestead -DB_USERNAME=homestead -DB_PASSWORD=secret +DB_CONNECTION=sqlite BROADCAST_DRIVER=log CACHE_DRIVER=file SESSION_DRIVER=file SESSION_LIFETIME=120 -QUEUE_DRIVER=sync +QUEUE_DRIVER=database REDIS_HOST=127.0.0.1 REDIS_PASSWORD=null diff --git a/features/fixtures/laravel56/Dockerfile b/features/fixtures/laravel56/Dockerfile index 1f5418c7..614a57a4 100644 --- a/features/fixtures/laravel56/Dockerfile +++ b/features/fixtures/laravel56/Dockerfile @@ -16,4 +16,7 @@ COPY --from=composer:2.2 /usr/bin/composer /usr/local/bin/composer RUN composer install RUN php artisan key:generate +# create database & apply migrations +RUN touch database/database.sqlite && php artisan migrate --no-interaction + CMD php artisan serve --port=8000 --host=0.0.0.0 diff --git a/features/fixtures/laravel56/app/Jobs/HandledJob.php b/features/fixtures/laravel56/app/Jobs/HandledJob.php new file mode 100644 index 00000000..f29ab2f3 --- /dev/null +++ b/features/fixtures/laravel56/app/Jobs/HandledJob.php @@ -0,0 +1,28 @@ + [ 'driver' => 'sqlite', - 'database' => env('DB_DATABASE', database_path('database.sqlite')), + 'database' => database_path('database.sqlite'), 'prefix' => '', ], diff --git a/features/fixtures/laravel56/database/migrations/2022_12_05_165523_create_jobs_table.php b/features/fixtures/laravel56/database/migrations/2022_12_05_165523_create_jobs_table.php new file mode 100644 index 00000000..58d77154 --- /dev/null +++ b/features/fixtures/laravel56/database/migrations/2022_12_05_165523_create_jobs_table.php @@ -0,0 +1,36 @@ +bigIncrements('id'); + $table->string('queue')->index(); + $table->longText('payload'); + $table->unsignedTinyInteger('attempts'); + $table->unsignedInteger('reserved_at')->nullable(); + $table->unsignedInteger('available_at'); + $table->unsignedInteger('created_at'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('jobs'); + } +} diff --git a/features/fixtures/laravel56/database/migrations/2022_12_05_165527_create_failed_jobs_table.php b/features/fixtures/laravel56/database/migrations/2022_12_05_165527_create_failed_jobs_table.php new file mode 100644 index 00000000..d432dff0 --- /dev/null +++ b/features/fixtures/laravel56/database/migrations/2022_12_05_165527_create_failed_jobs_table.php @@ -0,0 +1,35 @@ +bigIncrements('id'); + $table->text('connection'); + $table->text('queue'); + $table->longText('payload'); + $table->longText('exception'); + $table->timestamp('failed_at')->useCurrent(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('failed_jobs'); + } +} diff --git a/features/fixtures/laravel56/routes/web.php b/features/fixtures/laravel56/routes/web.php index 805ced2f..baa615a1 100644 --- a/features/fixtures/laravel56/routes/web.php +++ b/features/fixtures/laravel56/routes/web.php @@ -52,6 +52,14 @@ Route::view('/handled_view_exception', 'handledexception'); Route::view('/handled_view_error', 'handlederror'); +Route::get('/queue/unhandled', function () { + \App\Jobs\UnhandledJob::dispatch(); +}); + +Route::get('/queue/handled', function () { + \App\Jobs\HandledJob::dispatch(); +}); + /** * Return some diagnostics if an OOM did not happen when it should have. * diff --git a/features/fixtures/laravel58/.env b/features/fixtures/laravel58/.env index ec44a125..63cdf4a9 100644 --- a/features/fixtures/laravel58/.env +++ b/features/fixtures/laravel58/.env @@ -6,18 +6,13 @@ APP_URL=http://localhost LOG_CHANNEL=stack -DB_CONNECTION=mysql -DB_HOST=127.0.0.1 -DB_PORT=3306 -DB_DATABASE=homestead -DB_USERNAME=homestead -DB_PASSWORD=secret +DB_CONNECTION=sqlite BROADCAST_DRIVER=log CACHE_DRIVER=file SESSION_DRIVER=file SESSION_LIFETIME=120 -QUEUE_DRIVER=sync +QUEUE_CONNECTION=database REDIS_HOST=127.0.0.1 REDIS_PASSWORD=null diff --git a/features/fixtures/laravel58/Dockerfile b/features/fixtures/laravel58/Dockerfile index 1f5418c7..614a57a4 100644 --- a/features/fixtures/laravel58/Dockerfile +++ b/features/fixtures/laravel58/Dockerfile @@ -16,4 +16,7 @@ COPY --from=composer:2.2 /usr/bin/composer /usr/local/bin/composer RUN composer install RUN php artisan key:generate +# create database & apply migrations +RUN touch database/database.sqlite && php artisan migrate --no-interaction + CMD php artisan serve --port=8000 --host=0.0.0.0 diff --git a/features/fixtures/laravel58/app/Jobs/HandledJob.php b/features/fixtures/laravel58/app/Jobs/HandledJob.php new file mode 100644 index 00000000..f29ab2f3 --- /dev/null +++ b/features/fixtures/laravel58/app/Jobs/HandledJob.php @@ -0,0 +1,28 @@ + [ 'driver' => 'sqlite', - 'database' => env('DB_DATABASE', database_path('database.sqlite')), + 'database' => database_path('database.sqlite'), 'prefix' => '', 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true), ], diff --git a/features/fixtures/laravel58/database/migrations/2022_12_05_160750_create_jobs_table.php b/features/fixtures/laravel58/database/migrations/2022_12_05_160750_create_jobs_table.php new file mode 100644 index 00000000..58d77154 --- /dev/null +++ b/features/fixtures/laravel58/database/migrations/2022_12_05_160750_create_jobs_table.php @@ -0,0 +1,36 @@ +bigIncrements('id'); + $table->string('queue')->index(); + $table->longText('payload'); + $table->unsignedTinyInteger('attempts'); + $table->unsignedInteger('reserved_at')->nullable(); + $table->unsignedInteger('available_at'); + $table->unsignedInteger('created_at'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('jobs'); + } +} diff --git a/features/fixtures/laravel58/database/migrations/2022_12_05_161039_create_failed_jobs_table.php b/features/fixtures/laravel58/database/migrations/2022_12_05_161039_create_failed_jobs_table.php new file mode 100644 index 00000000..d432dff0 --- /dev/null +++ b/features/fixtures/laravel58/database/migrations/2022_12_05_161039_create_failed_jobs_table.php @@ -0,0 +1,35 @@ +bigIncrements('id'); + $table->text('connection'); + $table->text('queue'); + $table->longText('payload'); + $table->longText('exception'); + $table->timestamp('failed_at')->useCurrent(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('failed_jobs'); + } +} diff --git a/features/fixtures/laravel58/routes/web.php b/features/fixtures/laravel58/routes/web.php index 805ced2f..baa615a1 100644 --- a/features/fixtures/laravel58/routes/web.php +++ b/features/fixtures/laravel58/routes/web.php @@ -52,6 +52,14 @@ Route::view('/handled_view_exception', 'handledexception'); Route::view('/handled_view_error', 'handlederror'); +Route::get('/queue/unhandled', function () { + \App\Jobs\UnhandledJob::dispatch(); +}); + +Route::get('/queue/handled', function () { + \App\Jobs\HandledJob::dispatch(); +}); + /** * Return some diagnostics if an OOM did not happen when it should have. * diff --git a/features/fixtures/laravel66/.env b/features/fixtures/laravel66/.env index ec44a125..63cdf4a9 100644 --- a/features/fixtures/laravel66/.env +++ b/features/fixtures/laravel66/.env @@ -6,18 +6,13 @@ APP_URL=http://localhost LOG_CHANNEL=stack -DB_CONNECTION=mysql -DB_HOST=127.0.0.1 -DB_PORT=3306 -DB_DATABASE=homestead -DB_USERNAME=homestead -DB_PASSWORD=secret +DB_CONNECTION=sqlite BROADCAST_DRIVER=log CACHE_DRIVER=file SESSION_DRIVER=file SESSION_LIFETIME=120 -QUEUE_DRIVER=sync +QUEUE_CONNECTION=database REDIS_HOST=127.0.0.1 REDIS_PASSWORD=null diff --git a/features/fixtures/laravel66/Dockerfile b/features/fixtures/laravel66/Dockerfile index 1f5418c7..614a57a4 100644 --- a/features/fixtures/laravel66/Dockerfile +++ b/features/fixtures/laravel66/Dockerfile @@ -16,4 +16,7 @@ COPY --from=composer:2.2 /usr/bin/composer /usr/local/bin/composer RUN composer install RUN php artisan key:generate +# create database & apply migrations +RUN touch database/database.sqlite && php artisan migrate --no-interaction + CMD php artisan serve --port=8000 --host=0.0.0.0 diff --git a/features/fixtures/laravel66/app/Jobs/HandledJob.php b/features/fixtures/laravel66/app/Jobs/HandledJob.php new file mode 100644 index 00000000..ec101a25 --- /dev/null +++ b/features/fixtures/laravel66/app/Jobs/HandledJob.php @@ -0,0 +1,28 @@ + [ 'driver' => 'sqlite', - 'url' => env('DATABASE_URL'), - 'database' => env('DB_DATABASE', database_path('database.sqlite')), + 'url' => null, + 'database' => database_path('database.sqlite'), 'prefix' => '', 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true), ], diff --git a/features/fixtures/laravel66/database/migrations/2022_12_05_110544_create_jobs_table.php b/features/fixtures/laravel66/database/migrations/2022_12_05_110544_create_jobs_table.php new file mode 100644 index 00000000..1be9e8a8 --- /dev/null +++ b/features/fixtures/laravel66/database/migrations/2022_12_05_110544_create_jobs_table.php @@ -0,0 +1,36 @@ +bigIncrements('id'); + $table->string('queue')->index(); + $table->longText('payload'); + $table->unsignedTinyInteger('attempts'); + $table->unsignedInteger('reserved_at')->nullable(); + $table->unsignedInteger('available_at'); + $table->unsignedInteger('created_at'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('jobs'); + } +} diff --git a/features/fixtures/laravel66/routes/web.php b/features/fixtures/laravel66/routes/web.php index 805ced2f..baa615a1 100644 --- a/features/fixtures/laravel66/routes/web.php +++ b/features/fixtures/laravel66/routes/web.php @@ -52,6 +52,14 @@ Route::view('/handled_view_exception', 'handledexception'); Route::view('/handled_view_error', 'handlederror'); +Route::get('/queue/unhandled', function () { + \App\Jobs\UnhandledJob::dispatch(); +}); + +Route::get('/queue/handled', function () { + \App\Jobs\HandledJob::dispatch(); +}); + /** * Return some diagnostics if an OOM did not happen when it should have. * diff --git a/features/fixtures/laravel8/app/Jobs/HandledJob.php b/features/fixtures/laravel8/app/Jobs/HandledJob.php index 587677d3..0ba9744f 100644 --- a/features/fixtures/laravel8/app/Jobs/HandledJob.php +++ b/features/fixtures/laravel8/app/Jobs/HandledJob.php @@ -22,6 +22,8 @@ class HandledJob implements ShouldQueue */ public function handle() { + Bugsnag::leaveBreadcrumb(__METHOD__); + Bugsnag::notifyException(new Exception('Handled :)')); } } diff --git a/features/fixtures/laravel8/app/Jobs/UnhandledJob.php b/features/fixtures/laravel8/app/Jobs/UnhandledJob.php index eb6c9b84..c443e886 100644 --- a/features/fixtures/laravel8/app/Jobs/UnhandledJob.php +++ b/features/fixtures/laravel8/app/Jobs/UnhandledJob.php @@ -2,6 +2,7 @@ namespace App\Jobs; +use Bugsnag\BugsnagLaravel\Facades\Bugsnag; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldBeUnique; use Illuminate\Contracts\Queue\ShouldQueue; @@ -14,13 +15,6 @@ class UnhandledJob implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - public int $tries; - - public function __construct(int $tries) - { - $this->tries = $tries; - } - /** * Execute the job. * @@ -28,6 +22,8 @@ public function __construct(int $tries) */ public function handle() { + Bugsnag::leaveBreadcrumb(__METHOD__); + throw new RuntimeException('uh oh :o'); } } diff --git a/features/fixtures/laravel8/app/Providers/AppServiceProvider.php b/features/fixtures/laravel8/app/Providers/AppServiceProvider.php index 49823ec1..8c5de890 100644 --- a/features/fixtures/laravel8/app/Providers/AppServiceProvider.php +++ b/features/fixtures/laravel8/app/Providers/AppServiceProvider.php @@ -3,6 +3,7 @@ namespace App\Providers; use Illuminate\Support\ServiceProvider; +use Bugsnag\BugsnagLaravel\Facades\Bugsnag; class AppServiceProvider extends ServiceProvider { @@ -34,6 +35,6 @@ public function register() */ public function boot() { - // + Bugsnag::leaveBreadcrumb(__METHOD__); } } diff --git a/features/fixtures/laravel8/app/Providers/EventServiceProvider.php b/features/fixtures/laravel8/app/Providers/EventServiceProvider.php index a9f10a63..a6a22d3d 100644 --- a/features/fixtures/laravel8/app/Providers/EventServiceProvider.php +++ b/features/fixtures/laravel8/app/Providers/EventServiceProvider.php @@ -6,6 +6,11 @@ use Illuminate\Auth\Listeners\SendEmailVerificationNotification; use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider; use Illuminate\Support\Facades\Event; +use Illuminate\Support\Facades\Queue; +use Bugsnag\BugsnagLaravel\Facades\Bugsnag; +use Illuminate\Queue\Events\JobProcessing; +use Illuminate\Queue\Events\JobProcessed; +use Illuminate\Queue\Events\JobExceptionOccurred; class EventServiceProvider extends ServiceProvider { @@ -27,6 +32,16 @@ class EventServiceProvider extends ServiceProvider */ public function boot() { - // + Queue::before(function (JobProcessing $event) { + Bugsnag::leaveBreadcrumb('before'); + }); + + Queue::after(function (JobProcessed $event) { + Bugsnag::leaveBreadcrumb('after'); + }); + + Queue::exceptionOccurred(function (JobExceptionOccurred $event) { + Bugsnag::leaveBreadcrumb('exceptionOccurred'); + }); } } diff --git a/features/fixtures/laravel8/config/database.php b/features/fixtures/laravel8/config/database.php index b42d9b30..adfd7351 100644 --- a/features/fixtures/laravel8/config/database.php +++ b/features/fixtures/laravel8/config/database.php @@ -37,8 +37,8 @@ 'sqlite' => [ 'driver' => 'sqlite', - 'url' => env('DATABASE_URL'), - 'database' => env('DB_DATABASE', database_path('database.sqlite')), + 'url' => null, + 'database' => database_path('database.sqlite'), 'prefix' => '', 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true), ], diff --git a/features/fixtures/laravel8/routes/web.php b/features/fixtures/laravel8/routes/web.php index b7204196..2ebe2765 100644 --- a/features/fixtures/laravel8/routes/web.php +++ b/features/fixtures/laravel8/routes/web.php @@ -1,6 +1,5 @@ query('tries', '1')); +Route::get('/queue/unhandled', function () { + \App\Jobs\UnhandledJob::dispatch(); }); -Route::get('/queue/handled', function (Request $request) { - \App\Jobs\HandledJob::dispatch((int) $request->query('tries', '1')); +Route::get('/queue/handled', function () { + \App\Jobs\HandledJob::dispatch(); }); /** diff --git a/features/fixtures/laravel9/Dockerfile b/features/fixtures/laravel9/Dockerfile index fcd9bf65..74e70b99 100644 --- a/features/fixtures/laravel9/Dockerfile +++ b/features/fixtures/laravel9/Dockerfile @@ -17,8 +17,7 @@ RUN cp .env.example .env RUN composer install RUN php artisan key:generate -# apply database migrations -# --force is required to create the database -RUN php artisan migrate --force --no-interaction +# create database & apply migrations +RUN touch database/database.sqlite && php artisan migrate --no-interaction -CMD ./run-fixture +CMD php -S 0.0.0.0:8000 -t public diff --git a/features/fixtures/laravel9/app/Jobs/HandledJob.php b/features/fixtures/laravel9/app/Jobs/HandledJob.php index 587677d3..0ba9744f 100644 --- a/features/fixtures/laravel9/app/Jobs/HandledJob.php +++ b/features/fixtures/laravel9/app/Jobs/HandledJob.php @@ -22,6 +22,8 @@ class HandledJob implements ShouldQueue */ public function handle() { + Bugsnag::leaveBreadcrumb(__METHOD__); + Bugsnag::notifyException(new Exception('Handled :)')); } } diff --git a/features/fixtures/laravel9/app/Jobs/UnhandledJob.php b/features/fixtures/laravel9/app/Jobs/UnhandledJob.php index eb6c9b84..c443e886 100644 --- a/features/fixtures/laravel9/app/Jobs/UnhandledJob.php +++ b/features/fixtures/laravel9/app/Jobs/UnhandledJob.php @@ -2,6 +2,7 @@ namespace App\Jobs; +use Bugsnag\BugsnagLaravel\Facades\Bugsnag; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldBeUnique; use Illuminate\Contracts\Queue\ShouldQueue; @@ -14,13 +15,6 @@ class UnhandledJob implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - public int $tries; - - public function __construct(int $tries) - { - $this->tries = $tries; - } - /** * Execute the job. * @@ -28,6 +22,8 @@ public function __construct(int $tries) */ public function handle() { + Bugsnag::leaveBreadcrumb(__METHOD__); + throw new RuntimeException('uh oh :o'); } } diff --git a/features/fixtures/laravel9/app/Providers/AppServiceProvider.php b/features/fixtures/laravel9/app/Providers/AppServiceProvider.php index 49823ec1..8c5de890 100644 --- a/features/fixtures/laravel9/app/Providers/AppServiceProvider.php +++ b/features/fixtures/laravel9/app/Providers/AppServiceProvider.php @@ -3,6 +3,7 @@ namespace App\Providers; use Illuminate\Support\ServiceProvider; +use Bugsnag\BugsnagLaravel\Facades\Bugsnag; class AppServiceProvider extends ServiceProvider { @@ -34,6 +35,6 @@ public function register() */ public function boot() { - // + Bugsnag::leaveBreadcrumb(__METHOD__); } } diff --git a/features/fixtures/laravel9/app/Providers/EventServiceProvider.php b/features/fixtures/laravel9/app/Providers/EventServiceProvider.php index 23499eb8..1e181fb4 100644 --- a/features/fixtures/laravel9/app/Providers/EventServiceProvider.php +++ b/features/fixtures/laravel9/app/Providers/EventServiceProvider.php @@ -6,6 +6,11 @@ use Illuminate\Auth\Listeners\SendEmailVerificationNotification; use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider; use Illuminate\Support\Facades\Event; +use Illuminate\Support\Facades\Queue; +use Bugsnag\BugsnagLaravel\Facades\Bugsnag; +use Illuminate\Queue\Events\JobProcessing; +use Illuminate\Queue\Events\JobProcessed; +use Illuminate\Queue\Events\JobExceptionOccurred; class EventServiceProvider extends ServiceProvider { @@ -27,6 +32,16 @@ class EventServiceProvider extends ServiceProvider */ public function boot() { - // + Queue::before(function (JobProcessing $event) { + Bugsnag::leaveBreadcrumb('before'); + }); + + Queue::after(function (JobProcessed $event) { + Bugsnag::leaveBreadcrumb('after'); + }); + + Queue::exceptionOccurred(function (JobExceptionOccurred $event) { + Bugsnag::leaveBreadcrumb('exceptionOccurred'); + }); } } diff --git a/features/fixtures/laravel9/config/database.php b/features/fixtures/laravel9/config/database.php index dc722b5f..d303ceee 100644 --- a/features/fixtures/laravel9/config/database.php +++ b/features/fixtures/laravel9/config/database.php @@ -37,8 +37,8 @@ 'sqlite' => [ 'driver' => 'sqlite', - 'url' => env('DATABASE_URL'), - 'database' => env('DB_DATABASE', database_path('database.sqlite')), + 'url' => null, + 'database' => database_path('database.sqlite'), 'prefix' => '', 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true), ], diff --git a/features/fixtures/laravel9/routes/web.php b/features/fixtures/laravel9/routes/web.php index b7204196..2ebe2765 100644 --- a/features/fixtures/laravel9/routes/web.php +++ b/features/fixtures/laravel9/routes/web.php @@ -1,6 +1,5 @@ query('tries', '1')); +Route::get('/queue/unhandled', function () { + \App\Jobs\UnhandledJob::dispatch(); }); -Route::get('/queue/handled', function (Request $request) { - \App\Jobs\HandledJob::dispatch((int) $request->query('tries', '1')); +Route::get('/queue/handled', function () { + \App\Jobs\HandledJob::dispatch(); }); /** diff --git a/features/fixtures/laravel9/run-fixture b/features/fixtures/laravel9/run-fixture deleted file mode 100755 index ea45de3d..00000000 --- a/features/fixtures/laravel9/run-fixture +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env bash - -# run the PHP webserver -php -S 0.0.0.0:8000 -t public & - -# wait for any process to exit -wait -n - -# exit with the status of the first process to exit -exit $? diff --git a/features/lib/laravel.rb b/features/lib/laravel.rb index 4f9d6301..a9066191 100644 --- a/features/lib/laravel.rb +++ b/features/lib/laravel.rb @@ -1,5 +1,7 @@ require 'net/http' require 'yaml' +require 'json' +require 'tempfile' class Laravel class << self @@ -27,8 +29,13 @@ def fixture_port end def major_version - # e.g. laravel56 -> 5, lumen8 -> 8 - Integer(/^(?:laravel|lumen)(\d)/.match(fixture)[1]) + # the first "canonical segment" is the first digit of the version number, + # aka the major version + version.canonical_segments.first + end + + def version + @version ||= load_version_from_fixture end def lumen? @@ -46,6 +53,27 @@ def supports_sessions? true end + # the command to run the queue worker for a single job + def queue_worker_once_command(tries) + if version < "5.3.0" + "php artisan queue:work --tries=#{tries}" + else + "php artisan queue:work --once --tries=#{tries}" + end + end + + # the command to run the queue worker as a daemon + def queue_worker_daemon_command(tries) + # the command to run the queue worker was 'queue:listen' but changed to + # 'queue:work' in Laravel 5.3 ('queue:work' exists on older Laravels, but + # is not quite equivalent) + if version < "5.3.0" + "php artisan queue:listen --tries=#{tries}" + else + "php artisan queue:work --tries=#{tries}" + end + end + private def load_port_from_docker_compose @@ -54,5 +82,31 @@ def load_port_from_docker_compose service.fetch("ports").first.fetch("published") end + + def load_version_from_fixture + # get and parse the composer.lock file from the fixture + composer_lock = Tempfile.create("#{fixture}-composer.lock") do |file| + # copy the composer lock file out of the fixture so we can read it + Maze::Docker.cp(fixture, source: "/app/composer.lock", destination: file.path) + + # 'file.read' won't reflect the changes made by docker cp, so we use + # JSON.load_file to reload the file & parse it + JSON.load_file(file.path) + end + + framework_section = composer_lock["packages"].find { |package| package["name"] == framework_package_name } + version = framework_section["version"].delete_prefix("v") + + Gem::Version.new(version) + end + + # the composer package name of the framework being used (Lumen or Laravel) + def framework_package_name + if lumen? + "laravel/lumen-framework" + else + "laravel/framework" + end + end end end diff --git a/features/queues.feature b/features/queues.feature index 62cc4192..d0a15192 100644 --- a/features/queues.feature +++ b/features/queues.feature @@ -1,7 +1,12 @@ Feature: Queue support -@not-laravel-latest @not-laravel51 @not-laravel56 @not-laravel58 @not-laravel66 @not-lumen8 -Scenario: Unhandled exceptions are delivered from queues +Background: + # disable automatic query breadcrumbs as we assert against the specific number + # of breadcrumbs in these tests + Given I set environment variable "BUGSNAG_QUERY" to "false" + +@not-laravel-latest @not-lumen8 +Scenario: Unhandled exceptions are delivered from queues when running the queue worker as a daemon Given I start the laravel fixture And I start the laravel queue worker When I navigate to the route "/queue/unhandled" @@ -9,77 +14,129 @@ Scenario: Unhandled exceptions are delivered from queues Then the error is valid for the error reporting API version "4.0" for the "Bugsnag Laravel" notifier And the exception "errorClass" equals "RuntimeException" And the exception "message" equals "uh oh :o" - And the event "metaData.job.name" equals "Illuminate\Queue\CallQueuedHandler@call" - And the event "metaData.job.queue" equals "default" - And the event "metaData.job.attempts" equals 1 - And the event "metaData.job.connection" equals "database" - And the event "metaData.job.resolved" equals "App\Jobs\UnhandledJob" - And the event "app.type" equals "Queue" - And the event "context" equals "App\Jobs\UnhandledJob" And the event "severity" equals "error" And the event "unhandled" is true And the event "severityReason.type" equals "unhandledExceptionMiddleware" And the event "severityReason.attributes.framework" equals "Laravel" + And the event has a "manual" breadcrumb named "App\Jobs\UnhandledJob::handle" + And on Laravel versions >= 5.2: + """ + the event "metaData.job.name" equals "Illuminate\Queue\CallQueuedHandler@call" + the event "metaData.job.queue" equals "default" + the event "metaData.job.attempts" equals 1 + the event "metaData.job.connection" equals "database" + the event "metaData.job.resolved" equals "App\Jobs\UnhandledJob" + the event "app.type" equals "Queue" + the event "context" equals "App\Jobs\UnhandledJob" + the event has a "manual" breadcrumb named "before" + the event has a "manual" breadcrumb named "exceptionOccurred" + the event has 3 breadcrumbs + """ + And on Laravel versions < 5.2: + """ + the event "metaData.job" is null + the event has a "manual" breadcrumb named "App\Providers\AppServiceProvider::boot" + the event has 2 breadcrumbs + """ -@not-laravel-latest @not-laravel51 @not-laravel56 @not-laravel58 @not-laravel66 @not-lumen8 -Scenario: Unhandled exceptions are delivered from queued jobs with multiple attmpts +@not-laravel-latest @not-lumen8 +Scenario: Unhandled exceptions are delivered from queued jobs with multiple attmpts when running the queue worker as a daemon Given I start the laravel fixture - And I start the laravel queue worker - When I navigate to the route "/queue/unhandled?tries=3" + And I start the laravel queue worker with --tries=3 + When I navigate to the route "/queue/unhandled" And I wait to receive 3 errors # attempt 1 Then the error is valid for the error reporting API version "4.0" for the "Bugsnag Laravel" notifier And the exception "errorClass" equals "RuntimeException" And the exception "message" equals "uh oh :o" - And the event "metaData.job.name" equals "Illuminate\Queue\CallQueuedHandler@call" - And the event "metaData.job.queue" equals "default" - And the event "metaData.job.attempts" equals 1 - And the event "metaData.job.connection" equals "database" - And the event "metaData.job.resolved" equals "App\Jobs\UnhandledJob" - And the event "app.type" equals "Queue" - And the event "context" equals "App\Jobs\UnhandledJob" And the event "severity" equals "error" And the event "unhandled" is true And the event "severityReason.type" equals "unhandledExceptionMiddleware" And the event "severityReason.attributes.framework" equals "Laravel" + And the event has a "manual" breadcrumb named "App\Jobs\UnhandledJob::handle" + And on Laravel versions >= 5.2: + """ + the event "metaData.job.name" equals "Illuminate\Queue\CallQueuedHandler@call" + the event "metaData.job.queue" equals "default" + the event "metaData.job.attempts" equals 1 + the event "metaData.job.connection" equals "database" + the event "metaData.job.resolved" equals "App\Jobs\UnhandledJob" + the event "app.type" equals "Queue" + the event "context" equals "App\Jobs\UnhandledJob" + the event has a "manual" breadcrumb named "before" + the event has a "manual" breadcrumb named "exceptionOccurred" + the event has 3 breadcrumbs + """ + And on Laravel versions < 5.2: + """ + the event "metaData.job" is null + the event has a "manual" breadcrumb named "App\Providers\AppServiceProvider::boot" + the event has 2 breadcrumbs + """ # attempt 2 When I discard the oldest error Then the error is valid for the error reporting API version "4.0" for the "Bugsnag Laravel" notifier And the exception "errorClass" equals "RuntimeException" And the exception "message" equals "uh oh :o" - And the event "metaData.job.name" equals "Illuminate\Queue\CallQueuedHandler@call" - And the event "metaData.job.queue" equals "default" - And the event "metaData.job.attempts" equals 2 - And the event "metaData.job.connection" equals "database" - And the event "metaData.job.resolved" equals "App\Jobs\UnhandledJob" - And the event "app.type" equals "Queue" - And the event "context" equals "App\Jobs\UnhandledJob" And the event "severity" equals "error" And the event "unhandled" is true And the event "severityReason.type" equals "unhandledExceptionMiddleware" And the event "severityReason.attributes.framework" equals "Laravel" + And the event has a "manual" breadcrumb named "App\Jobs\UnhandledJob::handle" + And on Laravel versions >= 5.2: + """ + the event "metaData.job.name" equals "Illuminate\Queue\CallQueuedHandler@call" + the event "metaData.job.queue" equals "default" + the event "metaData.job.attempts" equals 2 + the event "metaData.job.connection" equals "database" + the event "metaData.job.resolved" equals "App\Jobs\UnhandledJob" + the event "app.type" equals "Queue" + the event "context" equals "App\Jobs\UnhandledJob" + the event has a "manual" breadcrumb named "before" + the event has a "manual" breadcrumb named "exceptionOccurred" + the event has 3 breadcrumbs + """ + And on Laravel versions < 5.2: + """ + the event "metaData.job" is null + the event has a "manual" breadcrumb named "App\Providers\AppServiceProvider::boot" + the event has 2 breadcrumbs + """ # attempt 3 When I discard the oldest error Then the error is valid for the error reporting API version "4.0" for the "Bugsnag Laravel" notifier And the exception "errorClass" equals "RuntimeException" And the exception "message" equals "uh oh :o" - And the event "metaData.job.name" equals "Illuminate\Queue\CallQueuedHandler@call" - And the event "metaData.job.queue" equals "default" - And the event "metaData.job.attempts" equals 3 - And the event "metaData.job.connection" equals "database" - And the event "metaData.job.resolved" equals "App\Jobs\UnhandledJob" - And the event "app.type" equals "Queue" - And the event "context" equals "App\Jobs\UnhandledJob" And the event "severity" equals "error" And the event "unhandled" is true And the event "severityReason.type" equals "unhandledExceptionMiddleware" And the event "severityReason.attributes.framework" equals "Laravel" + And the event has a "manual" breadcrumb named "App\Jobs\UnhandledJob::handle" + And on Laravel versions >= 5.2: + """ + the event "metaData.job.name" equals "Illuminate\Queue\CallQueuedHandler@call" + the event "metaData.job.queue" equals "default" + the event "metaData.job.attempts" equals 3 + the event "metaData.job.connection" equals "database" + the event "metaData.job.resolved" equals "App\Jobs\UnhandledJob" + the event "app.type" equals "Queue" + the event "context" equals "App\Jobs\UnhandledJob" + the event has a "manual" breadcrumb named "before" + the event has a "manual" breadcrumb named "exceptionOccurred" + the event has 3 breadcrumbs + """ + And on Laravel versions < 5.2: + """ + the event "metaData.job" is null + the event has a "manual" breadcrumb named "App\Providers\AppServiceProvider::boot" + the event has 2 breadcrumbs + """ -@not-laravel-latest @not-laravel51 @not-laravel56 @not-laravel58 @not-laravel66 @not-lumen8 -Scenario: Handled exceptions are delivered from queues +@not-laravel-latest @not-lumen8 +Scenario: Handled exceptions are delivered from queues when running the queue worker as a daemon Given I start the laravel fixture And I start the laravel queue worker When I navigate to the route "/queue/handled" @@ -87,13 +144,193 @@ Scenario: Handled exceptions are delivered from queues Then the error is valid for the error reporting API version "4.0" for the "Bugsnag Laravel" notifier And the exception "errorClass" equals "Exception" And the exception "message" equals "Handled :)" - And the event "metaData.job.name" equals "Illuminate\Queue\CallQueuedHandler@call" - And the event "metaData.job.queue" equals "default" - And the event "metaData.job.attempts" equals 1 - And the event "metaData.job.connection" equals "database" - And the event "metaData.job.resolved" equals "App\Jobs\HandledJob" - And the event "app.type" equals "Queue" - And the event "context" equals "App\Jobs\HandledJob" And the event "severity" equals "warning" And the event "unhandled" is false And the event "severityReason.type" equals "handledException" + And the event has a "manual" breadcrumb named "App\Jobs\HandledJob::handle" + And the event has 2 breadcrumbs + And on Laravel versions >= 5.2: + """ + the event "metaData.job.name" equals "Illuminate\Queue\CallQueuedHandler@call" + the event "metaData.job.queue" equals "default" + the event "metaData.job.attempts" equals 1 + the event "metaData.job.connection" equals "database" + the event "metaData.job.resolved" equals "App\Jobs\HandledJob" + the event "app.type" equals "Queue" + the event "context" equals "App\Jobs\HandledJob" + the event has a "manual" breadcrumb named "before" + """ + And on Laravel versions < 5.2: + """ + the event "metaData.job" is null + the event has a "manual" breadcrumb named "App\Providers\AppServiceProvider::boot" + """ + +@not-laravel-latest @not-lumen8 +Scenario: Unhandled exceptions are delivered from queues when running the queue worker once + Given I start the laravel fixture + When I navigate to the route "/queue/unhandled" + Then I should receive no errors + When I run the laravel queue worker + And I wait to receive an error + Then the error is valid for the error reporting API version "4.0" for the "Bugsnag Laravel" notifier + And the exception "errorClass" equals "RuntimeException" + And the exception "message" equals "uh oh :o" + And the event "severity" equals "error" + And the event "unhandled" is true + And the event "severityReason.type" equals "unhandledExceptionMiddleware" + And the event "severityReason.attributes.framework" equals "Laravel" + And the event has a "manual" breadcrumb named "App\Providers\AppServiceProvider::boot" + And the event has a "manual" breadcrumb named "App\Jobs\UnhandledJob::handle" + And on Laravel versions >= 5.2: + """ + the event "metaData.job.name" equals "Illuminate\Queue\CallQueuedHandler@call" + the event "metaData.job.queue" equals "default" + the event "metaData.job.attempts" equals 1 + the event "metaData.job.connection" equals "database" + the event "metaData.job.resolved" equals "App\Jobs\UnhandledJob" + the event "app.type" equals "Queue" + the event "context" equals "App\Jobs\UnhandledJob" + the event has a "manual" breadcrumb named "before" + the event has a "manual" breadcrumb named "exceptionOccurred" + the event has 4 breadcrumbs + """ + And on Laravel versions < 5.2: + """ + the event "metaData.job" is null + the event has 2 breadcrumbs + """ + +@not-laravel-latest @not-lumen8 +Scenario: Unhandled exceptions are delivered from queued jobs with multiple attmpts when running the queue worker once + Given I start the laravel fixture + When I navigate to the route "/queue/unhandled" + Then I should receive no errors + + # attempt 1 + When I run the laravel queue worker with --tries=3 + And I wait to receive an error + Then the error is valid for the error reporting API version "4.0" for the "Bugsnag Laravel" notifier + And the exception "errorClass" equals "RuntimeException" + And the exception "message" equals "uh oh :o" + And the event "severity" equals "error" + And the event "unhandled" is true + And the event "severityReason.type" equals "unhandledExceptionMiddleware" + And the event "severityReason.attributes.framework" equals "Laravel" + And the event has a "manual" breadcrumb named "App\Providers\AppServiceProvider::boot" + And the event has a "manual" breadcrumb named "App\Jobs\UnhandledJob::handle" + And on Laravel versions >= 5.2: + """ + the event "metaData.job.name" equals "Illuminate\Queue\CallQueuedHandler@call" + the event "metaData.job.queue" equals "default" + the event "metaData.job.attempts" equals 1 + the event "metaData.job.connection" equals "database" + the event "metaData.job.resolved" equals "App\Jobs\UnhandledJob" + the event "app.type" equals "Queue" + the event "context" equals "App\Jobs\UnhandledJob" + the event has a "manual" breadcrumb named "before" + the event has a "manual" breadcrumb named "exceptionOccurred" + the event has 4 breadcrumbs + """ + And on Laravel versions < 5.2: + """ + the event "metaData.job" is null + the event has 2 breadcrumbs + """ + + # attempt 2 + When I discard the oldest error + And I run the laravel queue worker with --tries=3 + And I wait to receive an error + Then the error is valid for the error reporting API version "4.0" for the "Bugsnag Laravel" notifier + And the exception "errorClass" equals "RuntimeException" + And the exception "message" equals "uh oh :o" + And the event "severity" equals "error" + And the event "unhandled" is true + And the event "severityReason.type" equals "unhandledExceptionMiddleware" + And the event "severityReason.attributes.framework" equals "Laravel" + And the event has a "manual" breadcrumb named "App\Providers\AppServiceProvider::boot" + And the event has a "manual" breadcrumb named "App\Jobs\UnhandledJob::handle" + And on Laravel versions >= 5.2: + """ + the event "metaData.job.name" equals "Illuminate\Queue\CallQueuedHandler@call" + the event "metaData.job.queue" equals "default" + the event "metaData.job.attempts" equals 2 + the event "metaData.job.connection" equals "database" + the event "metaData.job.resolved" equals "App\Jobs\UnhandledJob" + the event "app.type" equals "Queue" + the event "context" equals "App\Jobs\UnhandledJob" + the event has a "manual" breadcrumb named "before" + the event has a "manual" breadcrumb named "exceptionOccurred" + the event has 4 breadcrumbs + """ + And on Laravel versions < 5.2: + """ + the event "metaData.job" is null + the event has 2 breadcrumbs + """ + + # attempt 3 + When I discard the oldest error + And I run the laravel queue worker with --tries=3 + And I wait to receive an error + Then the error is valid for the error reporting API version "4.0" for the "Bugsnag Laravel" notifier + And the exception "errorClass" equals "RuntimeException" + And the exception "message" equals "uh oh :o" + And the event "severity" equals "error" + And the event "unhandled" is true + And the event "severityReason.type" equals "unhandledExceptionMiddleware" + And the event "severityReason.attributes.framework" equals "Laravel" + And the event has a "manual" breadcrumb named "App\Providers\AppServiceProvider::boot" + And the event has a "manual" breadcrumb named "App\Jobs\UnhandledJob::handle" + And on Laravel versions >= 5.2: + """ + the event "metaData.job.name" equals "Illuminate\Queue\CallQueuedHandler@call" + the event "metaData.job.queue" equals "default" + the event "metaData.job.attempts" equals 3 + the event "metaData.job.connection" equals "database" + the event "metaData.job.resolved" equals "App\Jobs\UnhandledJob" + the event "app.type" equals "Queue" + the event "context" equals "App\Jobs\UnhandledJob" + the event has a "manual" breadcrumb named "before" + the event has a "manual" breadcrumb named "exceptionOccurred" + the event has 4 breadcrumbs + """ + And on Laravel versions < 5.2: + """ + the event "metaData.job" is null + the event has 2 breadcrumbs + """ + +@not-laravel-latest @not-lumen8 +Scenario: Handled exceptions are delivered from queues when running the queue worker once + Given I start the laravel fixture + When I navigate to the route "/queue/handled" + Then I should receive no errors + When I run the laravel queue worker + And I wait to receive an error + Then the error is valid for the error reporting API version "4.0" for the "Bugsnag Laravel" notifier + And the exception "errorClass" equals "Exception" + And the exception "message" equals "Handled :)" + And the event "severity" equals "warning" + And the event "unhandled" is false + And the event "severityReason.type" equals "handledException" + And the event has a "manual" breadcrumb named "App\Providers\AppServiceProvider::boot" + And the event has a "manual" breadcrumb named "App\Jobs\HandledJob::handle" + And on Laravel versions >= 5.2: + """ + the event "metaData.job.name" equals "Illuminate\Queue\CallQueuedHandler@call" + the event "metaData.job.queue" equals "default" + the event "metaData.job.attempts" equals 1 + the event "metaData.job.connection" equals "database" + the event "metaData.job.resolved" equals "App\Jobs\HandledJob" + the event "app.type" equals "Queue" + the event "context" equals "App\Jobs\HandledJob" + the event has a "manual" breadcrumb named "before" + the event has 3 breadcrumbs + """ + And on Laravel versions < 5.2: + """ + the event "metaData.job" is null + the event has 2 breadcrumbs + """ diff --git a/features/steps/laravel_steps.rb b/features/steps/laravel_steps.rb index 7777928e..f7bddfc6 100644 --- a/features/steps/laravel_steps.rb +++ b/features/steps/laravel_steps.rb @@ -14,22 +14,41 @@ } end -# TODO: contribute this back to Maze Runner -# https://github.com/bugsnag/maze-runner/pull/425 module Maze class Docker class << self + # TODO: remove when https://github.com/bugsnag/maze-runner/pull/425 is merged def exec(service, command, detach: false) flags = detach ? "--detach" : "" run_docker_compose_command("exec #{flags} #{service} #{command}") end + + # TODO: contribute this back to Maze Runner + # probably need a nicer API, capable of doing a copy in either + # direction (right now this can only copy from the service to the + # local machine) + def cp(service, source:, destination:) + run_docker_compose_command("cp #{service}:#{source} #{destination}") + end end end end When("I start the laravel queue worker") do - Maze::Docker.exec(Laravel.fixture, "php artisan queue:work", detach: true) + step("I start the laravel queue worker with --tries=1") +end + +When("I start the laravel queue worker with --tries={int}") do |tries| + Maze::Docker.exec(Laravel.fixture, Laravel.queue_worker_daemon_command(tries), detach: true) +end + +When("I run the laravel queue worker") do + step("I run the laravel queue worker with --tries=1") +end + +When("I run the laravel queue worker with --tries={int}") do |tries| + Maze::Docker.exec(Laravel.fixture, Laravel.queue_worker_once_command(tries)) end When("I navigate to the route {string}") do |route| @@ -99,3 +118,67 @@ def exec(service, command, detach: false) step("the session payload field '#{path}' matches the regex '^((\\d+\\.){2}\\d+|\\d\\.x-dev)$'") end + +# TODO: remove when https://github.com/bugsnag/maze-runner/pull/433 is released +Then("the event has {int} breadcrumb(s)") do |expected| + breadcrumbs = Maze::Server.errors.current[:body]['events'].first['breadcrumbs'] + + Maze.check.equal( + expected, + breadcrumbs.length, + "Expected event to have '#{expected}' breadcrumbs, but got: #{breadcrumbs}" + ) +end + +Then("the event has no breadcrumbs") do + breadcrumbs = Maze::Server.errors.current[:body]['events'].first['breadcrumbs'] + + Maze.check.true( + breadcrumbs.nil? || breadcrumbs.empty?, + "Expected event not to have breadcrumbs, but got: #{breadcrumbs}" + ) +end + +# conditionally run a step if the laravel version matches a specified version +# +# e.g. this will only check app.type on Laravel 5.2 and above: +# on Laravel versions > 5.1 the event "app.type" equals "Queue" +Then(/^on Laravel versions (>=?|<=?|==) ([0-9.]+) (.*)$/) do |operator, version, step_to_run| + should_run_step = Laravel.version.send(operator, version) + + # make sure this step is debuggable! + $logger.debug("Laravel v#{Laravel.version} #{operator} #{version}? #{should_run_step}") + + if should_run_step + step(step_to_run) + else + $logger.info("Skipping step on Laravel v#{Laravel.version}: #{step_to_run}") + end +end + +# conditionally run a number of steps if the laravel version matches a specified version +# +# e.g. this will only run the indented steps on Laravel 5.2 and above: +# on Laravel versions > 5.1: +# """ +# the event "app.type" equals "Queue" +# the event "other.thing" equals "yes" +# """ +Then(/^on Laravel versions (>=?|<=?|==) ([0-9.]+):/) do |operator, version, steps_to_run| + should_run_steps = Laravel.version.send(operator, version) + + # make sure this step is debuggable! + $logger.debug("Laravel v#{Laravel.version} #{operator} #{version}? #{should_run_steps}") + + if should_run_steps + steps_to_run.each_line(chomp: true) do |step_to_run| + step(step_to_run) + end + else + indent = " " * 4 + # e.g. "a step\nanother step\n" -> " 1) a step\n 2) another step" + steps_indented = steps_to_run.each_line.map.with_index(1) { |step, i| "#{indent}#{i}) #{step.chomp}" }.join("\n") + + $logger.info("Skipping steps on Laravel v#{Laravel.version}:\n#{steps_indented}") + end +end diff --git a/src/BugsnagServiceProvider.php b/src/BugsnagServiceProvider.php index 9fba2014..146fc37d 100644 --- a/src/BugsnagServiceProvider.php +++ b/src/BugsnagServiceProvider.php @@ -37,7 +37,7 @@ class BugsnagServiceProvider extends ServiceProvider * * @var string */ - const VERSION = '2.25.0'; + const VERSION = '2.25.1'; /** * Boot the service provider. @@ -180,6 +180,71 @@ protected function setupQueue(QueueManager $queue) $this->app->make(Tracker::class)->set($job); }); + + $this->setupQueueForLaravelVapor($queue); + } + + /** + * Setup queue events for Laravel Vapor. + * + * This is required because Laravel Vapor's queue system doesn't behave as + * a daemonised queue worker (the 'looping' event never fires) but also + * doesn't behave as a non-daemonised queue worker (our shutdown function + * never fires). + * + * @param QueueManager $queue + * + * @return void + */ + private function setupQueueForLaravelVapor(QueueManager $queue) + { + // ensure we're running on vapor + // this is how vapor-core does it, e.g.: + // https://github.com/laravel/vapor-core/blob/61437221090850ba6e51dce15d0058d362654f9b/src/ConfiguresAssets.php#L16-L19 + if (!isset($_ENV['VAPOR_SSM_PATH'])) { + return; + } + + // used to keep track of if we're the ones disabling batch sending, so we + // know if we need to re-enable it - if the user disables batch sending + // then they don't want it enabled at all + static $batchSendingWasDisabledByUs = false; + + $queue->before(function () use (&$batchSendingWasDisabledByUs) { + // clear breadcrumbs to stop them leaking between jobs + $this->app->bugsnag->clearBreadcrumbs(); + + // only re-enable batch sending if we're the ones disabling it + // this allows users to disable batch sending entirely + if ($batchSendingWasDisabledByUs) { + $this->app->bugsnag->setBatchSending(true); + } + }); + + $flush = function () use (&$batchSendingWasDisabledByUs) { + // flush any events created in this job + $this->app->bugsnag->flush(); + + // disable batch sending so any events after this get sent synchronously + // this is important as exceptions are logged after the 'exceptionOccurred' + // event fires, so the above flush is too early to send them + // these exceptions would get sent after processing the next queued job, + // but we'd still drop the last event when this queue worker stops running + if ($this->app->bugsnag->isBatchSending()) { + $this->app->bugsnag->setBatchSending(false); + $batchSendingWasDisabledByUs = true; + } + }; + + // added in 5.2.41 + if (method_exists($queue, 'after')) { + $queue->after($flush); + } + + // added in 5.2.41 + if (method_exists($queue, 'exceptionOccurred')) { + $queue->exceptionOccurred($flush); + } } /** diff --git a/src/Internal/BacktraceProcessor.php b/src/Internal/BacktraceProcessor.php index 6fc11bce..512f0d7f 100644 --- a/src/Internal/BacktraceProcessor.php +++ b/src/Internal/BacktraceProcessor.php @@ -65,6 +65,7 @@ final class BacktraceProcessor */ const LARAVEL_VENDOR_NAMESPACE = 'Illuminate\\'; const LUMEN_VENDOR_NAMESPACE = 'Laravel\\'; + const COLLISION_VENDOR_NAMESPACE = 'NunoMaduro\\Collision\\'; /** * The current state; one of the self::STATE_ constants. @@ -132,7 +133,7 @@ private function processFrame(array $frame) // if this class is a framework exception handler and the function // matches self::HANDLER_METHOD, we can move on to searching for // the caller - if (($class === self::LARAVEL_HANDLER_CLASS || $class === self::LUMEN_HANDLER_CLASS) + if ($this->isFrameworkExceptionHandler($class) && isset($frame['function']) && $frame['function'] === self::HANDLER_METHOD ) { @@ -144,10 +145,7 @@ private function processFrame(array $frame) case self::STATE_HANDLER_CALLER: // if this is an app exception handler or a framework class, we // can move on to determine if this was unhandled or not - if ($class === self::LARAVEL_APP_EXCEPTION_HANDLER - || $class === self::LUMEN_APP_EXCEPTION_HANDLER - || $this->isVendor($class) - ) { + if ($this->isAppExceptionHandler($class) || $this->isVendor($class)) { $this->state = self::STATE_IS_UNHANDLED; } @@ -180,7 +178,47 @@ private function processFrame(array $frame) */ private function isVendor($class) { - return substr($class, 0, strlen(self::LARAVEL_VENDOR_NAMESPACE)) === self::LARAVEL_VENDOR_NAMESPACE - || substr($class, 0, strlen(self::LUMEN_VENDOR_NAMESPACE)) === self::LUMEN_VENDOR_NAMESPACE; + return $this->isInNamespace($class, self::LARAVEL_VENDOR_NAMESPACE) + || $this->isInNamespace($class, self::LUMEN_VENDOR_NAMESPACE) + || $this->isInNamespace($class, self::COLLISION_VENDOR_NAMESPACE); + } + + /** + * Check if the given class is in the given namespace. + * + * @param string $class + * @param string $namespace + * + * @return bool + */ + private function isInNamespace($class, $namespace) + { + return substr($class, 0, strlen($namespace)) === $namespace; + } + + /** + * Is the given class Laravel or Lumen's exception handler? + * + * @param string $class + * + * @return bool + */ + private function isFrameworkExceptionHandler($class) + { + return $class === self::LARAVEL_HANDLER_CLASS + || $class === self::LUMEN_HANDLER_CLASS; + } + + /** + * Is the given class an App's exception handler? + * + * @param string $class + * + * @return bool + */ + private function isAppExceptionHandler($class) + { + return $class === self::LARAVEL_APP_EXCEPTION_HANDLER + || $class === self::LUMEN_APP_EXCEPTION_HANDLER; } } diff --git a/tests/Middleware/UnhandledStateTest.php b/tests/Middleware/UnhandledStateTest.php index 79b045a6..658538e7 100644 --- a/tests/Middleware/UnhandledStateTest.php +++ b/tests/Middleware/UnhandledStateTest.php @@ -159,6 +159,34 @@ public function unhandledBacktraceProviderLaravel() ['function' => 'z'], ['class' => \Yet\AnotherClass::class], ]]; + + yield 'backtrace with NunoMaduro\\Collision Adapter' => [[ + [ + 'file' => '/app/vendor/laravel/framework/src/Illuminate/Foundation/Exceptions/Handler.php', + 'function' => 'error', + 'class' => 'Illuminate\\Log\\LogManager', + ], + [ + 'file' => '/app/app/Exceptions/Handler.php', + 'function' => 'report', + 'class' => 'Illuminate\\Foundation\\Exceptions\\Handler', + ], + [ + 'file' => '/app/vendor/nunomaduro/collision/src/Adapters/Laravel/ExceptionHandler.php', + 'function' => 'report', + 'class' => 'App\\Exceptions\\Handler', + ], + [ + 'file' => '/app/vendor/laravel/framework/src/Illuminate/Queue/Worker.php', + 'function' => 'report', + 'class' => 'NunoMaduro\\Collision\\Adapters\\Laravel\\ExceptionHandler', + ], + [ + 'file' => '/app/vendor/laravel/framework/src/Illuminate/Queue/Worker.php', + 'function' => 'runJob', + 'class' => 'Illuminate\\Queue\\Worker', + ], + ]]; } public function unhandledBacktraceProviderLumen()