Repository: getsentry/sentry-laravel (php)

Issue:
Http Response empty after sentry trace / breadcrumb
### How do you use Sentry?

Sentry SaaS (sentry.io)

### SDK version

3.7.*

### Steps to reproduce

1. Send a http request with Guzzle Client
2. Access the psr7 response
3. Response is empty

### Expected result

PSR7 response stream is filled with the body of the initial response from the server.


### Actual result

PSR7 response is empty as the sentry tracer reads the stream and does not rewind it.

Pull request description:
Fix consume PSR body streams when determining size
Fixes #754

We should not read the body to determine it's size since it could read the stream to completion. Rewinding is also a little convoluted since it requires checking if possible so I opted to get the size from the PSR object, downside is that it could fail to return a size because it's not known, but I don't think that's much of an issue for most use cases.

Patch:
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 26349cb..c46f074 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -96,4 +96,4 @@ jobs:
           composer install --no-interaction --prefer-dist --no-progress
 
       - name: Run phpunit
-        run: vendor/bin/phpunit
+        run: composer test:ci
diff --git a/.github/workflows/cs.yaml b/.github/workflows/cs.yaml
index e895ec1..faa8f7c 100644
--- a/.github/workflows/cs.yaml
+++ b/.github/workflows/cs.yaml
@@ -24,4 +24,4 @@ jobs:
         run: composer update --no-progress --no-interaction --prefer-dist
 
       - name: Run script
-        run: composer phpcs
+        run: composer phpcs:ci
diff --git a/.github/workflows/static-analysis.yaml b/.github/workflows/static-analysis.yaml
index e1bda21..0c3b62a 100644
--- a/.github/workflows/static-analysis.yaml
+++ b/.github/workflows/static-analysis.yaml
@@ -24,4 +24,4 @@ jobs:
         run: composer update --no-progress --no-interaction --prefer-dist
 
       - name: Run script
-        run: vendor/bin/phpstan analyse
+        run: composer phpstan:ci
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index eb44d3b..7f289c3 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -54,6 +54,14 @@ Tests can be run via [PHPUnit](https://phpunit.de).
 composer tests
 ```
 
+### Static analysis
+
+Static analysis can be run via [PHPStan](https://phpstan.org).
+
+```bash
+composer phpstan
+```
+
 ### Code style
 
 The code is automatically formatted through [php-cs-fixer](https://cs.symfony.com).
diff --git a/Makefile b/Makefile
index 0fbc956..58382ed 100644
--- a/Makefile
+++ b/Makefile
@@ -20,6 +20,10 @@ setup-git:
 phpcs:
 	composer phpcs
 
+.PHONY: phpstan
+phpstan:
+	composer phpstan
+
 .PHONY: tests
 tests:
 	composer tests
diff --git a/composer.json b/composer.json
index dea1f86..5574c77 100644
--- a/composer.json
+++ b/composer.json
@@ -47,12 +47,13 @@
         }
     },
     "scripts": {
-        "phpcs": [
-            "vendor/bin/php-cs-fixer fix --verbose --diff --dry-run"
-        ],
-        "tests": [
-            "vendor/bin/phpunit --verbose"
-        ]
+        "test": "vendor/bin/phpunit",
+        "tests": "@test",
+        "test:ci": "vendor/bin/phpunit",
+        "phpcs": "vendor/bin/php-cs-fixer fix",
+        "phpcs:ci": "vendor/bin/php-cs-fixer fix --dry-run --diff",
+        "phpstan": "vendor/bin/phpstan analyse",
+        "phpstan:ci": "vendor/bin/phpstan analyse --error-format github"
     },
     "extra": {
         "branch-alias": {
diff --git a/src/Sentry/Laravel/EventHandler.php b/src/Sentry/Laravel/EventHandler.php
index 09b5bca..e6c3add 100644
--- a/src/Sentry/Laravel/EventHandler.php
+++ b/src/Sentry/Laravel/EventHandler.php
@@ -291,8 +291,8 @@ class EventHandler
                 'status_code' => $event->response->status(),
                 'http.query' => $fullUri->getQuery(),
                 'http.fragment' => $fullUri->getFragment(),
-                'request_body_size' => strlen($event->request->body()),
-                'response_body_size' => strlen($event->response->body()),
+                'request_body_size' => $event->request->toPsrRequest()->getBody()->getSize(),
+                'response_body_size' => $event->response->toPsrResponse()->getBody()->getSize(),
             ]
         ));
     }
@@ -315,7 +315,7 @@ class EventHandler
                 'method' => $event->request->method(),
                 'http.query' => $fullUri->getQuery(),
                 'http.fragment' => $fullUri->getFragment(),
-                'request_body_size' => strlen($event->request->body()),
+                'request_body_size' => $event->request->toPsrRequest()->getBody()->getSize(),
             ]
         ));
     }


Test patch:
diff --git a/test/Sentry/Features/HttpClientIntegrationTest.php b/test/Sentry/Features/HttpClientIntegrationTest.php
new file mode 100644
index 0000000..2acb03b
--- /dev/null
+++ b/test/Sentry/Features/HttpClientIntegrationTest.php
@@ -0,0 +1,53 @@
+<?php
+
+namespace Sentry\Laravel\Tests\Features;
+
+use GuzzleHttp\Psr7\Request as PsrRequest;
+use GuzzleHttp\Psr7\Response as PsrResponse;
+use Illuminate\Http\Client\Events\ResponseReceived;
+use Illuminate\Http\Client\Request;
+use Illuminate\Http\Client\Response;
+use Sentry\Laravel\Tests\TestCase;
+
+class HttpClientIntegrationTest extends TestCase
+{
+    protected function setUp(): void
+    {
+        if (!class_exists(ResponseReceived::class)) {
+            $this->markTestSkipped('The Laravel HTTP client events are only available in Laravel 8.0+');
+        }
+
+        parent::setUp();
+    }
+
+    public function testHttpClientBreadcrumbIsRecordedForResponseReceivedEvent(): void
+    {
+        $this->dispatchLaravelEvent(new ResponseReceived(
+            new Request(new PsrRequest('GET', 'https://example.com', [], 'request')),
+            new Response(new PsrResponse(200, [], 'response'))
+        ));
+
+        $this->assertCount(1, $this->getCurrentBreadcrumbs());
+
+        $metadata = $this->getLastBreadcrumb()->getMetadata();
+
+        $this->assertEquals('GET', $metadata['method']);
+        $this->assertEquals('https://example.com', $metadata['url']);
+        $this->assertEquals(200, $metadata['status_code']);
+        $this->assertEquals(7, $metadata['request_body_size']);
+        $this->assertEquals(8, $metadata['response_body_size']);
+    }
+
+    public function testHttpClientBreadcrumbDoesntConsumeBodyStream(): void
+    {
+        $this->dispatchLaravelEvent(new ResponseReceived(
+            $request = new Request(new PsrRequest('GET', 'https://example.com', [], 'request')),
+            $response = new Response(new PsrResponse(200, [], 'response'))
+        ));
+
+        $this->assertCount(1, $this->getCurrentBreadcrumbs());
+
+        $this->assertEquals('request', $request->toPsrRequest()->getBody()->getContents());
+        $this->assertEquals('response', $response->toPsrResponse()->getBody()->getContents());
+    }
+}