Repository: getsentry/sentry-php (php)

Issue:
Log exception's code
### Problem Statement

I work a lot with the [Twilio SDK](https://github.com/twilio/twilio-php). In case of an error, a [RestException](https://github.com/twilio/twilio-php/blob/0739f62e6280d9a20925d2f30a71bfd2bcac48f5/src/Twilio/Version.php#L84-L88) is thrown with a text description of the error, as well as a code in [their internal error system](https://www.twilio.com/docs/api/errors). In the current implementation, I can't see this code.

### Solution Brainstorm

According to Sentry's documentation, there is a [exception's meta information](https://develop.sentry.dev/sdk/event-payloads/exception/#meta-information), which seems to be the way to provide the wanted information.

I have [tried to implement it](https://github.com/getsentry/sentry-php/compare/master...erickskrauch:sentry-php:32257d37f6fd405004b0c018534a8408d25130bc), but it seems like Sentry filters out this field on the service side (I checked that this field presented just before JSON encoding when a request is about to be sent).

Pull request:
Log exception's code
Fixes #1450.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b473afc..7dd858d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,8 @@
 ## Unreleased
 
 - feat: add object ID to serialized value (#1443)
+- feat: Add support for sending arbitrary extra data as part of the exception mechanism (#1450)
+- feat: Log exception's `code` to the `ExceptionMechanism::data` (#1450)
 
 ## 3.12.1 (2023-01-12)
 
diff --git a/src/Client.php b/src/Client.php
index 875947c..616491f 100644
--- a/src/Client.php
+++ b/src/Client.php
@@ -383,7 +383,7 @@ private function addThrowableToEvent(Event $event, \Throwable $exception, EventH
             $exceptions[] = new ExceptionDataBag(
                 $exception,
                 $this->stacktraceBuilder->buildFromException($exception),
-                $hint->mechanism ?? new ExceptionMechanism(ExceptionMechanism::TYPE_GENERIC, true)
+                $hint->mechanism ?? new ExceptionMechanism(ExceptionMechanism::TYPE_GENERIC, true, ['code' => $exception->getCode()])
             );
         } while ($exception = $exception->getPrevious());
 
diff --git a/src/ExceptionMechanism.php b/src/ExceptionMechanism.php
index 8dc83a9..9a3cf23 100644
--- a/src/ExceptionMechanism.php
+++ b/src/ExceptionMechanism.php
@@ -26,18 +26,27 @@ final class ExceptionMechanism
      */
     private $handled;
 
+    /**
+     * @var array<string, mixed> Arbitrary extra data that might help the user
+     *                           understand the error thrown by this mechanism
+     */
+    private $data;
+
     /**
      * Class constructor.
      *
-     * @param string $type    Unique identifier of this mechanism determining
-     *                        rendering and processing of the mechanism data
-     * @param bool   $handled Flag indicating whether the exception has been
-     *                        handled by the user (e.g. via try..catch)
+     * @param string               $type    Unique identifier of this mechanism determining
+     *                                      rendering and processing of the mechanism data
+     * @param bool                 $handled Flag indicating whether the exception has been
+     *                                      handled by the user (e.g. via try..catch)
+     * @param array<string, mixed> $data    Arbitrary extra data that might help the user
+     *                                      understand the error thrown by this mechanism
      */
-    public function __construct(string $type, bool $handled)
+    public function __construct(string $type, bool $handled, array $data = [])
     {
         $this->type = $type;
         $this->handled = $handled;
+        $this->data = $data;
     }
 
     /**
@@ -57,4 +66,25 @@ public function isHandled(): bool
     {
         return $this->handled;
     }
+
+    /**
+     * Returns arbitrary extra data that might help the user understand the error
+     * thrown by this mechanism.
+     *
+     * @return array<string, mixed>
+     */
+    public function getData(): array
+    {
+        return $this->data;
+    }
+
+    /**
+     * Sets the arbitrary extra data.
+     *
+     * @param array<string, mixed> $data
+     */
+    public function setData(array $data): void
+    {
+        $this->data = $data;
+    }
 }
diff --git a/src/Integration/AbstractErrorListenerIntegration.php b/src/Integration/AbstractErrorListenerIntegration.php
index 487933a..530dc86 100644
--- a/src/Integration/AbstractErrorListenerIntegration.php
+++ b/src/Integration/AbstractErrorListenerIntegration.php
@@ -36,7 +36,13 @@ protected function addExceptionMechanismToEvent(Event $event): Event
         $exceptions = $event->getExceptions();
 
         foreach ($exceptions as $exception) {
-            $exception->setMechanism(new ExceptionMechanism(ExceptionMechanism::TYPE_GENERIC, false));
+            $data = [];
+            $mechanism = $exception->getMechanism();
+            if (null !== $mechanism) {
+                $data = $mechanism->getData();
+            }
+
+            $exception->setMechanism(new ExceptionMechanism(ExceptionMechanism::TYPE_GENERIC, false, $data));
         }
 
         return $event;
diff --git a/src/Serializer/PayloadSerializer.php b/src/Serializer/PayloadSerializer.php
index baf3ede..6a71991 100644
--- a/src/Serializer/PayloadSerializer.php
+++ b/src/Serializer/PayloadSerializer.php
@@ -266,7 +266,8 @@ private function serializeBreadcrumb(Breadcrumb $breadcrumb): array
      *     },
      *     mechanism?: array{
      *         type: string,
-     *         handled: boolean
+     *         handled: boolean,
+     *         data?: array<string, mixed>
      *     }
      * }
      */
@@ -290,6 +291,10 @@ private function serializeException(ExceptionDataBag $exception): array
                 'type' => $exceptionMechanism->getType(),
                 'handled' => $exceptionMechanism->isHandled(),
             ];
+
+            if ([] !== $exceptionMechanism->getData()) {
+                $result['mechanism']['data'] = $exceptionMechanism->getData();
+            }
         }
 
         return $result;


Test patch:
diff --git a/tests/ClientTest.php b/tests/ClientTest.php
index 4bee020..078ddc1 100644
--- a/tests/ClientTest.php
+++ b/tests/ClientTest.php
@@ -724,7 +724,7 @@ public function testBuildEventWithException(): void
     {
         $options = new Options();
         $previousException = new \RuntimeException('testMessage2');
-        $exception = new \Exception('testMessage', 0, $previousException);
+        $exception = new \Exception('testMessage', 1, $previousException);
 
         /** @var TransportInterface&MockObject $transport */
         $transport = $this->createMock(TransportInterface::class);
@@ -735,12 +735,12 @@ public function testBuildEventWithException(): void
 
                 $this->assertCount(2, $capturedExceptions);
                 $this->assertNotNull($capturedExceptions[0]->getStacktrace());
-                $this->assertEquals(new ExceptionMechanism(ExceptionMechanism::TYPE_GENERIC, true), $capturedExceptions[0]->getMechanism());
+                $this->assertEquals(new ExceptionMechanism(ExceptionMechanism::TYPE_GENERIC, true, ['code' => 1]), $capturedExceptions[0]->getMechanism());
                 $this->assertSame(\Exception::class, $capturedExceptions[0]->getType());
                 $this->assertSame('testMessage', $capturedExceptions[0]->getValue());
 
                 $this->assertNotNull($capturedExceptions[1]->getStacktrace());
-                $this->assertEquals(new ExceptionMechanism(ExceptionMechanism::TYPE_GENERIC, true), $capturedExceptions[1]->getMechanism());
+                $this->assertEquals(new ExceptionMechanism(ExceptionMechanism::TYPE_GENERIC, true, ['code' => 0]), $capturedExceptions[1]->getMechanism());
                 $this->assertSame(\RuntimeException::class, $capturedExceptions[1]->getType());
                 $this->assertSame('testMessage2', $capturedExceptions[1]->getValue());
 
diff --git a/tests/ExceptionMechanismTest.php b/tests/ExceptionMechanismTest.php
new file mode 100644
index 0000000..60e9353
--- /dev/null
+++ b/tests/ExceptionMechanismTest.php
@@ -0,0 +1,58 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Sentry\Tests;
+
+use PHPUnit\Framework\TestCase;
+use Sentry\ExceptionMechanism;
+
+final class ExceptionMechanismTest extends TestCase
+{
+    /**
+     * @dataProvider constructorDataProvider
+     */
+    public function testConstructor(
+        array $constructorArgs,
+        string $expectedType,
+        bool $expectedHandled,
+        array $expectedData
+    ): void {
+        $exceptionMechanism = new ExceptionMechanism(...$constructorArgs);
+
+        $this->assertSame($expectedType, $exceptionMechanism->getType());
+        $this->assertSame($expectedHandled, $exceptionMechanism->isHandled());
+        $this->assertSame($expectedData, $exceptionMechanism->getData());
+    }
+
+    public function constructorDataProvider(): iterable
+    {
+        yield [
+            [
+                ExceptionMechanism::TYPE_GENERIC,
+                true,
+            ],
+            ExceptionMechanism::TYPE_GENERIC,
+            true,
+            [],
+        ];
+
+        yield [
+            [
+                'custom',
+                false,
+                ['key' => 'value'],
+            ],
+            'custom',
+            false,
+            ['key' => 'value'],
+        ];
+    }
+
+    public function testSetData(): void
+    {
+        $exceptionDataBag = new ExceptionMechanism(ExceptionMechanism::TYPE_GENERIC, true, ['replace' => 'me']);
+        $exceptionDataBag->setData(['new' => 'value']);
+        $this->assertSame(['new' => 'value'], $exceptionDataBag->getData());
+    }
+}
diff --git a/tests/Serializer/PayloadSerializerTest.php b/tests/Serializer/PayloadSerializerTest.php
index 73f60a3..3165f63 100644
--- a/tests/Serializer/PayloadSerializerTest.php
+++ b/tests/Serializer/PayloadSerializerTest.php
@@ -179,7 +179,7 @@ public function serializeDataProvider(): iterable
                     $frame1,
                     $frame2,
                 ]),
-                new ExceptionMechanism(ExceptionMechanism::TYPE_GENERIC, true)
+                new ExceptionMechanism(ExceptionMechanism::TYPE_GENERIC, true, ['code' => 123])
             ),
         ]);
 
@@ -314,7 +314,10 @@ public function serializeDataProvider(): iterable
                 },
                 "mechanism": {
                     "type": "generic",
-                    "handled": true
+                    "handled": true,
+                    "data": {
+                        "code": 123
+                    }
                 }
             },
             {