Skip to content

Commit 4e4ea83

Browse files
authored
ref(boto3): Move crumbs to integration (#7165)
Set boto3 breadcrumbs in the integration directly, without relying on `maybe_create_breadcrumbs_from_span`. Also, add breadcrumb tests to `test_s3.py` and remove the `maybe_create_breadcrumbs_from_span` util now that nothing needs it anymore. #### Issues Closes #7067
1 parent 0f0cd1f commit 4e4ea83

4 files changed

Lines changed: 140 additions & 60 deletions

File tree

sentry_sdk/integrations/boto3.py

Lines changed: 62 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from sentry_sdk.scope import should_send_default_pii
88
from sentry_sdk.traces import StreamedSpan
99
from sentry_sdk.tracing import Span
10-
from sentry_sdk.tracing_utils import has_span_streaming_enabled
10+
from sentry_sdk.tracing_utils import add_http_breadcrumb, has_span_streaming_enabled
1111
from sentry_sdk.utils import (
1212
capture_internal_exceptions,
1313
parse_url,
@@ -64,54 +64,83 @@ def _sentry_request_created(
6464
if client.get_integration(Boto3Integration) is None:
6565
return
6666

67+
parsed_url = None
68+
if request.url is not None:
69+
with capture_internal_exceptions():
70+
parsed_url = parse_url(request.url, sanitize=False)
71+
72+
breadcrumb: "dict[str, Any]" = {}
73+
6774
is_span_streaming_enabled = has_span_streaming_enabled(client.options)
68-
span: "Union[Span, StreamedSpan]"
75+
span: "Union[Span, StreamedSpan, None]" = None
6976
if is_span_streaming_enabled:
70-
if sentry_sdk.traces.get_current_span() is None:
71-
return
72-
span = sentry_sdk.traces.start_span(
73-
name=description,
74-
attributes={
75-
"sentry.op": OP.HTTP_CLIENT,
76-
"sentry.origin": Boto3Integration.origin,
77-
SPANDATA.RPC_METHOD: f"{service_id}/{operation_name}",
78-
},
79-
)
80-
if request.url is not None and should_send_default_pii():
81-
with capture_internal_exceptions():
82-
parsed_url = parse_url(request.url, sanitize=False)
83-
span.set_attribute(SPANDATA.URL_FULL, parsed_url.url)
84-
span.set_attribute(SPANDATA.URL_QUERY, parsed_url.query)
85-
span.set_attribute(SPANDATA.URL_FRAGMENT, parsed_url.fragment)
77+
if parsed_url and should_send_default_pii():
78+
breadcrumb.update(
79+
{
80+
SPANDATA.URL_FULL: parsed_url.url,
81+
SPANDATA.URL_QUERY: parsed_url.query,
82+
SPANDATA.URL_FRAGMENT: parsed_url.fragment,
83+
}
84+
)
8685

8786
if request.method is not None:
88-
span.set_attribute(SPANDATA.HTTP_REQUEST_METHOD, request.method)
87+
breadcrumb[SPANDATA.HTTP_REQUEST_METHOD] = request.method
88+
89+
if sentry_sdk.traces.get_current_span() is not None:
90+
span = sentry_sdk.traces.start_span(
91+
name=description,
92+
attributes={
93+
"sentry.op": OP.HTTP_CLIENT,
94+
"sentry.origin": Boto3Integration.origin,
95+
SPANDATA.RPC_METHOD: f"{service_id}/{operation_name}",
96+
},
97+
)
98+
if parsed_url and should_send_default_pii():
99+
span.set_attributes(
100+
{
101+
SPANDATA.URL_FULL: parsed_url.url,
102+
SPANDATA.URL_QUERY: parsed_url.query,
103+
SPANDATA.URL_FRAGMENT: parsed_url.fragment,
104+
}
105+
)
106+
107+
if request.method is not None:
108+
span.set_attribute(SPANDATA.HTTP_REQUEST_METHOD, request.method)
89109
else:
90110
span = sentry_sdk.start_span(
91111
op=OP.HTTP_CLIENT,
92112
name=description,
93113
origin=Boto3Integration.origin,
94114
)
95115

96-
if request.url is not None:
97-
with capture_internal_exceptions():
98-
parsed_url = parse_url(request.url, sanitize=False)
99-
span.set_data("aws.request.url", parsed_url.url)
100-
span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query)
101-
span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment)
116+
if parsed_url:
117+
span.set_data("aws.request.url", parsed_url.url)
118+
span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query)
119+
span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment)
120+
breadcrumb.update(
121+
{
122+
"aws.request.url": parsed_url.url,
123+
SPANDATA.HTTP_QUERY: parsed_url.query,
124+
SPANDATA.HTTP_FRAGMENT: parsed_url.fragment,
125+
}
126+
)
102127

103128
span.set_tag("aws.service_id", service_id.hyphenize())
104129
span.set_tag("aws.operation_name", operation_name)
105130
if request.method is not None:
106131
span.set_data(SPANDATA.HTTP_METHOD, request.method)
132+
breadcrumb[SPANDATA.HTTP_METHOD] = request.method
107133

108-
# We do it in order for subsequent http calls/retries be
109-
# attached to this span.
110-
span.__enter__()
134+
# We do it in order for subsequent http calls/retries be
135+
# attached to this span.
136+
span.__enter__()
111137

112-
# request.context is an open-ended data-structure
113-
# where we can add anything useful in request life cycle.
114-
request.context["_sentrysdk_span"] = span
138+
add_http_breadcrumb(None, breadcrumb)
139+
140+
if span is not None:
141+
# request.context is an open-ended data-structure
142+
# where we can add anything useful in request life cycle.
143+
request.context["_sentrysdk_span"] = span
115144

116145

117146
def _sentry_after_call(
@@ -122,6 +151,7 @@ def _sentry_after_call(
122151
# Span could be absent if the integration is disabled.
123152
if span is None:
124153
return
154+
125155
span.__exit__(None, None, None)
126156

127157
body = parsed.get("Body")
@@ -186,4 +216,5 @@ def _sentry_after_call_error(
186216
# Span could be absent if the integration is disabled.
187217
if span is None:
188218
return
219+
189220
span.__exit__(type(exception), exception, None)

sentry_sdk/tracing.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -700,8 +700,6 @@ def finish(
700700
if has_ai_op or is_ai_span_op:
701701
self.set_data("gen_ai.conversation.id", conversation_id)
702702

703-
maybe_create_breadcrumbs_from_span(scope, self)
704-
705703
return None
706704

707705
def to_json(self) -> "Dict[str, Any]":
@@ -1495,5 +1493,4 @@ def calculate_interest_rate(amount, rate, years):
14951493
extract_sentrytrace_data,
14961494
has_span_streaming_enabled,
14971495
has_tracing_enabled,
1498-
maybe_create_breadcrumbs_from_span,
14991496
)

sentry_sdk/tracing_utils.py

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -225,32 +225,6 @@ def add_http_breadcrumb(status_code: "Optional[int]", data: "dict[str, Any]") ->
225225
sentry_sdk.add_breadcrumb(**kwargs)
226226

227227

228-
def maybe_create_breadcrumbs_from_span(
229-
scope: "sentry_sdk.Scope", span: "sentry_sdk.tracing.Span"
230-
) -> None:
231-
if span.op == OP.HTTP_CLIENT and span.origin not in (
232-
"auto.http.aiohttp",
233-
"auto.http.pyreqwest",
234-
"auto.http.httpx",
235-
"auto.http.httpx2",
236-
"auto.http.stdlib.httplib",
237-
):
238-
level = None
239-
status_code = span._data.get(SPANDATA.HTTP_STATUS_CODE)
240-
if status_code:
241-
if 500 <= status_code <= 599:
242-
level = "error"
243-
elif 400 <= status_code <= 499:
244-
level = "warning"
245-
246-
if level:
247-
scope.add_breadcrumb(
248-
type="http", category="httplib", data=span._data, level=level
249-
)
250-
else:
251-
scope.add_breadcrumb(type="http", category="httplib", data=span._data)
252-
253-
254228
def _get_frame_module_abs_path(frame: "FrameType") -> "Optional[str]":
255229
try:
256230
return frame.f_code.co_filename

tests/integrations/boto3/test_s3.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import pytest
55

66
import sentry_sdk
7+
from sentry_sdk import capture_message
8+
from sentry_sdk.consts import SPANDATA
79
from sentry_sdk.integrations.boto3 import Boto3Integration
810
from tests.conftest import ApproxDict
911
from tests.integrations.boto3 import read_fixture
@@ -360,3 +362,79 @@ def test_span_origin(
360362

361363
assert event["contexts"]["trace"]["origin"] == "manual"
362364
assert event["spans"][0]["origin"] == "auto.http.boto3"
365+
366+
367+
def test_breadcrumb(sentry_init, capture_events):
368+
sentry_init(
369+
integrations=[Boto3Integration()],
370+
default_integrations=False,
371+
)
372+
373+
s3 = session.resource("s3")
374+
bucket = s3.Bucket("bucket")
375+
376+
events = capture_events()
377+
378+
with MockResponse(s3.meta.client, 200, {}, read_fixture("s3_list.xml")):
379+
_ = [obj for obj in bucket.objects.all()]
380+
381+
capture_message("Testing!")
382+
383+
(event,) = events
384+
(crumb,) = event["breadcrumbs"]["values"]
385+
assert crumb["type"] == "http"
386+
assert crumb["category"] == "httplib"
387+
assert crumb["data"] == ApproxDict(
388+
{
389+
"aws.request.url": mock.ANY,
390+
SPANDATA.HTTP_METHOD: "GET",
391+
SPANDATA.HTTP_QUERY: mock.ANY,
392+
SPANDATA.HTTP_FRAGMENT: "",
393+
}
394+
)
395+
396+
397+
@pytest.mark.parametrize("send_default_pii", [True, False])
398+
def test_breadcrumb_span_streaming(sentry_init, capture_events, send_default_pii):
399+
sentry_init(
400+
integrations=[Boto3Integration()],
401+
default_integrations=False,
402+
trace_lifecycle="stream",
403+
send_default_pii=send_default_pii,
404+
)
405+
406+
s3 = session.resource("s3")
407+
bucket = s3.Bucket("bucket")
408+
409+
events = capture_events()
410+
411+
with sentry_sdk.traces.start_span(name="custom parent"), MockResponse(
412+
s3.meta.client, 200, {}, read_fixture("s3_list.xml")
413+
):
414+
_ = [obj for obj in bucket.objects.all()]
415+
416+
capture_message("Testing!")
417+
418+
(event,) = events
419+
(crumb,) = event["breadcrumbs"]["values"]
420+
assert crumb["type"] == "http"
421+
assert crumb["category"] == "httplib"
422+
423+
if send_default_pii:
424+
assert crumb["data"] == ApproxDict(
425+
{
426+
SPANDATA.URL_FULL: mock.ANY,
427+
SPANDATA.HTTP_REQUEST_METHOD: "GET",
428+
SPANDATA.URL_QUERY: mock.ANY,
429+
SPANDATA.URL_FRAGMENT: "",
430+
}
431+
)
432+
else:
433+
assert crumb["data"] == ApproxDict(
434+
{
435+
SPANDATA.HTTP_REQUEST_METHOD: "GET",
436+
}
437+
)
438+
assert SPANDATA.URL_FULL not in crumb["data"]
439+
assert SPANDATA.URL_QUERY not in crumb["data"]
440+
assert SPANDATA.URL_FRAGMENT not in crumb["data"]

0 commit comments

Comments
 (0)