Skip to content

Commit c1cf0fe

Browse files
authored
Set level based on status code for HTTP client breadcrumbs (#4004)
- add logic to `maybe_create_breadcrumbs_from_span` to set the `level` of the breadcrumb to `warning` for the client error range (4xx) and to `error` for server errors (5xx) - add functionality to the simple HTTP server that we use in some tests to respond with a specific error code - we were (and are) still "using" `responses` in multiple places, but they're not actually active (the `activate` decorator is missing) and we're making actual requests outside -- we should clean this up - we also can't use `responses` for stdlib/requests tests since they patch something that we patch - add `httpx`, `stdlib`, `requests`, `aiohttp` tests for the new behavior - restrict the `requests` tests to 3.7+ since in 3.6, the span is finished before the HTTP status is set for some reason... Closes #4000
1 parent 5fb97a9 commit c1cf0fe

File tree

6 files changed

+235
-12
lines changed

6 files changed

+235
-12
lines changed

sentry_sdk/tracing_utils.py

+16-2
Original file line numberDiff line numberDiff line change
@@ -156,13 +156,27 @@ def record_sql_queries(
156156

157157
def maybe_create_breadcrumbs_from_span(scope, span):
158158
# type: (sentry_sdk.Scope, sentry_sdk.tracing.Span) -> None
159-
160159
if span.op == OP.DB_REDIS:
161160
scope.add_breadcrumb(
162161
message=span.description, type="redis", category="redis", data=span._tags
163162
)
163+
164164
elif span.op == OP.HTTP_CLIENT:
165-
scope.add_breadcrumb(type="http", category="httplib", data=span._data)
165+
level = None
166+
status_code = span._data.get(SPANDATA.HTTP_STATUS_CODE)
167+
if status_code:
168+
if 500 <= status_code <= 599:
169+
level = "error"
170+
elif 400 <= status_code <= 499:
171+
level = "warning"
172+
173+
if level:
174+
scope.add_breadcrumb(
175+
type="http", category="httplib", data=span._data, level=level
176+
)
177+
else:
178+
scope.add_breadcrumb(type="http", category="httplib", data=span._data)
179+
166180
elif span.op == "subprocess":
167181
scope.add_breadcrumb(
168182
type="subprocess",

tests/conftest.py

+8-2
Original file line numberDiff line numberDiff line change
@@ -587,8 +587,14 @@ def suppress_deprecation_warnings():
587587

588588
class MockServerRequestHandler(BaseHTTPRequestHandler):
589589
def do_GET(self): # noqa: N802
590-
# Process an HTTP GET request and return a response with an HTTP 200 status.
591-
self.send_response(200)
590+
# Process an HTTP GET request and return a response.
591+
# If the path ends with /status/<number>, return status code <number>.
592+
# Otherwise return a 200 response.
593+
code = 200
594+
if "/status/" in self.path:
595+
code = int(self.path[-3:])
596+
597+
self.send_response(code)
592598
self.end_headers()
593599
return
594600

tests/integrations/aiohttp/test_aiohttp.py

+55
Original file line numberDiff line numberDiff line change
@@ -525,6 +525,61 @@ async def handler(request):
525525
)
526526

527527

528+
@pytest.mark.parametrize(
529+
"status_code,level",
530+
[
531+
(200, None),
532+
(301, None),
533+
(403, "warning"),
534+
(405, "warning"),
535+
(500, "error"),
536+
],
537+
)
538+
@pytest.mark.asyncio
539+
async def test_crumb_capture_client_error(
540+
sentry_init,
541+
aiohttp_raw_server,
542+
aiohttp_client,
543+
event_loop,
544+
capture_events,
545+
status_code,
546+
level,
547+
):
548+
sentry_init(integrations=[AioHttpIntegration()])
549+
550+
async def handler(request):
551+
return web.Response(status=status_code)
552+
553+
raw_server = await aiohttp_raw_server(handler)
554+
555+
with start_transaction():
556+
events = capture_events()
557+
558+
client = await aiohttp_client(raw_server)
559+
resp = await client.get("/")
560+
assert resp.status == status_code
561+
capture_message("Testing!")
562+
563+
(event,) = events
564+
565+
crumb = event["breadcrumbs"]["values"][0]
566+
assert crumb["type"] == "http"
567+
if level is None:
568+
assert "level" not in crumb
569+
else:
570+
assert crumb["level"] == level
571+
assert crumb["category"] == "httplib"
572+
assert crumb["data"] == ApproxDict(
573+
{
574+
"url": "http://127.0.0.1:{}/".format(raw_server.port),
575+
"http.fragment": "",
576+
"http.method": "GET",
577+
"http.query": "",
578+
"http.response.status_code": status_code,
579+
}
580+
)
581+
582+
528583
@pytest.mark.asyncio
529584
async def test_outgoing_trace_headers(sentry_init, aiohttp_raw_server, aiohttp_client):
530585
sentry_init(

tests/integrations/httpx/test_httpx.py

+58
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,64 @@ def before_breadcrumb(crumb, hint):
5757
)
5858

5959

60+
@pytest.mark.parametrize(
61+
"httpx_client",
62+
(httpx.Client(), httpx.AsyncClient()),
63+
)
64+
@pytest.mark.parametrize(
65+
"status_code,level",
66+
[
67+
(200, None),
68+
(301, None),
69+
(403, "warning"),
70+
(405, "warning"),
71+
(500, "error"),
72+
],
73+
)
74+
def test_crumb_capture_client_error(
75+
sentry_init, capture_events, httpx_client, httpx_mock, status_code, level
76+
):
77+
httpx_mock.add_response(status_code=status_code)
78+
79+
sentry_init(integrations=[HttpxIntegration()])
80+
81+
url = "http://example.com/"
82+
83+
with start_transaction():
84+
events = capture_events()
85+
86+
if asyncio.iscoroutinefunction(httpx_client.get):
87+
response = asyncio.get_event_loop().run_until_complete(
88+
httpx_client.get(url)
89+
)
90+
else:
91+
response = httpx_client.get(url)
92+
93+
assert response.status_code == status_code
94+
capture_message("Testing!")
95+
96+
(event,) = events
97+
98+
crumb = event["breadcrumbs"]["values"][0]
99+
assert crumb["type"] == "http"
100+
assert crumb["category"] == "httplib"
101+
102+
if level is None:
103+
assert "level" not in crumb
104+
else:
105+
assert crumb["level"] == level
106+
107+
assert crumb["data"] == ApproxDict(
108+
{
109+
"url": url,
110+
SPANDATA.HTTP_METHOD: "GET",
111+
SPANDATA.HTTP_FRAGMENT: "",
112+
SPANDATA.HTTP_QUERY: "",
113+
SPANDATA.HTTP_STATUS_CODE: status_code,
114+
}
115+
)
116+
117+
60118
@pytest.mark.parametrize(
61119
"httpx_client",
62120
(httpx.Client(), httpx.AsyncClient()),

tests/integrations/requests/test_requests.py

+53-8
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,77 @@
1+
import sys
12
from unittest import mock
23

34
import pytest
45
import requests
5-
import responses
66

77
from sentry_sdk import capture_message
88
from sentry_sdk.consts import SPANDATA
99
from sentry_sdk.integrations.stdlib import StdlibIntegration
10-
from tests.conftest import ApproxDict
10+
from tests.conftest import ApproxDict, create_mock_http_server
11+
12+
PORT = create_mock_http_server()
1113

1214

1315
def test_crumb_capture(sentry_init, capture_events):
1416
sentry_init(integrations=[StdlibIntegration()])
17+
events = capture_events()
1518

16-
url = "http://example.com/"
17-
responses.add(responses.GET, url, status=200)
19+
url = f"http://localhost:{PORT}/hello-world" # noqa:E231
20+
response = requests.get(url)
21+
capture_message("Testing!")
22+
23+
(event,) = events
24+
(crumb,) = event["breadcrumbs"]["values"]
25+
assert crumb["type"] == "http"
26+
assert crumb["category"] == "httplib"
27+
assert crumb["data"] == ApproxDict(
28+
{
29+
"url": url,
30+
SPANDATA.HTTP_METHOD: "GET",
31+
SPANDATA.HTTP_FRAGMENT: "",
32+
SPANDATA.HTTP_QUERY: "",
33+
SPANDATA.HTTP_STATUS_CODE: response.status_code,
34+
"reason": response.reason,
35+
}
36+
)
37+
38+
39+
@pytest.mark.skipif(
40+
sys.version_info < (3, 7),
41+
reason="The response status is not set on the span early enough in 3.6",
42+
)
43+
@pytest.mark.parametrize(
44+
"status_code,level",
45+
[
46+
(200, None),
47+
(301, None),
48+
(403, "warning"),
49+
(405, "warning"),
50+
(500, "error"),
51+
],
52+
)
53+
def test_crumb_capture_client_error(sentry_init, capture_events, status_code, level):
54+
sentry_init(integrations=[StdlibIntegration()])
1855

1956
events = capture_events()
2057

58+
url = f"http://localhost:{PORT}/status/{status_code}" # noqa:E231
2159
response = requests.get(url)
60+
61+
assert response.status_code == status_code
62+
2263
capture_message("Testing!")
2364

2465
(event,) = events
2566
(crumb,) = event["breadcrumbs"]["values"]
2667
assert crumb["type"] == "http"
2768
assert crumb["category"] == "httplib"
69+
70+
if level is None:
71+
assert "level" not in crumb
72+
else:
73+
assert crumb["level"] == level
74+
2875
assert crumb["data"] == ApproxDict(
2976
{
3077
"url": url,
@@ -41,11 +88,10 @@ def test_crumb_capture(sentry_init, capture_events):
4188
def test_omit_url_data_if_parsing_fails(sentry_init, capture_events):
4289
sentry_init(integrations=[StdlibIntegration()])
4390

44-
url = "https://example.com"
45-
responses.add(responses.GET, url, status=200)
46-
4791
events = capture_events()
4892

93+
url = f"http://localhost:{PORT}/ok" # noqa:E231
94+
4995
with mock.patch(
5096
"sentry_sdk.integrations.stdlib.parse_url",
5197
side_effect=ValueError,
@@ -63,7 +109,6 @@ def test_omit_url_data_if_parsing_fails(sentry_init, capture_events):
63109
# no url related data
64110
}
65111
)
66-
67112
assert "url" not in event["breadcrumbs"]["values"][0]["data"]
68113
assert SPANDATA.HTTP_FRAGMENT not in event["breadcrumbs"]["values"][0]["data"]
69114
assert SPANDATA.HTTP_QUERY not in event["breadcrumbs"]["values"][0]["data"]

tests/integrations/stdlib/test_httplib.py

+45
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import random
22
from http.client import HTTPConnection, HTTPSConnection
33
from socket import SocketIO
4+
from urllib.error import HTTPError
45
from urllib.request import urlopen
56
from unittest import mock
67

@@ -42,6 +43,50 @@ def test_crumb_capture(sentry_init, capture_events):
4243
)
4344

4445

46+
@pytest.mark.parametrize(
47+
"status_code,level",
48+
[
49+
(200, None),
50+
(301, None),
51+
(403, "warning"),
52+
(405, "warning"),
53+
(500, "error"),
54+
],
55+
)
56+
def test_crumb_capture_client_error(sentry_init, capture_events, status_code, level):
57+
sentry_init(integrations=[StdlibIntegration()])
58+
events = capture_events()
59+
60+
url = f"http://localhost:{PORT}/status/{status_code}" # noqa:E231
61+
try:
62+
urlopen(url)
63+
except HTTPError:
64+
pass
65+
66+
capture_message("Testing!")
67+
68+
(event,) = events
69+
(crumb,) = event["breadcrumbs"]["values"]
70+
71+
assert crumb["type"] == "http"
72+
assert crumb["category"] == "httplib"
73+
74+
if level is None:
75+
assert "level" not in crumb
76+
else:
77+
assert crumb["level"] == level
78+
79+
assert crumb["data"] == ApproxDict(
80+
{
81+
"url": url,
82+
SPANDATA.HTTP_METHOD: "GET",
83+
SPANDATA.HTTP_STATUS_CODE: status_code,
84+
SPANDATA.HTTP_FRAGMENT: "",
85+
SPANDATA.HTTP_QUERY: "",
86+
}
87+
)
88+
89+
4590
def test_crumb_capture_hint(sentry_init, capture_events):
4691
def before_breadcrumb(crumb, hint):
4792
crumb["data"]["extra"] = "foo"

0 commit comments

Comments
 (0)