From 665261f1f6f3ba512cdbe4a7a8bf7112f5dc1884 Mon Sep 17 00:00:00 2001 From: Joe Haines Date: Thu, 26 Aug 2021 15:03:03 +0100 Subject: [PATCH 1/6] Add unit tests for UnhandledState --- tests/Middleware/UnhandledStateTest.php | 244 ++++++++++++++++++++++++ tests/Stubs/DebugBacktraceStub.php | 32 ++++ 2 files changed, 276 insertions(+) create mode 100644 tests/Middleware/UnhandledStateTest.php create mode 100644 tests/Stubs/DebugBacktraceStub.php diff --git a/tests/Middleware/UnhandledStateTest.php b/tests/Middleware/UnhandledStateTest.php new file mode 100644 index 00000000..a2a03d5e --- /dev/null +++ b/tests/Middleware/UnhandledStateTest.php @@ -0,0 +1,244 @@ +report = Report::fromPHPThrowable( + new Configuration('api-key'), + new Exception('abc') + ); + + $this->nextWasCalled = false; + $this->next = function (Report $report) { + $this->assertSame($this->report, $report); + $this->nextWasCalled = true; + }; + } + + /** + * @after + */ + protected function afterEach() + { + DebugBacktraceStub::clear(); + + $this->assertTrue($this->nextWasCalled); + } + + /** + * @dataProvider unhandledBacktraceProvider + */ + public function testReportIsUnhandled(array $backtrace) + { + DebugBacktraceStub::set($backtrace); + + $this->assertFalse($this->report->getUnhandled()); + $this->assertSame(['type' => 'handledException'], $this->report->getSeverityReason()); + + $a = new UnhandledState(); + $a->__invoke($this->report, $this->next); + + $this->assertTrue( + $this->report->getUnhandled(), + 'Expected the report to be unhandled but it was handled!' + ); + + $this->assertSame( + [ + 'type' => 'unhandledExceptionMiddleware', + 'attributes' => ['framework' => 'Laravel'], + ], + $this->report->getSeverityReason() + ); + } + + public function unhandledBacktraceProvider() + { + yield 'minimal backtrace' => [[ + // the backtrace must go through the Handler::report method + ['class' => \Illuminate\Foundation\Exceptions\Handler::class, 'function' => 'report'], + // then any class in the Illuminate namespace + ['class' => \Illuminate\Something::class], + // followed by any other class in the Illuminate namespace + ['class' => \Illuminate\SomethingElse::class], + ]]; + + yield 'minimal backtrace (App exception handler)' => [[ + // the backtrace must go through the Handler::report method + ['class' => \Illuminate\Foundation\Exceptions\Handler::class, 'function' => 'report'], + // then through the app exception handler + ['class' => \App\Exception\Handler::class], + // followed by any other class in the Illuminate namespace + ['class' => \Illuminate\SomethingElse::class], + ]]; + + yield 'backtrace with other classes' => [[ + ['class' => \SomeClass::class], + ['class' => \Illuminate\Foundation\Exceptions\Handler::class, 'function' => 'report'], + ['class' => \Some\OtherClass::class], + ['class' => \Illuminate\Abc::class], + ['class' => \Illuminate\AbcElse::class], + ['class' => \Yet\AnotherClass::class], + ]]; + + yield 'backtrace with other classes (App exception handler)' => [[ + ['class' => \SomeClass::class], + ['class' => \Illuminate\Foundation\Exceptions\Handler::class, 'function' => 'report'], + ['class' => \Some\OtherClass::class], + ['class' => \App\Exception\Handler::class], + ['class' => \Illuminate\AbcElse::class], + ['class' => \Yet\AnotherClass::class], + ]]; + + yield 'backtrace with non-class frames' => [[ + ['class' => \SomeClass::class], + ['function' => 'a'], + ['class' => \Illuminate\Foundation\Exceptions\Handler::class, 'function' => 'report'], + ['function' => 'b'], + ['function' => 'c'], + ['class' => \Some\OtherClass::class], + ['class' => \Illuminate\Abc::class], + ['function' => 'x'], + ['function' => 'y'], + ['class' => \Illuminate\AbcElse::class], + ['function' => 'z'], + ['class' => \Yet\AnotherClass::class], + ]]; + + yield 'backtrace with non-class frames (App exception handler)' => [[ + ['class' => \SomeClass::class], + ['function' => 'a'], + ['class' => \Illuminate\Foundation\Exceptions\Handler::class, 'function' => 'report'], + ['function' => 'b'], + ['function' => 'c'], + ['class' => \Some\OtherClass::class], + ['class' => \App\Exception\Handler::class], + ['function' => 'x'], + ['function' => 'y'], + ['class' => \Illuminate\AbcElse::class], + ['function' => 'z'], + ['class' => \Yet\AnotherClass::class], + ]]; + } + + /** + * @dataProvider handledBacktraceProvider + */ + public function testReportIsHandled(array $backtrace) + { + DebugBacktraceStub::set($backtrace); + + $this->assertFalse($this->report->getUnhandled()); + $this->assertSame(['type' => 'handledException'], $this->report->getSeverityReason()); + + $a = new UnhandledState(); + $a->__invoke($this->report, $this->next); + + $this->assertFalse($this->report->getUnhandled()); + $this->assertSame(['type' => 'handledException'], $this->report->getSeverityReason()); + } + + public function handledBacktraceProvider() + { + yield 'empty backtrace' => [[[]]]; + + yield 'no illuminate exception handler' => [[ + ['class' => \NotIlluminate\Foundation\Exceptions\Handler::class, 'function' => 'report'], + ['class' => \Illuminate\Something::class], + ['class' => \Illuminate\SomethingElse::class], + ]]; + + yield 'illuminate exception handler but wrong method' => [[ + ['class' => \Illuminate\Foundation\Exceptions\Handler::class, 'function' => 'notReport'], + ['class' => \Illuminate\Something::class], + ['class' => \Illuminate\SomethingElse::class], + ]]; + + yield 'no illuminate exception handler (App exception handler)' => [[ + ['class' => \NotIlluminate\Foundation\Exceptions\Handler::class, 'function' => 'report'], + ['class' => \App\Exception\Handler::class], + ['class' => \Illuminate\SomethingElse::class], + ]]; + + yield 'illuminate exception handler but wrong method (App exception handler)' => [[ + ['class' => \Illuminate\Foundation\Exceptions\Handler::class, 'function' => 'notReport'], + ['class' => \App\Exception\Handler::class], + ['class' => \Illuminate\SomethingElse::class], + ]]; + + yield 'illuminate namespace not at the beginning' => [[ + ['class' => \Illuminate\Foundation\Exceptions\Handler::class, 'function' => 'report'], + ['class' => \Abc\Illuminate\Something::class], + ['class' => \Abc\Illuminate\SomethingElse::class], + ]]; + + yield 'illuminate namespace not at the beginning (App exception handler)' => [[ + ['class' => \Illuminate\Foundation\Exceptions\Handler::class, 'function' => 'report'], + ['class' => \App\Exception\Handler::class], + ['class' => \Abc\Illuminate\SomethingElse::class], + ]]; + + yield 'no consecutive Illuminate classes' => [[ + ['class' => \SomeClass::class], + ['class' => \Illuminate\Foundation\Exceptions\Handler::class, 'function' => 'report'], + ['class' => \Some\OtherClass::class], + ['class' => \Illuminate\Abc::class], + // this ensures the report is handled as there must be two + // consecutive Illuminate frames + ['class' => \AnotherClass::class], + ['class' => \Illuminate\AbcElse::class], + ['class' => \Yet\AnotherClass::class], + ]]; + + yield 'no consecutive Illuminate classes (App exception handler)' => [[ + ['class' => \SomeClass::class], + ['class' => \Illuminate\Foundation\Exceptions\Handler::class, 'function' => 'report'], + ['class' => \Some\OtherClass::class], + ['class' => \App\Exception\Handler::class], + // this ensures the report is handled as there must be two + // consecutive Illuminate frames + ['class' => \AnotherClass::class], + ['class' => \Illuminate\AbcElse::class], + ['class' => \Yet\AnotherClass::class], + ]]; + } +} diff --git a/tests/Stubs/DebugBacktraceStub.php b/tests/Stubs/DebugBacktraceStub.php new file mode 100644 index 00000000..df5bb9e7 --- /dev/null +++ b/tests/Stubs/DebugBacktraceStub.php @@ -0,0 +1,32 @@ + Date: Fri, 27 Aug 2021 13:29:14 +0100 Subject: [PATCH 2/6] Fix events in Lumen always being handled --- src/Internal/BacktraceProcessor.php | 40 ++++++--- tests/Middleware/UnhandledStateTest.php | 110 +++++++++++++++++++++++- 2 files changed, 132 insertions(+), 18 deletions(-) diff --git a/src/Internal/BacktraceProcessor.php b/src/Internal/BacktraceProcessor.php index 6d9152ac..6fc11bce 100644 --- a/src/Internal/BacktraceProcessor.php +++ b/src/Internal/BacktraceProcessor.php @@ -3,9 +3,11 @@ namespace Bugsnag\BugsnagLaravel\Internal; /** - * The criteria for an error to be unhandled in a Laravel app is as follows. + * The criteria for an error to be unhandled in a Laravel or Lumen app is as + * follows. * - * 1. All unhandled exceptions must pass through the `HANDLER_CLASS` report method + * 1. All unhandled exceptions must pass through one of the `HANDLER_CLASSES` + * report method * 2. Unhandled exceptions will have had a caller from inside a vendor namespace * or the App exception handler * 3. The above exception handler must have originally been called from @@ -36,25 +38,33 @@ final class BacktraceProcessor const STATE_DONE = 4; /** - * Laravel's built-in exception handler. + * Laravel and Lumen use different exception handlers which live in different + * namespaces. */ - const HANDLER_CLASS = \Illuminate\Foundation\Exceptions\Handler::class; + const LARAVEL_HANDLER_CLASS = \Illuminate\Foundation\Exceptions\Handler::class; + const LUMEN_HANDLER_CLASS = \Laravel\Lumen\Exceptions\Handler::class; /** - * The method used by the HANDLER_CLASS to report errors. + * The method used by the x_HANDLER_CLASS to report errors. */ const HANDLER_METHOD = 'report'; /** - * The default app exception handler in a Laravel app. + * Laravel uses the "Exception" namespace, Lumen uses the plural "Exceptions" + * for the default app exception handler. */ - const APP_EXCEPTION_HANDLER = \App\Exception\Handler::class; + const LARAVEL_APP_EXCEPTION_HANDLER = \App\Exception\Handler::class; + const LUMEN_APP_EXCEPTION_HANDLER = \App\Exceptions\Handler::class; /** - * Namespace used by Laravel so we can determine if code was called by the - * user's app or the framework itself. + * Namespaces used by Laravel and Lumen so we can determine if code was + * called by the user's app or the framework itself. + * + * Note this is not a mistake - Lumen uses the 'Laravel' namespace but + * Laravel itself does not */ - const VENDOR_NAMESPACE = 'Illuminate\\'; + const LARAVEL_VENDOR_NAMESPACE = 'Illuminate\\'; + const LUMEN_VENDOR_NAMESPACE = 'Laravel\\'; /** * The current state; one of the self::STATE_ constants. @@ -122,7 +132,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::HANDLER_CLASS + if (($class === self::LARAVEL_HANDLER_CLASS || $class === self::LUMEN_HANDLER_CLASS) && isset($frame['function']) && $frame['function'] === self::HANDLER_METHOD ) { @@ -134,7 +144,8 @@ 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::APP_EXCEPTION_HANDLER + if ($class === self::LARAVEL_APP_EXCEPTION_HANDLER + || $class === self::LUMEN_APP_EXCEPTION_HANDLER || $this->isVendor($class) ) { $this->state = self::STATE_IS_UNHANDLED; @@ -161,7 +172,7 @@ private function processFrame(array $frame) /** * Does the given class belong to a vendor namespace? * - * @see self::VENDOR_NAMESPACE + * @see self::VENDOR_NAMESPACES * * @param string $class * @@ -169,6 +180,7 @@ private function processFrame(array $frame) */ private function isVendor($class) { - return substr($class, 0, strlen(self::VENDOR_NAMESPACE)) === self::VENDOR_NAMESPACE; + 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; } } diff --git a/tests/Middleware/UnhandledStateTest.php b/tests/Middleware/UnhandledStateTest.php index c4f56246..79b045a6 100644 --- a/tests/Middleware/UnhandledStateTest.php +++ b/tests/Middleware/UnhandledStateTest.php @@ -65,7 +65,8 @@ protected function afterEach() } /** - * @dataProvider unhandledBacktraceProvider + * @dataProvider unhandledBacktraceProviderLaravel + * @dataProvider unhandledBacktraceProviderLumen */ public function testReportIsUnhandled(array $backtrace) { @@ -91,7 +92,7 @@ public function testReportIsUnhandled(array $backtrace) ); } - public function unhandledBacktraceProvider() + public function unhandledBacktraceProviderLaravel() { yield 'minimal backtrace' => [[ // the backtrace must go through the Handler::report method @@ -160,8 +161,71 @@ public function unhandledBacktraceProvider() ]]; } + public function unhandledBacktraceProviderLumen() + { + yield 'Lumen (App exception handler)' => [[ + ['class' => \Bugsnag\PsrLogger\AbstractLogger::class, 'function' => 'error'], + ['class' => \Laravel\Lumen\Exceptions\Handler::class, 'function' => 'report'], + ['class' => \App\Exceptions\Handler::class, 'function' => 'report'], + ['class' => \Laravel\Lumen\Application::class, 'function' => 'sendExceptionToHandler'], + ['class' => \Laravel\Lumen\Application::class, 'function' => 'dispatch'], + ['class' => \Laravel\Lumen\Application::class, 'function' => 'run'], + ]]; + + yield 'Lumen' => [[ + ['class' => \SomeClass::class], + ['class' => \Laravel\Lumen\Exceptions\Handler::class, 'function' => 'report'], + ['class' => \Some\OtherClass::class], + ['class' => \Laravel\Lumen\Application::class, 'function' => 'sendExceptionToHandler'], + ['class' => \Laravel\Lumen\Application::class, 'function' => 'dispatch'], + ['class' => \Laravel\Lumen\Application::class, 'function' => 'run'], + ]]; + + yield 'Lumen with other classes (App exception handler)' => [[ + ['class' => \SomeClass::class], + ['class' => \Laravel\Lumen\Exceptions\Handler::class, 'function' => 'report'], + ['class' => \Some\OtherClass::class], + ['class' => \App\Exceptions\Handler::class, 'function' => 'report'], + ['class' => \Laravel\Lumen\Application::class, 'function' => 'sendExceptionToHandler'], + ['class' => \Laravel\Lumen\Application::class, 'function' => 'dispatch'], + ['class' => \Laravel\Lumen\Application::class, 'function' => 'run'], + ]]; + + yield 'Lumen with other classes' => [[ + ['class' => \Bugsnag\PsrLogger\AbstractLogger::class, 'function' => 'error'], + ['class' => \SomeClass::class], + ['class' => \Laravel\Lumen\Exceptions\Handler::class, 'function' => 'report'], + ['class' => \Some\OtherClass::class], + ['class' => \Laravel\Lumen\Application::class, 'function' => 'sendExceptionToHandler'], + ['class' => \Laravel\Lumen\Application::class, 'function' => 'dispatch'], + ['class' => \Laravel\Lumen\Application::class, 'function' => 'run'], + ]]; + + yield 'Lumen with non-class frames (App exception handler)' => [[ + ['class' => \Bugsnag\PsrLogger\AbstractLogger::class, 'function' => 'error'], + ['function' => 'abc'], + ['class' => \Laravel\Lumen\Exceptions\Handler::class, 'function' => 'report'], + ['function' => 'xyz'], + ['class' => \App\Exceptions\Handler::class, 'function' => 'report'], + ['class' => \Laravel\Lumen\Application::class, 'function' => 'sendExceptionToHandler'], + ['class' => \Laravel\Lumen\Application::class, 'function' => 'dispatch'], + ['class' => \Laravel\Lumen\Application::class, 'function' => 'run'], + ]]; + + yield 'Lumen with non-class frames' => [[ + ['class' => \Bugsnag\PsrLogger\AbstractLogger::class, 'function' => 'error'], + ['function' => 'abc'], + ['class' => \Laravel\Lumen\Exceptions\Handler::class, 'function' => 'report'], + ['function' => 'xyz'], + ['class' => \Laravel\Lumen\Application::class, 'function' => 'sendExceptionToHandler'], + ['class' => \Laravel\Lumen\Application::class, 'function' => 'dispatch'], + ['class' => \Laravel\Lumen\Application::class, 'function' => 'run'], + ]]; + } + /** - * @dataProvider handledBacktraceProvider + * @dataProvider handledBacktraceProviderLaravel + * @dataProvider handledBacktraceProviderLumen */ public function testReportIsHandled(array $backtrace) { @@ -177,7 +241,7 @@ public function testReportIsHandled(array $backtrace) $this->assertSame(['type' => 'handledException'], $this->report->getSeverityReason()); } - public function handledBacktraceProvider() + public function handledBacktraceProviderLaravel() { yield 'empty backtrace' => [[[]]]; @@ -241,4 +305,42 @@ public function handledBacktraceProvider() ['class' => \Yet\AnotherClass::class], ]]; } + + public function handledBacktraceProviderLumen() + { + yield 'Lumen no exception handler' => [[ + ['class' => \Bugsnag\PsrLogger\AbstractLogger::class, 'function' => 'error'], + ['class' => \Not\Laravel\Lumen\Exceptions\Handler::class, 'function' => 'report'], + ['class' => \App\Exceptions\Handler::class, 'function' => 'report'], + ['class' => \Laravel\Lumen\Application::class, 'function' => 'sendExceptionToHandler'], + ['class' => \Laravel\Lumen\Application::class, 'function' => 'dispatch'], + ['class' => \Laravel\Lumen\Application::class, 'function' => 'run'], + ]]; + + yield 'Lumen exception handler but wrong method' => [[ + ['class' => \Bugsnag\PsrLogger\AbstractLogger::class, 'function' => 'error'], + ['class' => \Laravel\Lumen\Exceptions\Handler::class, 'function' => 'notReport'], + ['class' => \App\Exceptions\Handler::class, 'function' => 'report'], + ['class' => \Laravel\Lumen\Application::class, 'function' => 'sendExceptionToHandler'], + ['class' => \Laravel\Lumen\Application::class, 'function' => 'dispatch'], + ['class' => \Laravel\Lumen\Application::class, 'function' => 'run'], + ]]; + + yield 'Lumen non-consecutive frames after App exception handler' => [[ + ['class' => \Bugsnag\PsrLogger\AbstractLogger::class, 'function' => 'error'], + ['class' => \Laravel\Lumen\Exceptions\Handler::class, 'function' => 'report'], + ['class' => \App\Exceptions\Handler::class, 'function' => 'report'], + ['class' => \Not\Laravel\Lumen\Application::class, 'function' => 'sendExceptionToHandler'], + ['class' => \Laravel\Lumen\Application::class, 'function' => 'dispatch'], + ['class' => \Laravel\Lumen\Application::class, 'function' => 'run'], + ]]; + + yield 'Lumen non-consecutive frames after Lumen exception handler' => [[ + ['class' => \Bugsnag\PsrLogger\AbstractLogger::class, 'function' => 'error'], + ['class' => \Laravel\Lumen\Exceptions\Handler::class, 'function' => 'report'], + ['class' => \Laravel\Lumen\Application::class, 'function' => 'sendExceptionToHandler'], + ['class' => \Not\Laravel\Lumen\Application::class, 'function' => 'dispatch'], + ['class' => \Laravel\Lumen\Application::class, 'function' => 'run'], + ]]; + } } From bc35ab257f2b39c8d5ba8b43258418ea89b11c38 Mon Sep 17 00:00:00 2001 From: Joe Haines Date: Fri, 27 Aug 2021 11:07:00 +0100 Subject: [PATCH 3/6] Refactor UnhandledState middleware --- src/Internal/BacktraceProcessor.php | 174 ++++++++++++++++++++++++ src/Middleware/UnhandledState.php | 68 ++------- tests/Middleware/UnhandledStateTest.php | 8 +- 3 files changed, 188 insertions(+), 62 deletions(-) create mode 100644 src/Internal/BacktraceProcessor.php diff --git a/src/Internal/BacktraceProcessor.php b/src/Internal/BacktraceProcessor.php new file mode 100644 index 00000000..6d9152ac --- /dev/null +++ b/src/Internal/BacktraceProcessor.php @@ -0,0 +1,174 @@ +backtrace = $backtrace; + } + + /** + * Determine if the backtrace was from an unhandled error. + * + * @return bool + */ + public function isUnhandled() + { + foreach ($this->backtrace as $frame) { + $this->processFrame($frame); + + // stop iterating early if we know we're done + if ($this->state === self::STATE_DONE) { + break; + } + } + + return $this->unhandled; + } + + /** + * @param array $frame + * + * @return void + */ + private function processFrame(array $frame) + { + if (!isset($frame['class'])) { + return; + } + + $class = $frame['class']; + + switch ($this->state) { + case self::STATE_FRAMEWORK_HANDLER: + // 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::HANDLER_CLASS + && isset($frame['function']) + && $frame['function'] === self::HANDLER_METHOD + ) { + $this->state = self::STATE_HANDLER_CALLER; + } + + break; + + 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::APP_EXCEPTION_HANDLER + || $this->isVendor($class) + ) { + $this->state = self::STATE_IS_UNHANDLED; + } + + break; + + case self::STATE_IS_UNHANDLED: + // we are only interested in running this once so move immediately + // into the "done" state. This ensures we only check the frame + // immediately before the caller of the exception handler + $this->state = self::STATE_DONE; + + // if this class is internal to the framework then the exception + // was unhandled + if ($this->isVendor($class)) { + $this->unhandled = true; + } + + break; + } + } + + /** + * Does the given class belong to a vendor namespace? + * + * @see self::VENDOR_NAMESPACE + * + * @param string $class + * + * @return bool + */ + private function isVendor($class) + { + return substr($class, 0, strlen(self::VENDOR_NAMESPACE)) === self::VENDOR_NAMESPACE; + } +} diff --git a/src/Middleware/UnhandledState.php b/src/Middleware/UnhandledState.php index 40cf7c0c..d237f2e7 100644 --- a/src/Middleware/UnhandledState.php +++ b/src/Middleware/UnhandledState.php @@ -2,30 +2,11 @@ namespace Bugsnag\BugsnagLaravel\Middleware; +use Bugsnag\BugsnagLaravel\Internal\BacktraceProcessor; use Bugsnag\Report; class UnhandledState { - /** - * Unhandled state middleware implementation details. - * - * This middleware functions on the basis of three things: - * 1. All unhandled exceptions must pass through the `HANDLER_CLASS` report - * method - * 2. Unhandled exceptions will have had a caller from inside the Illuminate - * namespace or App exception handler - * 3. The above exception handler must have originally been called from - * within the Illuminate namespace - * - * This middleware calls the inbuilt PHP backtrace, and traverses each frame - * to determine if the above conditions are met. If they are, the report is - * marked as unhandled. - */ - const HANDLER_CLASS = 'Illuminate\\Foundation\\Exceptions\\Handler'; - const HANDLER_METHOD = 'report'; - const ILLUMINATE_NAMESPACE = 'Illuminate'; - const APP_EXCEPTION_HANDLER = 'App\\Exception\\Handler'; - /** * Execute the unhandled state middleware. * @@ -37,50 +18,21 @@ class UnhandledState public function __invoke(Report $report, callable $next) { $stackFrames = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); - $reportFrame = false; - $callerFrame = false; - $unhandled = false; - if (is_null($stackFrames)) { - return; - } - foreach ($stackFrames as $frame) { - if (!array_key_exists('class', $frame)) { - continue; - } - $class = $frame['class']; - if (!$reportFrame) { - if (!array_key_exists('function', $frame)) { - continue; - } elseif (($class === $this::HANDLER_CLASS) && ($frame['function'] === $this::HANDLER_METHOD)) { - $reportFrame = true; - } - } elseif (!$callerFrame) { - $startsWithIlluminate = substr($class, 0, strlen($this::ILLUMINATE_NAMESPACE)) === $this::ILLUMINATE_NAMESPACE; - if ($startsWithIlluminate || ($class == $this::APP_EXCEPTION_HANDLER)) { - $callerFrame = true; - } - } elseif (!$unhandled) { - $startsWithIlluminate = substr($class, 0, strlen($this::ILLUMINATE_NAMESPACE)) === $this::ILLUMINATE_NAMESPACE; - if ($startsWithIlluminate) { - $unhandled = true; - } - break; - } + + if (!is_array($stackFrames)) { + $stackFrames = []; } - if ($unhandled) { + + $backtraceProcessor = new BacktraceProcessor($stackFrames); + + if ($backtraceProcessor->isUnhandled()) { $report->setUnhandled(true); $report->setSeverityReason([ 'type' => 'unhandledExceptionMiddleware', - 'attributes' => [ - 'framework' => 'Laravel', - ], + 'attributes' => ['framework' => 'Laravel'], ]); } - $next($report); - } - protected function stringStartsWith($haystack, $needle) - { - return substr($haystack, 0, strlen($needle)) === $needle; + $next($report); } } diff --git a/tests/Middleware/UnhandledStateTest.php b/tests/Middleware/UnhandledStateTest.php index a2a03d5e..c4f56246 100644 --- a/tests/Middleware/UnhandledStateTest.php +++ b/tests/Middleware/UnhandledStateTest.php @@ -74,8 +74,8 @@ public function testReportIsUnhandled(array $backtrace) $this->assertFalse($this->report->getUnhandled()); $this->assertSame(['type' => 'handledException'], $this->report->getSeverityReason()); - $a = new UnhandledState(); - $a->__invoke($this->report, $this->next); + $unhandledState = new UnhandledState(); + $unhandledState->__invoke($this->report, $this->next); $this->assertTrue( $this->report->getUnhandled(), @@ -170,8 +170,8 @@ public function testReportIsHandled(array $backtrace) $this->assertFalse($this->report->getUnhandled()); $this->assertSame(['type' => 'handledException'], $this->report->getSeverityReason()); - $a = new UnhandledState(); - $a->__invoke($this->report, $this->next); + $unhandledState = new UnhandledState(); + $unhandledState->__invoke($this->report, $this->next); $this->assertFalse($this->report->getUnhandled()); $this->assertSame(['type' => 'handledException'], $this->report->getSeverityReason()); From b3e003a88f678347473e266407b6f4a61816a446 Mon Sep 17 00:00:00 2001 From: Joe Haines Date: Thu, 2 Sep 2021 10:31:30 +0100 Subject: [PATCH 4/6] Update changelog --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 51cba1fc..0e3f1e4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,13 @@ Changelog ========= +## TBD + +### Bug Fixes + +* Fix events in Lumen always being handled + [#452](https://github.com/bugsnag/bugsnag-laravel/pull/452) + ## 2.22.1 (2021-04-29) ### Bug Fixes From c889135c2e3c8e1771d9f21a4fca232992f1f8d0 Mon Sep 17 00:00:00 2001 From: Joe Haines Date: Fri, 3 Sep 2021 16:39:19 +0100 Subject: [PATCH 5/6] Update changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e3f1e4b..c51edb9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ Changelog ========= -## TBD +## 2.22.2 (2021-09-06) ### Bug Fixes From a50b18867b6d4a25d2c51be6d2927c7ea356a5d2 Mon Sep 17 00:00:00 2001 From: Joe Haines Date: Fri, 3 Sep 2021 16:39:35 +0100 Subject: [PATCH 6/6] Bump version --- src/BugsnagServiceProvider.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/BugsnagServiceProvider.php b/src/BugsnagServiceProvider.php index cd9102d7..dfd1ea60 100644 --- a/src/BugsnagServiceProvider.php +++ b/src/BugsnagServiceProvider.php @@ -36,7 +36,7 @@ class BugsnagServiceProvider extends ServiceProvider * * @var string */ - const VERSION = '2.22.1'; + const VERSION = '2.22.2'; /** * Boot the service provider.