diff --git a/composer.json b/composer.json index 2651e869..2cf50001 100644 --- a/composer.json +++ b/composer.json @@ -41,7 +41,8 @@ "friendsofphp/php-cs-fixer": "^3.11", "mockery/mockery": "^1.3", "phpstan/phpstan": "^1.10", - "laravel/folio": "^1.0" + "laravel/folio": "^1.0", + "guzzlehttp/guzzle": "^7.2" }, "autoload-dev": { "psr-4": { diff --git a/src/Sentry/Laravel/EventHandler.php b/src/Sentry/Laravel/EventHandler.php index 7e250981..7a9eddaa 100644 --- a/src/Sentry/Laravel/EventHandler.php +++ b/src/Sentry/Laravel/EventHandler.php @@ -11,7 +11,6 @@ use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Events as DatabaseEvents; -use Illuminate\Http\Client\Events as HttpClientEvents; use Illuminate\Http\Request; use Illuminate\Log\Events as LogEvents; use Illuminate\Routing\Events as RoutingEvents; @@ -20,7 +19,6 @@ use RuntimeException; use Sentry\Breadcrumb; use Sentry\Laravel\Tracing\Middleware; -use Sentry\Laravel\Util\WorksWithUris; use Sentry\SentrySdk; use Sentry\State\Scope; use Symfony\Component\Console\Input\ArgvInput; @@ -28,8 +26,6 @@ class EventHandler { - use WorksWithUris; - /** * Map event handlers to events. * @@ -41,8 +37,6 @@ class EventHandler DatabaseEvents\QueryExecuted::class => 'queryExecuted', ConsoleEvents\CommandStarting::class => 'commandStarting', ConsoleEvents\CommandFinished::class => 'commandFinished', - HttpClientEvents\ResponseReceived::class => 'httpClientResponseReceived', - HttpClientEvents\ConnectionFailed::class => 'httpClientConnectionFailed', ]; /** @@ -123,13 +117,6 @@ class EventHandler */ private $recordOctaneTaskInfo; - /** - * Indicates if we should add HTTP client requests info to the breadcrumbs. - * - * @var bool - */ - private $recordHttpClientRequests; - /** * Indicates if we pushed a scope for Octane. * @@ -153,7 +140,6 @@ public function __construct(Container $container, array $config) $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; - $this->recordHttpClientRequests = ($config['breadcrumbs.http_client_requests'] ?? $config['breadcrumbs']['http_client_requests'] ?? true) === true; } /** @@ -278,59 +264,6 @@ protected function messageLoggedHandler(LogEvents\MessageLogged $logEntry): void )); } - protected function httpClientResponseReceivedHandler(HttpClientEvents\ResponseReceived $event): void - { - if (!$this->recordHttpClientRequests) { - return; - } - - $level = Breadcrumb::LEVEL_INFO; - if ($event->response->failed()) { - $level = Breadcrumb::LEVEL_ERROR; - } - - $fullUri = $this->getFullUri($event->request->url()); - - Integration::addBreadcrumb(new Breadcrumb( - $level, - Breadcrumb::TYPE_HTTP, - 'http', - null, - [ - 'url' => $this->getPartialUri($fullUri), - 'http.request.method' => $event->request->method(), - 'http.response.status_code' => $event->response->status(), - 'http.query' => $fullUri->getQuery(), - 'http.fragment' => $fullUri->getFragment(), - 'http.request.body.size' => $event->request->toPsrRequest()->getBody()->getSize(), - 'http.response.body.size' => $event->response->toPsrResponse()->getBody()->getSize(), - ] - )); - } - - protected function httpClientConnectionFailedHandler(HttpClientEvents\ConnectionFailed $event): void - { - if (!$this->recordHttpClientRequests) { - return; - } - - $fullUri = $this->getFullUri($event->request->url()); - - Integration::addBreadcrumb(new Breadcrumb( - Breadcrumb::LEVEL_ERROR, - Breadcrumb::TYPE_HTTP, - 'http', - null, - [ - 'url' => $this->getPartialUri($fullUri), - 'http.request.method' => $event->request->method(), - 'http.query' => $fullUri->getQuery(), - 'http.fragment' => $fullUri->getFragment(), - 'http.request.body.size' => $event->request->toPsrRequest()->getBody()->getSize(), - ] - )); - } - protected function authenticatedHandler(AuthEvents\Authenticated $event): void { $this->configureUserScopeFromModel($event->user); diff --git a/src/Sentry/Laravel/Features/Concerns/TracksPushedScopesAndSpans.php b/src/Sentry/Laravel/Features/Concerns/TracksPushedScopesAndSpans.php new file mode 100644 index 00000000..922458f7 --- /dev/null +++ b/src/Sentry/Laravel/Features/Concerns/TracksPushedScopesAndSpans.php @@ -0,0 +1,75 @@ + + */ + private $parentSpanStack = []; + + /** + * Hold the stack of current spans that need to be finished still. + * + * @var array + */ + private $currentSpanStack = []; + + protected function pushSpan(Span $span): void + { + $hub = SentrySdk::getCurrentHub(); + + $this->parentSpanStack[] = $hub->getSpan(); + + $hub->setSpan($span); + + $this->currentSpanStack[] = $span; + } + + protected function pushScope(): void + { + SentrySdk::getCurrentHub()->pushScope(); + + ++$this->pushedScopeCount; + } + + protected function maybePopSpan(): ?Span + { + if (count($this->currentSpanStack) === 0) { + return null; + } + + $parent = array_pop($this->parentSpanStack); + + SentrySdk::getCurrentHub()->setSpan($parent); + + return array_pop($this->currentSpanStack); + } + + protected function maybePopScope(): void + { + Integration::flushEvents(); + + if ($this->pushedScopeCount === 0) { + return; + } + + SentrySdk::getCurrentHub()->popScope(); + + --$this->pushedScopeCount; + } +} diff --git a/src/Sentry/Laravel/Features/HttpClientIntegration.php b/src/Sentry/Laravel/Features/HttpClientIntegration.php new file mode 100644 index 00000000..be57e7f5 --- /dev/null +++ b/src/Sentry/Laravel/Features/HttpClientIntegration.php @@ -0,0 +1,207 @@ +isTracingFeatureEnabled(self::FEATURE_KEY) + || $this->isBreadcrumbFeatureEnabled(self::FEATURE_KEY); + } + + public function onBoot(Dispatcher $events, Factory $factory): void + { + if ($this->isTracingFeatureEnabled(self::FEATURE_KEY)) { + $events->listen(RequestSending::class, [$this, 'handleRequestSendingHandlerForTracing']); + $events->listen(ResponseReceived::class, [$this, 'handleResponseReceivedHandlerForTracing']); + $events->listen(ConnectionFailed::class, [$this, 'handleConnectionFailedHandlerForTracing']); + + // The `globalRequestMiddleware` functionality was introduced in Laravel 10.14 + if (method_exists($factory, 'globalRequestMiddleware')) { + $factory->globalRequestMiddleware([$this, 'attachTracingHeadersToRequest']); + } + } + + if ($this->isBreadcrumbFeatureEnabled(self::FEATURE_KEY)) { + $events->listen(ResponseReceived::class, [$this, 'handleResponseReceivedHandlerForBreadcrumb']); + $events->listen(ConnectionFailed::class, [$this, 'handleConnectionFailedHandlerForBreadcrumb']); + } + } + + public function attachTracingHeadersToRequest(RequestInterface $request) + { + if ($this->shouldAttachTracingHeaders($request)) { + return $request + ->withHeader('baggage', getBaggage()) + ->withHeader('sentry-trace', getTraceparent()); + } + + return $request; + } + + public function handleRequestSendingHandlerForTracing(RequestSending $event): void + { + $parentSpan = SentrySdk::getCurrentHub()->getSpan(); + + // If there is no tracing span active there is no need to handle the event + if ($parentSpan === null) { + return; + } + + $context = new SpanContext; + + $fullUri = $this->getFullUri($event->request->url()); + $partialUri = $this->getPartialUri($fullUri); + + $context->setOp('http.client'); + $context->setDescription($event->request->method() . ' ' . $partialUri); + $context->setData([ + 'url' => $partialUri, + // See: https://develop.sentry.dev/sdk/performance/span-data-conventions/#http + 'http.query' => $fullUri->getQuery(), + 'http.fragment' => $fullUri->getFragment(), + 'http.request.method' => $event->request->method(), + 'http.request.body.size' => $event->request->toPsrRequest()->getBody()->getSize(), + ]); + + $this->pushSpan($parentSpan->startChild($context)); + } + + public function handleResponseReceivedHandlerForTracing(ResponseReceived $event): void + { + $span = $this->maybePopSpan(); + + if ($span !== null) { + $span->finish(); + $span->setData(array_merge($span->getData(), [ + // See: https://develop.sentry.dev/sdk/performance/span-data-conventions/#http + 'http.response.status_code' => $event->response->status(), + 'http.response.body.size' => $event->response->toPsrResponse()->getBody()->getSize(), + ])); + $span->setHttpStatus($event->response->status()); + } + } + + public function handleConnectionFailedHandlerForTracing(ConnectionFailed $event): void + { + $span = $this->maybePopSpan(); + + if ($span !== null) { + $span->finish(); + $span->setStatus(SpanStatus::internalError()); + } + } + + public function handleResponseReceivedHandlerForBreadcrumb(ResponseReceived $event): void + { + $level = Breadcrumb::LEVEL_INFO; + + if ($event->response->failed()) { + $level = Breadcrumb::LEVEL_ERROR; + } + + $fullUri = $this->getFullUri($event->request->url()); + + Integration::addBreadcrumb(new Breadcrumb( + $level, + Breadcrumb::TYPE_HTTP, + 'http', + null, + [ + 'url' => $this->getPartialUri($fullUri), + // See: https://develop.sentry.dev/sdk/performance/span-data-conventions/#http + 'http.query' => $fullUri->getQuery(), + 'http.fragment' => $fullUri->getFragment(), + 'http.request.method' => $event->request->method(), + 'http.response.status_code' => $event->response->status(), + 'http.request.body.size' => $event->request->toPsrRequest()->getBody()->getSize(), + 'http.response.body.size' => $event->response->toPsrResponse()->getBody()->getSize(), + ] + )); + } + + public function handleConnectionFailedHandlerForBreadcrumb(ConnectionFailed $event): void + { + $fullUri = $this->getFullUri($event->request->url()); + + Integration::addBreadcrumb(new Breadcrumb( + Breadcrumb::LEVEL_ERROR, + Breadcrumb::TYPE_HTTP, + 'http', + null, + [ + 'url' => $this->getPartialUri($fullUri), + // See: https://develop.sentry.dev/sdk/performance/span-data-conventions/#http + 'http.query' => $fullUri->getQuery(), + 'http.fragment' => $fullUri->getFragment(), + 'http.request.method' => $event->request->method(), + 'http.request.body.size' => $event->request->toPsrRequest()->getBody()->getSize(), + ] + )); + } + + /** + * Construct a full URI. + * + * @param string $url + * + * @return UriInterface + */ + private function getFullUri(string $url): UriInterface + { + return new Uri($url); + } + + /** + * Construct a partial URI, excluding the authority, query and fragment parts. + * + * @param UriInterface $uri + * + * @return string + */ + private function getPartialUri(UriInterface $uri): string + { + return (string)Uri::fromParts([ + 'scheme' => $uri->getScheme(), + 'host' => $uri->getHost(), + 'port' => $uri->getPort(), + 'path' => $uri->getPath(), + ]); + } + + private function shouldAttachTracingHeaders(RequestInterface $request): bool + { + $client = SentrySdk::getCurrentHub()->getClient(); + if ($client === null) { + return false; + } + + $sdkOptions = $client->getOptions(); + + // Check if the request destination is allow listed in the trace_propagation_targets option. + return $sdkOptions->getTracePropagationTargets() === null + || in_array($request->getUri()->getHost(), $sdkOptions->getTracePropagationTargets()); + } +} diff --git a/src/Sentry/Laravel/Features/QueueIntegration.php b/src/Sentry/Laravel/Features/QueueIntegration.php index 520f882a..28560dc4 100644 --- a/src/Sentry/Laravel/Features/QueueIntegration.php +++ b/src/Sentry/Laravel/Features/QueueIntegration.php @@ -9,11 +9,11 @@ use Illuminate\Queue\Events\WorkerStopping; use Illuminate\Queue\Queue; use Sentry\Breadcrumb; +use Sentry\Laravel\Features\Concerns\TracksPushedScopesAndSpans; use Sentry\Laravel\Integration; use Sentry\SentrySdk; use Sentry\State\Scope; use Sentry\Tracing\PropagationContext; -use Sentry\Tracing\Span; use Sentry\Tracing\SpanContext; use Sentry\Tracing\SpanStatus; use Sentry\Tracing\TransactionContext; @@ -25,30 +25,13 @@ class QueueIntegration extends Feature { + use TracksPushedScopesAndSpans { + pushScope as private pushScopeTrait; + } + private const QUEUE_PAYLOAD_BAGGAGE_DATA = 'sentry_baggage_data'; private const QUEUE_PAYLOAD_TRACE_PARENT_DATA = 'sentry_trace_parent_data'; - /** - * Hold the number of times the scope was pushed. - * - * @var int - */ - private $pushedScopeCount = 0; - - /** - * Hold the stack of parent spans that need to be put back on the scope. - * - * @var array - */ - private $parentSpanStack = []; - - /** - * Hold the stack of current spans that need to be finished still. - * - * @var array - */ - private $currentSpanStack = []; - public function isApplicable(): bool { if (!$this->container()->bound('queue')) { @@ -181,22 +164,19 @@ public function handleJobExceptionOccurredQueueEvent(JobExceptionOccurred $event Integration::flushEvents(); } - private function pushSpan(Span $span): void + private function finishJobWithStatus(SpanStatus $status): void { - $hub = SentrySdk::getCurrentHub(); - - $this->parentSpanStack[] = $hub->getSpan(); - - $hub->setSpan($span); + $span = $this->maybePopSpan(); - $this->currentSpanStack[] = $span; + if ($span !== null) { + $span->finish(); + $span->setStatus($status); + } } - private function pushScope(): void + protected function pushScope(): void { - SentrySdk::getCurrentHub()->pushScope(); - - ++$this->pushedScopeCount; + $this->pushScopeTrait(); // When a job starts, we want to make sure the scope is cleared of breadcrumbs // as well as setting a new propagation context. @@ -205,40 +185,4 @@ private function pushScope(): void $scope->setPropagationContext(PropagationContext::fromDefaults()); }); } - - private function maybePopSpan(): ?Span - { - if (count($this->currentSpanStack) === 0) { - return null; - } - - $parent = array_pop($this->parentSpanStack); - - SentrySdk::getCurrentHub()->setSpan($parent); - - return array_pop($this->currentSpanStack); - } - - private function maybePopScope(): void - { - Integration::flushEvents(); - - if ($this->pushedScopeCount === 0) { - return; - } - - SentrySdk::getCurrentHub()->popScope(); - - --$this->pushedScopeCount; - } - - private function finishJobWithStatus(SpanStatus $status): void - { - $span = $this->maybePopSpan(); - - if ($span !== null) { - $span->finish(); - $span->setStatus($status); - } - } } diff --git a/src/Sentry/Laravel/ServiceProvider.php b/src/Sentry/Laravel/ServiceProvider.php index 9d441fab..f239e815 100644 --- a/src/Sentry/Laravel/ServiceProvider.php +++ b/src/Sentry/Laravel/ServiceProvider.php @@ -57,8 +57,9 @@ class ServiceProvider extends BaseServiceProvider Features\CacheIntegration::class, Features\QueueIntegration::class, Features\ConsoleIntegration::class, - Features\FolioPackageIntegration::class, Features\Storage\Integration::class, + Features\HttpClientIntegration::class, + Features\FolioPackageIntegration::class, Features\LivewirePackageIntegration::class, ]; diff --git a/src/Sentry/Laravel/Tracing/EventHandler.php b/src/Sentry/Laravel/Tracing/EventHandler.php index 15314c8e..b184ca82 100644 --- a/src/Sentry/Laravel/Tracing/EventHandler.php +++ b/src/Sentry/Laravel/Tracing/EventHandler.php @@ -5,12 +5,10 @@ use Exception; use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Database\Events as DatabaseEvents; -use Illuminate\Http\Client\Events as HttpClientEvents; use Illuminate\Routing\Events as RoutingEvents; use RuntimeException; use Sentry\Laravel\Features\Concerns\ResolvesEventOrigin; use Sentry\Laravel\Integration; -use Sentry\Laravel\Util\WorksWithUris; use Sentry\SentrySdk; use Sentry\Tracing\Span; use Sentry\Tracing\SpanContext; @@ -19,7 +17,7 @@ class EventHandler { - use WorksWithUris, ResolvesEventOrigin; + use ResolvesEventOrigin; /** * Map event handlers to events. @@ -31,9 +29,6 @@ class EventHandler DatabaseEvents\QueryExecuted::class => 'queryExecuted', RoutingEvents\ResponsePrepared::class => 'responsePrepared', RoutingEvents\PreparingResponse::class => 'responsePreparing', - HttpClientEvents\RequestSending::class => 'httpClientRequestSending', - HttpClientEvents\ResponseReceived::class => 'httpClientResponseReceived', - HttpClientEvents\ConnectionFailed::class => 'httpClientConnectionFailed', DatabaseEvents\TransactionBeginning::class => 'transactionBeginning', DatabaseEvents\TransactionCommitted::class => 'transactionCommitted', DatabaseEvents\TransactionRolledBack::class => 'transactionRolledBack', @@ -67,13 +62,6 @@ class EventHandler */ private $traceQueueJobsAsTransactions; - /** - * Indicates if we should trace HTTP client requests. - * - * @var bool - */ - private $traceHttpClientRequests; - /** * Hold the stack of parent spans that need to be put back on the scope. * @@ -96,8 +84,6 @@ public function __construct(array $config) $this->traceSqlQueries = ($config['sql_queries'] ?? true) === true; $this->traceSqlQueryOrigins = ($config['sql_origin'] ?? true) === true; - $this->traceHttpClientRequests = ($config['http_client_requests'] ?? true) === true; - $this->traceQueueJobs = ($config['queue_jobs'] ?? false) === true; $this->traceQueueJobsAsTransactions = ($config['queue_job_transactions'] ?? false) === true; } @@ -112,9 +98,6 @@ public function __construct(array $config) * @uses self::transactionBeginningHandler() * @uses self::transactionCommittedHandler() * @uses self::transactionRolledBackHandler() - * @uses self::httpClientRequestSendingHandler() - * @uses self::httpClientResponseReceivedHandler() - * @uses self::httpClientConnectionFailedHandler() */ public function subscribe(Dispatcher $dispatcher): void { @@ -282,64 +265,6 @@ protected function transactionRolledBackHandler(DatabaseEvents\TransactionRolled } } - protected function httpClientRequestSendingHandler(HttpClientEvents\RequestSending $event): void - { - if (!$this->traceHttpClientRequests) { - return; - } - - $parentSpan = SentrySdk::getCurrentHub()->getSpan(); - - // If there is no tracing span active there is no need to handle the event - if ($parentSpan === null) { - return; - } - - $context = new SpanContext; - - $fullUri = $this->getFullUri($event->request->url()); - $partialUri = $this->getPartialUri($fullUri); - - $context->setOp('http.client'); - $context->setDescription($event->request->method() . ' ' . $partialUri); - $context->setData([ - 'url' => $partialUri, - 'http.request.method' => $event->request->method(), - 'http.query' => $fullUri->getQuery(), - 'http.fragment' => $fullUri->getFragment(), - ]); - - $this->pushSpan($parentSpan->startChild($context)); - } - - protected function httpClientResponseReceivedHandler(HttpClientEvents\ResponseReceived $event): void - { - if (!$this->traceHttpClientRequests) { - return; - } - - $span = $this->popSpan(); - - if ($span !== null) { - $span->finish(); - $span->setHttpStatus($event->response->status()); - } - } - - protected function httpClientConnectionFailedHandler(HttpClientEvents\ConnectionFailed $event): void - { - if (!$this->traceHttpClientRequests) { - return; - } - - $span = $this->popSpan(); - - if ($span !== null) { - $span->finish(); - $span->setStatus(SpanStatus::internalError()); - } - } - private function pushSpan(Span $span): void { $hub = SentrySdk::getCurrentHub(); diff --git a/src/Sentry/Laravel/Util/WorksWithUris.php b/src/Sentry/Laravel/Util/WorksWithUris.php deleted file mode 100644 index 84937ee5..00000000 --- a/src/Sentry/Laravel/Util/WorksWithUris.php +++ /dev/null @@ -1,38 +0,0 @@ - $uri->getScheme(), - 'host' => $uri->getHost(), - 'port' => $uri->getPort(), - 'path' => $uri->getPath(), - ]); - } -} diff --git a/test/Sentry/Features/HttpClientIntegrationTest.php b/test/Sentry/Features/HttpClientIntegrationTest.php index 17828ca5..edd61698 100644 --- a/test/Sentry/Features/HttpClientIntegrationTest.php +++ b/test/Sentry/Features/HttpClientIntegrationTest.php @@ -7,7 +7,9 @@ use Illuminate\Http\Client\Events\ResponseReceived; use Illuminate\Http\Client\Request; use Illuminate\Http\Client\Response; +use Illuminate\Support\Facades\Http; use Sentry\Laravel\Tests\TestCase; +use Sentry\Tracing\SpanStatus; class HttpClientIntegrationTest extends TestCase { @@ -50,4 +52,102 @@ public function testHttpClientBreadcrumbDoesntConsumeBodyStream(): void $this->assertEquals('request', $request->toPsrRequest()->getBody()->getContents()); $this->assertEquals('response', $response->toPsrResponse()->getBody()->getContents()); } + + public function testHttpClientBreadcrumbIsNotRecordedWhenDisabled(): void + { + $this->resetApplicationWithConfig([ + 'sentry.breadcrumbs.http_client_requests' => false, + ]); + + $this->dispatchLaravelEvent(new ResponseReceived( + new Request(new PsrRequest('GET', 'https://example.com', [], 'request')), + new Response(new PsrResponse(200, [], 'response')) + )); + + $this->assertEmpty($this->getCurrentSentryBreadcrumbs()); + } + + public function testHttpClientSpanIsRecorded(): void + { + $transaction = $this->startTransaction(); + + $client = Http::fake(); + + $client->get('https://example.com'); + + /** @var \Sentry\Tracing\Span $span */ + $span = last($transaction->getSpanRecorder()->getSpans()); + + $this->assertEquals('http.client', $span->getOp()); + $this->assertEquals('GET https://example.com', $span->getDescription()); + } + + public function testHttpClientSpanIsRecordedWithCorrectResult(): void + { + $transaction = $this->startTransaction(); + + $client = Http::fake([ + 'example.com/success' => Http::response('OK'), + 'example.com/error' => Http::response('Internal Server Error', 500), + ]); + + $client->get('https://example.com/success'); + + /** @var \Sentry\Tracing\Span $span */ + $span = last($transaction->getSpanRecorder()->getSpans()); + + $this->assertEquals('http.client', $span->getOp()); + $this->assertEquals(SpanStatus::ok(), $span->getStatus()); + + $client->get('https://example.com/error'); + + /** @var \Sentry\Tracing\Span $span */ + $span = last($transaction->getSpanRecorder()->getSpans()); + + $this->assertEquals('http.client', $span->getOp()); + $this->assertEquals(SpanStatus::internalError(), $span->getStatus()); + } + + public function testHttpClientSpanIsNotRecordedWhenDisabled(): void + { + $this->resetApplicationWithConfig([ + 'sentry.tracing.http_client_requests' => false, + ]); + + $transaction = $this->startTransaction(); + + $client = Http::fake(); + + $client->get('https://example.com'); + + /** @var \Sentry\Tracing\Span $span */ + $span = last($transaction->getSpanRecorder()->getSpans()); + + $this->assertNotEquals('http.client', $span->getOp()); + } + + public function testHttpClientRequestTracingHeadersAreAttached(): void + { + if (!method_exists(Http::class, 'globalRequestMiddleware')) { + $this->markTestSkipped('The `globalRequestMiddleware` functionality we rely on was introduced in Laravel 10.14'); + } + + $this->resetApplicationWithConfig([ + 'sentry.trace_propagation_targets' => ['example.com'], + ]); + + $client = Http::fake(); + + $client->get('https://example.com'); + + Http::assertSent(function (Request $request) { + return $request->hasHeader('baggage') && $request->hasHeader('sentry-trace'); + }); + + $client->get('https://no-headers.example.com'); + + Http::assertSent(function (Request $request) { + return !$request->hasHeader('baggage') && !$request->hasHeader('sentry-trace'); + }); + } }