diff --git a/config/sentry.php b/config/sentry.php index eff85421..e248cbdd 100644 --- a/config/sentry.php +++ b/config/sentry.php @@ -109,6 +109,12 @@ // Capture HTTP client requests as spans 'http_client_requests' => env('SENTRY_TRACE_HTTP_CLIENT_REQUESTS_ENABLED', true), + // Capture where the HTTP client request originated from on the HTTP client request spans + 'http_client_requests_origin' => env('SENTRY_TRACE_HTTP_CLIENT_REQUESTS_ORIGIN_ENABLED', true), + + // Define a threshold in milliseconds for HTTP client requests to resolve their origin + 'http_client_requests_origin_threshold_ms' => env('SENTRY_TRACE_HTTP_CLIENT_REQUESTS_ORIGIN_THRESHOLD_MS', 250), + // Capture Laravel cache events (hits, writes etc.) as spans 'cache' => env('SENTRY_TRACE_CACHE_ENABLED', true), diff --git a/src/Sentry/Laravel/Features/HttpClientIntegration.php b/src/Sentry/Laravel/Features/HttpClientIntegration.php index 2f1f4862..21a6321a 100644 --- a/src/Sentry/Laravel/Features/HttpClientIntegration.php +++ b/src/Sentry/Laravel/Features/HttpClientIntegration.php @@ -11,6 +11,7 @@ use Psr\Http\Message\RequestInterface; use Psr\Http\Message\UriInterface; use Sentry\Breadcrumb; +use Sentry\Laravel\Features\Concerns\ResolvesEventOrigin; use Sentry\Laravel\Features\Concerns\TracksPushedScopesAndSpans; use Sentry\Laravel\Integration; use Sentry\SentrySdk; @@ -21,10 +22,25 @@ class HttpClientIntegration extends Feature { + use ResolvesEventOrigin; use TracksPushedScopesAndSpans; private const FEATURE_KEY = 'http_client_requests'; + /** + * Indicates if we should trace the origin of the HTTP client requests. + * + * @var bool|null + */ + private $traceHttpClientRequestsOrigin; + + /** + * The threshold in milliseconds for HTTP client requests to resolve their origin. + * + * @var int|null + */ + private $traceHttpClientRequestsOriginThresholdMs; + public function isApplicable(): bool { return $this->isTracingFeatureEnabled(self::FEATURE_KEY) @@ -101,6 +117,19 @@ public function handleResponseReceivedHandlerForTracing(ResponseReceived $event) 'http.response.status_code' => $event->response->status(), 'http.response.body.size' => $event->response->toPsrResponse()->getBody()->getSize(), ])); + + if ($this->shouldTraceHttpClientRequestsOrigin()) { + $duration = ($span->getEndTimestamp() ?? microtime(true)) - $span->getStartTimestamp(); + $durationMs = $duration * 1000; + + if ($durationMs >= $this->getHttpClientRequestsOriginThresholdMs()) { + $requestOrigin = $this->resolveEventOrigin(); + if ($requestOrigin !== null) { + $span->setData(array_merge($span->getData(), $requestOrigin)); + } + } + } + $span->setHttpStatus($event->response->status()); $span->finish(); } @@ -203,4 +232,32 @@ private function shouldAttachTracingHeaders(RequestInterface $request): bool return $sdkOptions->getTracePropagationTargets() === null || in_array($request->getUri()->getHost(), $sdkOptions->getTracePropagationTargets()); } + + /** + * Indicates if we should trace the origin of the HTTP client requests. + */ + private function shouldTraceHttpClientRequestsOrigin(): bool + { + if ($this->traceHttpClientRequestsOrigin === null) { + $tracingConfig = $this->getUserConfig()['tracing'] ?? []; + + $this->traceHttpClientRequestsOrigin = ($tracingConfig['http_client_requests_origin'] ?? true) === true; + } + + return $this->traceHttpClientRequestsOrigin; + } + + /** + * Get the threshold in milliseconds for HTTP client requests to resolve their origin. + */ + private function getHttpClientRequestsOriginThresholdMs(): int + { + if ($this->traceHttpClientRequestsOriginThresholdMs === null) { + $tracingConfig = $this->getUserConfig()['tracing'] ?? []; + + $this->traceHttpClientRequestsOriginThresholdMs = $tracingConfig['http_client_requests_origin_threshold_ms'] ?? 250; + } + + return $this->traceHttpClientRequestsOriginThresholdMs; + } } diff --git a/test/Sentry/Features/HttpClientIntegrationTest.php b/test/Sentry/Features/HttpClientIntegrationTest.php index 63907c8d..043ae1ec 100644 --- a/test/Sentry/Features/HttpClientIntegrationTest.php +++ b/test/Sentry/Features/HttpClientIntegrationTest.php @@ -151,4 +151,88 @@ public function testHttpClientRequestTracingHeadersAreAttached(): void return !$request->hasHeader('baggage') && !$request->hasHeader('sentry-trace'); }); } + + public function testHttpClientOriginIsResolvedWhenEnabled(): void + { + $this->resetApplicationWithConfig([ + 'sentry.tracing.http_client_requests_origin' => true, + 'sentry.tracing.http_client_requests_origin_threshold_ms' => 0, + ]); + + $transaction = $this->startTransaction(); + + $client = Http::fake(); + + $client->get('https://example.com'); + + /** @var \Sentry\Tracing\Span $span */ + $span = last($transaction->getSpanRecorder()->getSpans()); + + $this->assertArrayHasKey('code.filepath', $span->getData()); + $this->assertArrayHasKey('code.lineno', $span->getData()); + } + + public function testHttpClientOriginIsNotResolvedWhenDisabled(): void + { + $this->resetApplicationWithConfig([ + 'sentry.tracing.http_client_requests_origin' => false, + ]); + + $transaction = $this->startTransaction(); + + $client = Http::fake(); + + $client->get('https://example.com'); + + /** @var \Sentry\Tracing\Span $span */ + $span = last($transaction->getSpanRecorder()->getSpans()); + + $this->assertArrayNotHasKey('code.filepath', $span->getData()); + $this->assertArrayNotHasKey('code.lineno', $span->getData()); + } + + public function testHttpClientOriginIsResolvedWhenOverThreshold(): void + { + $this->resetApplicationWithConfig([ + 'sentry.tracing.http_client_requests_origin' => true, + 'sentry.tracing.http_client_requests_origin_threshold_ms' => 10, + ]); + + $transaction = $this->startTransaction(); + + $client = Http::fake([ + 'slow.example.com' => function () { + usleep(20000); // 20ms delay + return Http::response('OK'); + }, + ]); + + $client->get('https://slow.example.com'); + + /** @var \Sentry\Tracing\Span $span */ + $span = last($transaction->getSpanRecorder()->getSpans()); + + $this->assertArrayHasKey('code.filepath', $span->getData()); + $this->assertArrayHasKey('code.lineno', $span->getData()); + } + + public function testHttpClientOriginIsNotResolvedWhenUnderThreshold(): void + { + $this->resetApplicationWithConfig([ + 'sentry.tracing.http_client_requests_origin' => true, + 'sentry.tracing.http_client_requests_origin_threshold_ms' => 1000, + ]); + + $transaction = $this->startTransaction(); + + $client = Http::fake(); + + $client->get('https://example.com'); + + /** @var \Sentry\Tracing\Span $span */ + $span = last($transaction->getSpanRecorder()->getSpans()); + + $this->assertArrayNotHasKey('code.filepath', $span->getData()); + $this->assertArrayNotHasKey('code.lineno', $span->getData()); + } }