Repository: getsentry/sentry-php (php)

Issue:
Expose the handled property to users
One of the most common ways frameworks integrate Sentry is by using `captureException` somewhere in the framework-specific error handling code.
This creates a small issue though, as everything that gets sent via `captureException` is `handled: true`, and only errors handled by the SDK's error handler are `handled: false`.

One solution to this could be the addition of a new function, `\Sentry\captureUnhandledException`, which would allow people to mark exceptions as `handled: false`.



Pull request description:
ref: Expose the ExceptionMechanism through the event hint
This makes it possible to access the `handled` property when reporting errors to Sentry.
While this approach is definitely not the best solution, I didn't find a better way without breaking BC.

```
$hint = EventHint::fromArray([
    'exception' => $exception,
    'mechanism' => new ExceptionMechanism(ExceptionMechanism::TYPE_GENERIC, false),
]);

captureEvent(Event::createEvent(), $hint);
```

Remaining ToDos

- [x] Add more tests 

Closes #1350 

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e6487e4..d5f4dad 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,7 @@
 ## Unreleased
 
 ref: Enable the ModulesIntegration by default (#1415)
+ref: Expose the ExceptionMechanism through the event hint (#1416)
 
 ## 3.10.0 (2022-10-19)
 
diff --git a/src/Client.php b/src/Client.php
index a5ca1b2..e91926c 100644
--- a/src/Client.php
+++ b/src/Client.php
@@ -248,7 +248,7 @@ private function prepareEvent(Event $event, ?EventHint $hint = null, ?Scope $sco
     {
         if (null !== $hint) {
             if (null !== $hint->exception && empty($event->getExceptions())) {
-                $this->addThrowableToEvent($event, $hint->exception);
+                $this->addThrowableToEvent($event, $hint->exception, $hint);
             }
 
             if (null !== $hint->stacktrace && null === $event->getStacktrace()) {
@@ -338,8 +338,9 @@ private function addMissingStacktraceToEvent(Event $event): void
      *
      * @param Event      $event     The event that will be enriched with the exception
      * @param \Throwable $exception The exception that will be processed and added to the event
+     * @param EventHint  $hint      Contains additional information about the event
      */
-    private function addThrowableToEvent(Event $event, \Throwable $exception): void
+    private function addThrowableToEvent(Event $event, \Throwable $exception, EventHint $hint): void
     {
         if ($exception instanceof \ErrorException && null === $event->getLevel()) {
             $event->setLevel(Severity::fromError($exception->getSeverity()));
@@ -351,7 +352,7 @@ private function addThrowableToEvent(Event $event, \Throwable $exception): void
             $exceptions[] = new ExceptionDataBag(
                 $exception,
                 $this->stacktraceBuilder->buildFromException($exception),
-                new ExceptionMechanism(ExceptionMechanism::TYPE_GENERIC, true)
+                $hint->mechanism ?? new ExceptionMechanism(ExceptionMechanism::TYPE_GENERIC, true)
             );
         } while ($exception = $exception->getPrevious());
 
diff --git a/src/EventHint.php b/src/EventHint.php
index 145156e..f1f5d6a 100644
--- a/src/EventHint.php
+++ b/src/EventHint.php
@@ -16,6 +16,13 @@ final class EventHint
      */
     public $exception;
 
+    /**
+     * An object describing the mechanism of the original exception.
+     *
+     * @var ExceptionMechanism|null
+     */
+    public $mechanism;
+
     /**
      * The stacktrace to set on the event.
      *
@@ -43,6 +50,7 @@ public static function fromArray(array $hintData): self
     {
         $hint = new self();
         $exception = $hintData['exception'] ?? null;
+        $mechanism = $hintData['mechanism'] ?? null;
         $stacktrace = $hintData['stacktrace'] ?? null;
         $extra = $hintData['extra'] ?? [];
 
@@ -50,6 +58,10 @@ public static function fromArray(array $hintData): self
             throw new \InvalidArgumentException(sprintf('The value of the "exception" field must be an instance of a class implementing the "%s" interface. Got: "%s".', \Throwable::class, get_debug_type($exception)));
         }
 
+        if (null !== $mechanism && !$mechanism instanceof ExceptionMechanism) {
+            throw new \InvalidArgumentException(sprintf('The value of the "mechanism" field must be an instance of the "%s" class. Got: "%s".', ExceptionMechanism::class, get_debug_type($mechanism)));
+        }
+
         if (null !== $stacktrace && !$stacktrace instanceof Stacktrace) {
             throw new \InvalidArgumentException(sprintf('The value of the "stacktrace" field must be an instance of the "%s" class. Got: "%s".', Stacktrace::class, get_debug_type($stacktrace)));
         }
@@ -59,6 +71,7 @@ public static function fromArray(array $hintData): self
         }
 
         $hint->exception = $exception;
+        $hint->mechanism = $mechanism;
         $hint->stacktrace = $stacktrace;
         $hint->extra = $extra;
 


Test patch:
diff --git a/tests/ClientTest.php b/tests/ClientTest.php
index 9bfc982..cbc3334 100644
--- a/tests/ClientTest.php
+++ b/tests/ClientTest.php
@@ -705,6 +705,42 @@ public function testBuildEventWithException(): void
         ]));
     }
 
+    public function testBuildEventWithExceptionAndMechansim(): void
+    {
+        $options = new Options();
+        $exception = new \Exception('testMessage');
+
+        /** @var TransportInterface&MockObject $transport */
+        $transport = $this->createMock(TransportInterface::class);
+        $transport->expects($this->once())
+            ->method('send')
+            ->with($this->callback(function (Event $event): bool {
+                $capturedExceptions = $event->getExceptions();
+
+                $this->assertCount(1, $capturedExceptions);
+                $this->assertNotNull($capturedExceptions[0]->getStacktrace());
+                $this->assertEquals(new ExceptionMechanism(ExceptionMechanism::TYPE_GENERIC, false), $capturedExceptions[0]->getMechanism());
+                $this->assertSame(\Exception::class, $capturedExceptions[0]->getType());
+                $this->assertSame('testMessage', $capturedExceptions[0]->getValue());
+
+                return true;
+            }));
+
+        $client = new Client(
+            $options,
+            $transport,
+            'sentry.sdk.identifier',
+            '1.2.3',
+            new Serializer($options),
+            $this->createMock(RepresentationSerializerInterface::class)
+        );
+
+        $client->captureEvent(Event::createEvent(), EventHint::fromArray([
+            'exception' => $exception,
+            'mechanism' => new ExceptionMechanism(ExceptionMechanism::TYPE_GENERIC, false),
+        ]));
+    }
+
     public function testBuildWithErrorException(): void
     {
         $options = new Options();
diff --git a/tests/EventHintTest.php b/tests/EventHintTest.php
index 4a5b01c..3c8bffb 100644
--- a/tests/EventHintTest.php
+++ b/tests/EventHintTest.php
@@ -6,6 +6,7 @@
 
 use PHPUnit\Framework\TestCase;
 use Sentry\EventHint;
+use Sentry\ExceptionMechanism;
 use Sentry\Frame;
 use Sentry\Stacktrace;
 
@@ -14,17 +15,20 @@ final class EventHintTest extends TestCase
     public function testCreateFromArray(): void
     {
         $exception = new \Exception();
+        $mechanism = new ExceptionMechanism(ExceptionMechanism::TYPE_GENERIC, false);
         $stacktrace = new Stacktrace([
             new Frame('function_1', 'path/to/file_1', 10),
         ]);
 
         $hint = EventHint::fromArray([
             'exception' => $exception,
+            'mechanism' => $mechanism,
             'stacktrace' => $stacktrace,
             'extra' => ['foo' => 'bar'],
         ]);
 
         $this->assertSame($exception, $hint->exception);
+        $this->assertSame($mechanism, $hint->mechanism);
         $this->assertSame($stacktrace, $hint->stacktrace);
         $this->assertSame(['foo' => 'bar'], $hint->extra);
     }
@@ -47,6 +51,11 @@ public function createFromArrayWithInvalidValuesDataProvider(): \Generator
             'The value of the "exception" field must be an instance of a class implementing the "Throwable" interface. Got: "string".',
         ];
 
+        yield [
+            ['mechanism' => 'foo'],
+            'The value of the "mechanism" field must be an instance of the "Sentry\\ExceptionMechanism" class. Got: "string".',
+        ];
+
         yield [
             ['stacktrace' => 'foo'],
             'The value of the "stacktrace" field must be an instance of the "Sentry\\Stacktrace" class. Got: "string".',