Skip to content

Commit 0c564cc

Browse files
committed
ref(boto3): Move crumbs to integration
1 parent 33a8c0c commit 0c564cc

4 files changed

Lines changed: 132 additions & 48 deletions

File tree

sentry_sdk/integrations/boto3.py

Lines changed: 54 additions & 19 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,28 +64,48 @@ 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)
6875
span: "Union[Span, StreamedSpan]"
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,
@@ -95,15 +115,22 @@ def _sentry_request_created(
95115

96116
if request.url is not None:
97117
with capture_internal_exceptions():
98-
parsed_url = parse_url(request.url, sanitize=False)
99118
span.set_data("aws.request.url", parsed_url.url)
100119
span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query)
101120
span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment)
121+
breadcrumb.update(
122+
{
123+
"aws.request.url": parsed_url.url,
124+
SPANDATA.HTTP_QUERY: parsed_url.query,
125+
SPANDATA.HTTP_FRAGMENT: parsed_url.fragment,
126+
}
127+
)
102128

103129
span.set_tag("aws.service_id", service_id.hyphenize())
104130
span.set_tag("aws.operation_name", operation_name)
105131
if request.method is not None:
106132
span.set_data(SPANDATA.HTTP_METHOD, request.method)
133+
breadcrumb[SPANDATA.HTTP_METHOD] = request.method
107134

108135
# We do it in order for subsequent http calls/retries be
109136
# attached to this span.
@@ -112,17 +139,22 @@ def _sentry_request_created(
112139
# request.context is an open-ended data-structure
113140
# where we can add anything useful in request life cycle.
114141
request.context["_sentrysdk_span"] = span
142+
request.context["_sentrysdk_breadcrumb"] = breadcrumb
115143

116144

117145
def _sentry_after_call(
118146
context: "Dict[str, Any]", parsed: "Dict[str, Any]", **kwargs: "Any"
119147
) -> None:
120148
span: "Optional[Union[Span, StreamedSpan]]" = context.pop("_sentrysdk_span", None)
149+
breadcrumb: "Optional[dict[str, Any]]" = context.pop("_sentrysdk_breadcrumb", None)
121150

122151
# Span could be absent if the integration is disabled.
123152
if span is None:
124153
return
154+
125155
span.__exit__(None, None, None)
156+
if breadcrumb:
157+
add_http_breadcrumb(None, breadcrumb)
126158

127159
body = parsed.get("Body")
128160
if not isinstance(body, StreamingBody):
@@ -182,8 +214,11 @@ def _sentry_after_call_error(
182214
context: "Dict[str, Any]", exception: "Type[BaseException]", **kwargs: "Any"
183215
) -> None:
184216
span: "Optional[Union[Span, StreamedSpan]]" = context.pop("_sentrysdk_span", None)
217+
breadcrumb: "Optional[dict[str, Any]]" = context.pop("_sentrysdk_breadcrumb", None)
185218

186219
# Span could be absent if the integration is disabled.
187220
if span is None:
188221
return
222+
223+
add_http_breadcrumb(None, breadcrumb)
189224
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)