Repository: getsentry/sentry-laravel (php)

Issue:
Adding support for Laravel Octane?
It looks like if we're running the web server using a long running process mechanism like **Laravel's Octane** or other packages like `spiral/roadrunner-laravel` or `php-pm/php-pm` or `swooletw/laravel-swoole` the events do not get flushed unless the workers are restarted and also cause issues with scoping each individual requests since the cleaning up and flushing of events aren't being called after each request like how it's handled for job queues.

As Laravel now has an official package (currently in public beta), it would be great if Sentry adds this mechanism for HTTP requests as Octane has the potential to go big.

Pull request:
Add Laravel Octane support, fixes #479
This adds support for Octane Listeners so that Sentry is reset (scopes) correctly between request, ticks and tasks as part of Laravel's Octane (https://laravel.com/docs/8.x/octane).

I followed how Queues were detected and reset and tested afterwards.

Additionally this will fix #479 (and confirmed that #475 fixes pushing of events in long running processes like Octane!) 

Some things to note:

- I'm not sure if breadcrumbs should be added for RequestReceived since it probably should act like a route (I mean it is..) additionally the route is properly labeled here like in non Octane land. For consistency sake I added breadcrumbs but they are rather useless
![image](https://user-images.githubusercontent.com/564256/121097653-2e974c80-c7a9-11eb-9a96-e6d57af6f18e.png)
- Breadcrumbs on Tasks (https://laravel.com/docs/8.x/octane#concurrent-tasks) and Ticks (https://laravel.com/docs/8.x/octane#ticks-and-intervals) are also kind of useless because on Ticks there's nothing to really breadcrumb (like there is on queues) additionally on tasks you can breadcrumb $data but $data is mixed which makes it harder to deal with, unless you generically transform it, probably not worth the hassle (if end user wants to do this then they can on their end)
- Unsure what to do with RequestHandled (https://github.com/laravel/octane/blob/722b7567e2817b3f5e4a4ae7b49290f53968a114/src/ApplicationGateway.php#L37). It appears this is only hit if there is no route defined for said request. 
- I did not handle anything in regards to WorkerStarting (same as the already existing Queues code)

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a49bc57..e711556 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,7 @@
 
 ## Unreleased
 
+- Add support for Laravel Octane (#495)
 - Fix bug in Sentry log channel handler checking an undefined variable resulting in an error (#515)
 - Add `action_level` configuration option for Sentry log channel which configures a Monolog `FingersCrossedHandler` (#516)
 
diff --git a/src/Sentry/Laravel/EventHandler.php b/src/Sentry/Laravel/EventHandler.php
index c0914c6..eb8a030 100644
--- a/src/Sentry/Laravel/EventHandler.php
+++ b/src/Sentry/Laravel/EventHandler.php
@@ -17,6 +17,7 @@ use Illuminate\Queue\Events\JobProcessed;
 use Illuminate\Queue\Events\JobProcessing;
 use Illuminate\Queue\Events\WorkerStopping;
 use Illuminate\Queue\QueueManager;
+use Laravel\Octane\Events as Octane;
 use Illuminate\Routing\Events\RouteMatched;
 use Illuminate\Routing\Route;
 use RuntimeException;
@@ -32,13 +33,13 @@ class EventHandler
      * @var array
      */
     protected static $eventHandlerMap = [
-        'router.matched' => 'routerMatched',                         // Until Laravel 5.1
+        'router.matched'                         => 'routerMatched',                         // Until Laravel 5.1
         'Illuminate\Routing\Events\RouteMatched' => 'routeMatched',  // Since Laravel 5.2
 
-        'illuminate.query' => 'query',                                 // Until Laravel 5.1
+        'illuminate.query'                         => 'query',                                 // Until Laravel 5.1
         'Illuminate\Database\Events\QueryExecuted' => 'queryExecuted', // Since Laravel 5.2
 
-        'illuminate.log' => 'log',                                // Until Laravel 5.3
+        'illuminate.log'                      => 'log',                                // Until Laravel 5.3
         'Illuminate\Log\Events\MessageLogged' => 'messageLogged', // Since Laravel 5.4
 
         'Illuminate\Console\Events\CommandStarting' => 'commandStarting', // Since Laravel 5.5
@@ -60,10 +61,29 @@ class EventHandler
      * @var array
      */
     protected static $queueEventHandlerMap = [
-        'Illuminate\Queue\Events\JobProcessing' => 'queueJobProcessing', // Since Laravel 5.2
-        'Illuminate\Queue\Events\JobProcessed' => 'queueJobProcessed', // Since Laravel 5.2
+        'Illuminate\Queue\Events\JobProcessing'        => 'queueJobProcessing',        // Since Laravel 5.2
+        'Illuminate\Queue\Events\JobProcessed'         => 'queueJobProcessed',         // Since Laravel 5.2
         'Illuminate\Queue\Events\JobExceptionOccurred' => 'queueJobExceptionOccurred', // Since Laravel 5.2
-        'Illuminate\Queue\Events\WorkerStopping' => 'queueWorkerStopping', // Since Laravel 5.2
+        'Illuminate\Queue\Events\WorkerStopping'       => 'queueWorkerStopping',       // Since Laravel 5.2
+    ];
+
+    /**
+     * Map Octane event handlers to events.
+     *
+     * @var array
+     */
+    protected static $octaneEventHandlerMap = [
+        'Laravel\Octane\Events\RequestReceived'   => 'octaneRequestReceived',
+        'Laravel\Octane\Events\RequestTerminated' => 'octaneRequestTerminated',
+
+        'Laravel\Octane\Events\TaskReceived'   => 'octaneTaskReceived',
+        'Laravel\Octane\Events\TaskTerminated' => 'octaneTaskTerminated',
+
+        'Laravel\Octane\Events\TickReceived'   => 'octaneTickReceived',
+        'Laravel\Octane\Events\TickTerminated' => 'octaneTickTerminated',
+
+        'Laravel\Octane\Events\WorkerErrorOccurred' => 'octaneWorkerErrorOccurred',
+        'Laravel\Octane\Events\WorkerStopping'      => 'octaneWorkerStopping',
     ];
 
     /**
@@ -108,6 +128,20 @@ class EventHandler
      */
     private $recordCommandInfo;
 
+    /**
+     * Indicates if we should we add tick info to the breadcrumbs.
+     *
+     * @var bool
+     */
+    private $recordOctaneTickInfo;
+
+    /**
+     * Indicates if we should we add task info to the breadcrumbs.
+     *
+     * @var bool
+     */
+    private $recordOctaneTaskInfo;
+
     /**
      * Indicates if we pushed a scope for the queue.
      *
@@ -115,6 +149,13 @@ class EventHandler
      */
     private $pushedQueueScope = false;
 
+    /**
+     * Indicates if we pushed a scope for Octane.
+     *
+     * @var bool
+     */
+    private $pushedOctaneScope = false;
+
     /**
      * EventHandler constructor.
      *
@@ -125,11 +166,13 @@ class EventHandler
     {
         $this->container = $container;
 
-        $this->recordSqlQueries = ($config['breadcrumbs.sql_queries'] ?? $config['breadcrumbs']['sql_queries'] ?? true) === true;
-        $this->recordSqlBindings = ($config['breadcrumbs.sql_bindings'] ?? $config['breadcrumbs']['sql_bindings'] ?? false) === true;
-        $this->recordLaravelLogs = ($config['breadcrumbs.logs'] ?? $config['breadcrumbs']['logs'] ?? true) === true;
-        $this->recordQueueInfo = ($config['breadcrumbs.queue_info'] ?? $config['breadcrumbs']['queue_info'] ?? true) === true;
-        $this->recordCommandInfo = ($config['breadcrumbs.command_info'] ?? $config['breadcrumbs']['command_info'] ?? true) === true;
+        $this->recordSqlQueries     = ($config['breadcrumbs.sql_queries'] ?? $config['breadcrumbs']['sql_queries'] ?? true) === true;
+        $this->recordSqlBindings    = ($config['breadcrumbs.sql_bindings'] ?? $config['breadcrumbs']['sql_bindings'] ?? false) === true;
+        $this->recordLaravelLogs    = ($config['breadcrumbs.logs'] ?? $config['breadcrumbs']['logs'] ?? true) === true;
+        $this->recordQueueInfo      = ($config['breadcrumbs.queue_info'] ?? $config['breadcrumbs']['queue_info'] ?? true) === true;
+        $this->recordCommandInfo    = ($config['breadcrumbs.command_info'] ?? $config['breadcrumbs']['command_info'] ?? true) === true;
+        $this->recordOctaneTickInfo = ($config['breadcrumbs.octane_tick_info'] ?? $config['breadcrumbs']['octane_tick_info'] ?? true) === true;
+        $this->recordOctaneTaskInfo = ($config['breadcrumbs.octane_task_info'] ?? $config['breadcrumbs']['octane_task_info'] ?? true) === true;
     }
 
     /**
@@ -166,6 +209,23 @@ class EventHandler
         }
     }
 
+    /**
+     * Attach all queue event handlers.
+     */
+    public function subscribeOctaneEvents(): void
+    {
+        /** @var \Illuminate\Contracts\Events\Dispatcher $dispatcher */
+        try {
+            $dispatcher = $this->container->make(Dispatcher::class);
+
+            foreach (static::$octaneEventHandlerMap as $eventName => $handler) {
+                $dispatcher->listen($eventName, [$this, $handler]);
+            }
+        } catch (BindingResolutionException $e) {
+            // If we cannot resolve the event dispatcher we also cannot listen to events
+        }
+    }
+
     /**
      * Attach all queue event handlers.
      *
@@ -174,8 +234,9 @@ class EventHandler
     public function subscribeQueueEvents(QueueManager $queue): void
     {
         $queue->looping(function () {
-            $this->cleanupScopeForQueuedJob();
-            $this->afterQueuedJob();
+            $this->cleanupScopeForTaskWithinLongRunningProcessWhen($this->pushedQueueScope);
+
+            $this->pushedQueueScope = false;
         });
 
         /** @var \Illuminate\Contracts\Events\Dispatcher $dispatcher */
@@ -351,33 +412,6 @@ class EventHandler
         ));
     }
 
-    /**
-     * Translates common log levels to Sentry breadcrumb levels.
-     *
-     * @param string $level Log level. Maybe any standard.
-     *
-     * @return string Breadcrumb level.
-     */
-    protected function logLevelToBreadcrumbLevel(string $level): string
-    {
-        switch (strtolower($level)) {
-            case 'debug':
-                return Breadcrumb::LEVEL_DEBUG;
-            case 'warning':
-                return Breadcrumb::LEVEL_WARNING;
-            case 'error':
-                return Breadcrumb::LEVEL_ERROR;
-            case 'critical':
-            case 'alert':
-            case 'emergency':
-                return Breadcrumb::LEVEL_FATAL;
-            case 'info':
-            case 'notice':
-            default:
-                return Breadcrumb::LEVEL_INFO;
-        }
-    }
-
     /**
      * Since Laravel 5.3
      *
@@ -416,16 +450,20 @@ class EventHandler
      */
     protected function queueJobProcessingHandler(JobProcessing $event)
     {
-        $this->prepareScopeForQueuedJob();
+        $this->cleanupScopeForTaskWithinLongRunningProcessWhen($this->pushedQueueScope);
+
+        $this->prepareScopeForTaskWithinLongRunningProcess();
+
+        $this->pushedQueueScope = true;
 
         if (!$this->recordQueueInfo) {
             return;
         }
 
         $job = [
-            'job' => $event->job->getName(),
-            'queue' => $event->job->getQueue(),
-            'attempts' => $event->job->attempts(),
+            'job'        => $event->job->getName(),
+            'queue'      => $event->job->getQueue(),
+            'attempts'   => $event->job->attempts(),
             'connection' => $event->connectionName,
         ];
 
@@ -450,7 +488,7 @@ class EventHandler
      */
     protected function queueJobExceptionOccurredHandler(JobExceptionOccurred $event)
     {
-        $this->afterQueuedJob();
+        $this->afterTaskWithinLongRunningProcess();
     }
 
     /**
@@ -460,7 +498,7 @@ class EventHandler
      */
     protected function queueJobProcessedHandler(JobProcessed $event)
     {
-        $this->afterQueuedJob();
+        $this->afterTaskWithinLongRunningProcess();
     }
 
     /**
@@ -531,34 +569,149 @@ class EventHandler
         Integration::flushEvents();
     }
 
-    private function afterQueuedJob(): void
+    protected function octaneRequestReceivedHandler(Octane\RequestReceived $event): void
+    {
+        $this->prepareScopeForOctane();
+    }
+
+    protected function octaneRequestTerminatedHandler(Octane\RequestTerminated $event): void
+    {
+        $this->cleanupScopeForOctane();
+    }
+
+    protected function octaneTaskReceivedHandler(Octane\TaskReceived $event): void
+    {
+        $this->prepareScopeForOctane();
+
+        if (!$this->recordOctaneTaskInfo) {
+            return;
+        }
+
+        Integration::addBreadcrumb(new Breadcrumb(
+            Breadcrumb::LEVEL_INFO,
+            Breadcrumb::TYPE_DEFAULT,
+            'octane.task',
+            'Processing Octane task'
+        ));
+    }
+
+    protected function octaneTaskTerminatedHandler(Octane\TaskTerminated $event): void
+    {
+        $this->cleanupScopeForOctane();
+    }
+
+    protected function octaneTickReceivedHandler(Octane\TickReceived $event): void
+    {
+        $this->prepareScopeForOctane();
+
+        if (!$this->recordOctaneTickInfo) {
+            return;
+        }
+
+        Integration::addBreadcrumb(new Breadcrumb(
+            Breadcrumb::LEVEL_INFO,
+            Breadcrumb::TYPE_DEFAULT,
+            'octane.tick',
+            'Processing Octane tick'
+        ));
+    }
+
+    protected function octaneTickTerminatedHandler(Octane\TickTerminated $event): void
+    {
+        $this->cleanupScopeForOctane();
+    }
+
+    protected function octaneWorkerErrorOccurredHandler(Octane\WorkerErrorOccurred $event): void
+    {
+        $this->afterTaskWithinLongRunningProcess();
+    }
+
+    protected function octaneWorkerStoppingHandler(Octane\WorkerStopping $event): void
+    {
+        $this->afterTaskWithinLongRunningProcess();
+    }
+
+    private function prepareScopeForOctane(): void
+    {
+        $this->cleanupScopeForOctane();
+
+        $this->prepareScopeForTaskWithinLongRunningProcess();
+
+        $this->pushedOctaneScope = true;
+    }
+
+    private function cleanupScopeForOctane(): void
+    {
+        $this->cleanupScopeForTaskWithinLongRunningProcessWhen($this->pushedOctaneScope);
+
+        $this->pushedOctaneScope = false;
+    }
+
+    /**
+     * Translates common log levels to Sentry breadcrumb levels.
+     *
+     * @param string $level Log level. Maybe any standard.
+     *
+     * @return string Breadcrumb level.
+     */
+    private function logLevelToBreadcrumbLevel(string $level): string
+    {
+        switch (strtolower($level)) {
+            case 'debug':
+                return Breadcrumb::LEVEL_DEBUG;
+            case 'warning':
+                return Breadcrumb::LEVEL_WARNING;
+            case 'error':
+                return Breadcrumb::LEVEL_ERROR;
+            case 'critical':
+            case 'alert':
+            case 'emergency':
+                return Breadcrumb::LEVEL_FATAL;
+            case 'info':
+            case 'notice':
+            default:
+                return Breadcrumb::LEVEL_INFO;
+        }
+    }
+
+    /**
+     * Should be called after a task within a long running process has ended so events can be flushed.
+     */
+    private function afterTaskWithinLongRunningProcess(): void
     {
         // Flush any and all events that were possibly generated by queue jobs
         Integration::flushEvents();
     }
 
-    private function prepareScopeForQueuedJob(): void
+    /**
+     * Should be called before starting a task within a long running process, this is done to prevent
+     * the task to have effect on the scope for the next task to run within the long running process.
+     */
+    private function prepareScopeForTaskWithinLongRunningProcess(): void
     {
-        $this->cleanupScopeForQueuedJob();
-
         SentrySdk::getCurrentHub()->pushScope();
 
-        $this->pushedQueueScope = true;
-
         // When a job starts, we want to make sure the scope is cleared of breadcrumbs
         SentrySdk::getCurrentHub()->configureScope(static function (Scope $scope) {
             $scope->clearBreadcrumbs();
         });
     }
 
-    private function cleanupScopeForQueuedJob(): void
+    /**
+     * Cleanup a previously prepared scope.
+     *
+     * @param bool $when Only cleanup the scope when this is true.
+     *
+     * @see prepareScopeForTaskWithinLongRunningProcess
+     */
+    private function cleanupScopeForTaskWithinLongRunningProcessWhen(bool $when): void
     {
-        if (!$this->pushedQueueScope) {
+        if (!$when) {
             return;
         }
 
-        SentrySdk::getCurrentHub()->popScope();
+        $this->afterTaskWithinLongRunningProcess();
 
-        $this->pushedQueueScope = false;
+        SentrySdk::getCurrentHub()->popScope();
     }
 }
diff --git a/src/Sentry/Laravel/Http/LaravelRequestFetcher.php b/src/Sentry/Laravel/Http/LaravelRequestFetcher.php
index 77ec374..8a652c5 100644
--- a/src/Sentry/Laravel/Http/LaravelRequestFetcher.php
+++ b/src/Sentry/Laravel/Http/LaravelRequestFetcher.php
@@ -2,7 +2,7 @@
 
 namespace Sentry\Laravel\Http;
 
-use Illuminate\Contracts\Container\Container;
+use Illuminate\Container\Container;
 use Psr\Http\Message\ServerRequestInterface;
 use Sentry\Integration\RequestFetcher;
 use Sentry\Integration\RequestFetcherInterface;
@@ -14,26 +14,16 @@ class LaravelRequestFetcher implements RequestFetcherInterface
      */
     public const CONTAINER_PSR7_INSTANCE_KEY = 'sentry-laravel.psr7.request';
 
-    /**
-     * The Laravel container.
-     *
-     * @var \Illuminate\Contracts\Container\Container
-     */
-    private $container;
-
-    public function __construct(Container $container)
-    {
-        $this->container = $container;
-    }
-
     public function fetchRequest(): ?ServerRequestInterface
     {
-        if (\PHP_SAPI === 'cli' || \PHP_SAPI === 'phpdbg' || !$this->container->bound('request')) {
+        $container = Container::getInstance();
+
+        if (!$container->bound('request')) {
             return null;
         }
 
-        if ($this->container->bound(self::CONTAINER_PSR7_INSTANCE_KEY)) {
-            return $this->container->make(self::CONTAINER_PSR7_INSTANCE_KEY);
+        if ($container->bound(self::CONTAINER_PSR7_INSTANCE_KEY)) {
+            return $container->make(self::CONTAINER_PSR7_INSTANCE_KEY);
         }
 
         return (new RequestFetcher)->fetchRequest();
diff --git a/src/Sentry/Laravel/Http/SetRequestIpMiddleware.php b/src/Sentry/Laravel/Http/SetRequestIpMiddleware.php
index 9a67fef..ca708d8 100644
--- a/src/Sentry/Laravel/Http/SetRequestIpMiddleware.php
+++ b/src/Sentry/Laravel/Http/SetRequestIpMiddleware.php
@@ -3,7 +3,7 @@
 namespace Sentry\Laravel\Http;
 
 use Closure;
-use Illuminate\Contracts\Container\Container;
+use Illuminate\Container\Container;
 use Illuminate\Http\Request;
 use Sentry\State\HubInterface;
 use Sentry\State\Scope;
@@ -15,18 +15,6 @@ use Sentry\State\Scope;
  */
 class SetRequestIpMiddleware
 {
-    /**
-     * The Laravel container.
-     *
-     * @var \Illuminate\Contracts\Container\Container
-     */
-    private $container;
-
-    public function __construct(Container $container)
-    {
-        $this->container = $container;
-    }
-
     /**
      * Handle an incoming request.
      *
@@ -37,9 +25,11 @@ class SetRequestIpMiddleware
      */
     public function handle(Request $request, Closure $next)
     {
-        if ($this->container->bound(HubInterface::class)) {
+        $container = Container::getInstance();
+
+        if ($container->bound(HubInterface::class)) {
             /** @var \Sentry\State\HubInterface $sentry */
-            $sentry = $this->container->make(HubInterface::class);
+            $sentry = $container->make(HubInterface::class);
 
             $client = $sentry->getClient();
 
diff --git a/src/Sentry/Laravel/Http/SetRequestMiddleware.php b/src/Sentry/Laravel/Http/SetRequestMiddleware.php
index d7b9868..8781f0b 100644
--- a/src/Sentry/Laravel/Http/SetRequestMiddleware.php
+++ b/src/Sentry/Laravel/Http/SetRequestMiddleware.php
@@ -3,7 +3,7 @@
 namespace Sentry\Laravel\Http;
 
 use Closure;
-use Illuminate\Contracts\Container\Container;
+use Illuminate\Container\Container;
 use Illuminate\Http\Request;
 use Psr\Http\Message\ServerRequestInterface;
 use Sentry\State\HubInterface;
@@ -15,18 +15,6 @@ use Throwable;
  */
 class SetRequestMiddleware
 {
-    /**
-     * The Laravel container.
-     *
-     * @var \Illuminate\Contracts\Container\Container
-     */
-    private $container;
-
-    public function __construct(Container $container)
-    {
-        $this->container = $container;
-    }
-
     /**
      * Handle an incoming request.
      *
@@ -37,11 +25,13 @@ class SetRequestMiddleware
      */
     public function handle(Request $request, Closure $next)
     {
-        if ($this->container->bound(HubInterface::class)) {
+        $container = Container::getInstance();
+
+        if ($container->bound(HubInterface::class)) {
             try {
-                $this->container->instance(
+                $container->instance(
                     LaravelRequestFetcher::CONTAINER_PSR7_INSTANCE_KEY,
-                    $this->container->make(ServerRequestInterface::class)
+                    $container->make(ServerRequestInterface::class)
                 );
             } catch (Throwable $e) {
                 // Ignore problems getting the PSR-7 server request instance here
diff --git a/src/Sentry/Laravel/ServiceProvider.php b/src/Sentry/Laravel/ServiceProvider.php
index 58b2926..bf18a97 100644
--- a/src/Sentry/Laravel/ServiceProvider.php
+++ b/src/Sentry/Laravel/ServiceProvider.php
@@ -2,6 +2,7 @@
 
 namespace Sentry\Laravel;
 
+use Illuminate\Container\Container;
 use Illuminate\Contracts\Http\Kernel as HttpKernelInterface;
 use Illuminate\Foundation\Application as Laravel;
 use Illuminate\Foundation\Http\Kernel as HttpKernel;
@@ -102,6 +103,10 @@ class ServiceProvider extends BaseServiceProvider
 
         $handler->subscribe();
 
+        if ($this->app->bound('octane')) {
+            $handler->subscribeOctaneEvents();
+        }
+
         if ($this->app->bound('queue')) {
             $handler->subscribeQueueEvents($this->app->queue);
         }
@@ -163,7 +168,7 @@ class ServiceProvider extends BaseServiceProvider
             return $clientBuilder;
         });
 
-        $this->app->singleton(HubInterface::class, function () {
+        $this->app->singleton(HubInterface::class, function ($app) {
             /** @var \Sentry\ClientBuilderInterface $clientBuilder */
             $clientBuilder = $this->app->make(ClientBuilderInterface::class);
 
@@ -171,7 +176,7 @@ class ServiceProvider extends BaseServiceProvider
 
             $userIntegrations = $this->resolveIntegrationsFromUserConfig();
 
-            $options->setIntegrations(function (array $integrations) use ($options, $userIntegrations) {
+            $options->setIntegrations(function (array $integrations) use ($options, $userIntegrations, $app) {
                 if ($options->hasDefaultIntegrations()) {
                     // Remove the default error and fatal exception listeners to let Laravel handle those
                     // itself. These event are still bubbling up through the documented changes in the users
@@ -200,7 +205,7 @@ class ServiceProvider extends BaseServiceProvider
                     });
 
                     $integrations[] = new SdkIntegration\RequestIntegration(
-                        new LaravelRequestFetcher($this->app)
+                        new LaravelRequestFetcher($app)
                     );
                 }
 


Test patch:
diff --git a/test/Sentry/EventHandlerTest.php b/test/Sentry/EventHandlerTest.php
index aa87ed1..4d1472c 100644
--- a/test/Sentry/EventHandlerTest.php
+++ b/test/Sentry/EventHandlerTest.php
@@ -41,6 +41,13 @@ class EventHandlerTest extends TestCase
         );
     }
 
+    public function test_all_mapped_octane_event_handlers_exist()
+    {
+        $this->tryAllEventHandlerMethods(
+            $this->getStaticPropertyValueFromClass(EventHandler::class, 'octaneEventHandlerMap')
+        );
+    }
+
     private function tryAllEventHandlerMethods(array $methods): void
     {
         $handler = new EventHandler($this->app, []);