|
1 | 1 | """Tests for aignostics_foundry_core.sentry.""" |
2 | 2 |
|
3 | 3 | import json |
4 | | -from collections.abc import Generator |
| 4 | +from collections.abc import Callable, Generator |
5 | 5 | from typing import TYPE_CHECKING, Any |
6 | 6 | from unittest.mock import patch |
7 | 7 |
|
8 | 8 | import httpx |
| 9 | +import httpx2 |
9 | 10 | import pytest |
10 | 11 | import sentry_sdk |
11 | 12 | from fastapi import FastAPI |
|
52 | 53 | _HTTP_QUERY_KEY = "http.query" |
53 | 54 | _HTTP_FRAGMENT_KEY = "http.fragment" |
54 | 55 | _HTTPLIB_CATEGORY = "httplib" |
| 56 | +# The env var form that README.md documents: a JSON list with one regex, internal\.example\.com |
| 57 | +_INTERNAL_TRACE_TARGETS_JSON = r'["internal\\.example\\.com"]' |
| 58 | +_INTERNAL_URL = "https://internal.example.com/" |
| 59 | +_PARTNER_URL = "https://partner.example.org/" |
| 60 | +_SENTRY_TRACE_HEADER = "sentry-trace" |
| 61 | +_BAGGAGE_HEADER = "baggage" |
55 | 62 |
|
56 | 63 |
|
57 | 64 | class _CapturingTransport(Transport): |
@@ -155,6 +162,54 @@ def _get_signed_blob_url() -> None: |
155 | 162 | client.get(_SIGNED_BLOB_URL) |
156 | 163 |
|
157 | 164 |
|
| 165 | +def _httpx_request_headers(url: str) -> dict[str, str]: |
| 166 | + """Send a GET to *url* through an :class:`httpx.Client` on a mock transport, inside a Sentry transaction. |
| 167 | +
|
| 168 | + The Sentry httpx integration adds trace headers only to requests inside a transaction, as in a |
| 169 | + request handler of a service. |
| 170 | +
|
| 171 | + Returns: |
| 172 | + dict[str, str]: The headers of the request that reached the transport. |
| 173 | + """ |
| 174 | + sent: list[httpx.Request] = [] |
| 175 | + |
| 176 | + def handle(request: httpx.Request) -> httpx.Response: |
| 177 | + sent.append(request) |
| 178 | + return httpx.Response(200) |
| 179 | + |
| 180 | + with ( |
| 181 | + sentry_sdk.start_transaction(name=_PROBE_MESSAGE), |
| 182 | + httpx.Client(transport=httpx.MockTransport(handle)) as client, |
| 183 | + ): |
| 184 | + client.get(url) |
| 185 | + (request,) = sent |
| 186 | + return dict(request.headers) |
| 187 | + |
| 188 | + |
| 189 | +def _httpx2_request_headers(url: str) -> dict[str, str]: |
| 190 | + """Send a GET to *url* through an :class:`httpx2.Client` on a mock transport, inside a Sentry transaction. |
| 191 | +
|
| 192 | + The Sentry httpx2 integration adds trace headers only to requests inside a transaction, as in a |
| 193 | + request handler of a service. |
| 194 | +
|
| 195 | + Returns: |
| 196 | + dict[str, str]: The headers of the request that reached the transport. |
| 197 | + """ |
| 198 | + sent: list[httpx2.Request] = [] |
| 199 | + |
| 200 | + def handle(request: httpx2.Request) -> httpx2.Response: |
| 201 | + sent.append(request) |
| 202 | + return httpx2.Response(200) |
| 203 | + |
| 204 | + with ( |
| 205 | + sentry_sdk.start_transaction(name=_PROBE_MESSAGE), |
| 206 | + httpx2.Client(transport=httpx2.MockTransport(handle)) as client, |
| 207 | + ): |
| 208 | + client.get(url) |
| 209 | + (request,) = sent |
| 210 | + return dict(request.headers) |
| 211 | + |
| 212 | + |
158 | 213 | def _breadcrumbs(event: dict[str, Any], category: str) -> list[dict[str, Any]]: |
159 | 214 | """Return the breadcrumbs of *event* that have *category*.""" |
160 | 215 | return [crumb for crumb in event["breadcrumbs"]["values"] if crumb.get("category") == category] |
@@ -355,6 +410,41 @@ def test_transaction_span_has_no_query_string( |
355 | 410 | assert all(_HTTP_QUERY_KEY not in span["data"] for span in http_spans) |
356 | 411 | assert _SIGNED_URL_MARKER not in json.dumps(transaction) |
357 | 412 |
|
| 413 | + @pytest.mark.parametrize("send_get", [_httpx_request_headers, _httpx2_request_headers], ids=["httpx", "httpx2"]) |
| 414 | + def test_outbound_request_has_no_trace_headers_by_default( |
| 415 | + self, sentry_capture: SentryCapture, send_get: Callable[[str], dict[str, str]] |
| 416 | + ) -> None: |
| 417 | + """An outbound request gets no ``sentry-trace`` or ``baggage`` header at default settings.""" |
| 418 | + sentry_capture.start() |
| 419 | + |
| 420 | + headers = send_get(_PARTNER_URL) |
| 421 | + |
| 422 | + assert _SENTRY_TRACE_HEADER not in headers |
| 423 | + assert _BAGGAGE_HEADER not in headers |
| 424 | + |
| 425 | + def test_outbound_request_to_allowed_host_has_trace_headers( |
| 426 | + self, sentry_capture: SentryCapture, monkeypatch: pytest.MonkeyPatch |
| 427 | + ) -> None: |
| 428 | + """A request to a host in TRACE_PROPAGATION_TARGETS gets the ``sentry-trace`` header.""" |
| 429 | + monkeypatch.setenv(f"{_SENTRY_PREFIX}TRACE_PROPAGATION_TARGETS", _INTERNAL_TRACE_TARGETS_JSON) |
| 430 | + sentry_capture.start() |
| 431 | + |
| 432 | + headers = _httpx_request_headers(_INTERNAL_URL) |
| 433 | + |
| 434 | + assert _SENTRY_TRACE_HEADER in headers |
| 435 | + |
| 436 | + def test_outbound_request_to_other_host_has_no_trace_headers_when_targets_set( |
| 437 | + self, sentry_capture: SentryCapture, monkeypatch: pytest.MonkeyPatch |
| 438 | + ) -> None: |
| 439 | + """A request to a host outside TRACE_PROPAGATION_TARGETS gets no ``sentry-trace`` or ``baggage`` header.""" |
| 440 | + monkeypatch.setenv(f"{_SENTRY_PREFIX}TRACE_PROPAGATION_TARGETS", _INTERNAL_TRACE_TARGETS_JSON) |
| 441 | + sentry_capture.start() |
| 442 | + |
| 443 | + headers = _httpx_request_headers(_PARTNER_URL) |
| 444 | + |
| 445 | + assert _SENTRY_TRACE_HEADER not in headers |
| 446 | + assert _BAGGAGE_HEADER not in headers |
| 447 | + |
358 | 448 |
|
359 | 449 | @pytest.mark.integration |
360 | 450 | class TestSentrySettingsDsnValidation: |
|
0 commit comments