Repository: getsentry/sentry-php (php)

Issue:
Sentry cannot serialise `extra` with recursive arrays
### How do you use Sentry?

Sentry Saas (sentry.io)

### Version

3.13.1

### Steps to Reproduce

1.
```php
            $data = [
                'foo' => 'bar',
            ];
            $data['ref'] = &$data;

            $event = \Sentry\Event::createEvent();
            $event->setExtra([
                'data' => $data,
            ]);
            \Sentry\captureEvent($event);
```

### Expected Result

Event reported to the sentry server.

### Actual Result

No event is sent at all.

It happens because `Sentry\Util\JSON::encode` method throws.

Capturing _anything_ would be better, at the moment no event is sent to the server at all.

I'm keen to create a PR with similar to `monolog/monolog` approach where they substitute a too nested key with a "reached max level" message.

Pull request description:
Try to encode anything when encountering JSON encoding error
We try to output a partial JSON encoded value when encoding as JSON to try and send anything if we can. This makes the output a little more lenient for recursion, unsupported types and inf/nan in the JSON data.

Fixes #1480.

Doing this:

```php
\Sentry\init(['dsn' => 'https://...']);

$data = ['foo' => 'bar'];
$data['ref'] = &$data;

\Sentry\withScope(function (\Sentry\State\Scope $scope) use ($data): void {
    $scope->setExtra('data', $data);
    \Sentry\captureException(new Exception('This is a test!'));
});
```

Now results in:

![image](https://user-images.githubusercontent.com/1090754/221543486-679fefdc-85c5-40d4-ba5c-1930438d3332.png)

with the rest of the event details intact.

Patch:
diff --git a/src/Util/JSON.php b/src/Util/JSON.php
index 3cd0929..3c9360c 100644
--- a/src/Util/JSON.php
+++ b/src/Util/JSON.php
@@ -30,11 +30,15 @@ public static function encode($data, int $options = 0, int $maxDepth = 512): str
             throw new \InvalidArgumentException('The $maxDepth argument must be an integer greater than 0.');
         }
 
-        $options |= \JSON_UNESCAPED_UNICODE | \JSON_INVALID_UTF8_SUBSTITUTE;
+        $options |= \JSON_UNESCAPED_UNICODE | \JSON_INVALID_UTF8_SUBSTITUTE | \JSON_PARTIAL_OUTPUT_ON_ERROR;
 
         $encodedData = json_encode($data, $options, $maxDepth);
 
-        if (\JSON_ERROR_NONE !== json_last_error()) {
+        $allowedErrors = [\JSON_ERROR_NONE, \JSON_ERROR_RECURSION, \JSON_ERROR_INF_OR_NAN, \JSON_ERROR_UNSUPPORTED_TYPE];
+
+        $encounteredAnyError = \JSON_ERROR_NONE !== json_last_error();
+
+        if (($encounteredAnyError && ('null' === $encodedData || false === $encodedData)) || !\in_array(json_last_error(), $allowedErrors, true)) {
             throw new JsonException(sprintf('Could not encode value into JSON format. Error was: "%s".', json_last_error_msg()));
         }
 


Test patch:
diff --git a/tests/Util/JSONTest.php b/tests/Util/JSONTest.php
index ecc086a..a5b382f 100644
--- a/tests/Util/JSONTest.php
+++ b/tests/Util/JSONTest.php
@@ -60,6 +60,14 @@ public function encodeDataProvider(): \Generator
             new JsonSerializableClass(),
             '{"key":"value"}',
         ];
+
+        $data = [];
+        $data['recursive'] = &$data;
+
+        yield [
+            $data,
+            '{"recursive":{"recursive":null}}',
+        ];
     }
 
     /**