Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
07a3ea3
ref(subprocess): Create breadcrumbs directly in integration
sentrivana Aug 7, 2026
2f91234
.
sentrivana Aug 7, 2026
c3aea86
ref: Move Redis breadcrumbs to integration
sentrivana Aug 7, 2026
15d2e2f
.
sentrivana Aug 7, 2026
53ab7f0
really mypy?
sentrivana Aug 7, 2026
0ea13df
.
sentrivana Aug 7, 2026
2f54487
move even earlier
sentrivana Aug 7, 2026
8e6087f
.
sentrivana Aug 7, 2026
91fe6a4
.
sentrivana Aug 7, 2026
5858c87
.
sentrivana Aug 7, 2026
906838b
Merge branch 'master' into ivana/move-breadcrumbs-to-integrations
sentrivana Aug 7, 2026
4e093aa
.
sentrivana Aug 7, 2026
040f223
Merge branch 'ivana/move-breadcrumbs-to-integrations' into ivana/move…
sentrivana Aug 7, 2026
7472af9
remove extra guards
sentrivana Aug 7, 2026
8ace993
Merge branch 'master' into ivana/move-redis-breadcrumbs-to-integration
sentrivana Aug 7, 2026
f492ba4
ref(aiohttp): Move breadcrumb capture to integration
sentrivana Aug 7, 2026
8e3adaf
exclude the spans
sentrivana Aug 7, 2026
2a172c1
.
sentrivana Aug 7, 2026
1239d84
.
sentrivana Aug 7, 2026
9adbede
Merge branch 'master' into ivana/move-http-crumbs-1
sentrivana Aug 7, 2026
abdc32b
.
sentrivana Aug 7, 2026
d03072b
.
sentrivana Aug 7, 2026
689d9d1
fix type annotation
sentrivana Aug 10, 2026
8bfb879
defensive access
sentrivana Aug 10, 2026
405fb12
Merge branch 'master' into ivana/move-http-crumbs-1
sentrivana Aug 10, 2026
31486cf
ref(pyreqwest): Move crumbs to integration
sentrivana Aug 10, 2026
ccf1de9
make it work in async
sentrivana Aug 10, 2026
0de51ad
fix sphinx
sentrivana Aug 10, 2026
1bdf216
gate no response
sentrivana Aug 10, 2026
62ca144
ref(httpx,httpx2): Move crumbs to integrations
sentrivana Aug 10, 2026
86c8163
.
sentrivana Aug 10, 2026
579d224
simplify
sentrivana Aug 10, 2026
3e992fb
Merge branch 'ivana/move-http-crumbs-2' into ivana/move-http-crumbs-3
sentrivana Aug 10, 2026
8ca14b3
.
sentrivana Aug 10, 2026
720071a
.
sentrivana Aug 10, 2026
f1312f7
.
sentrivana Aug 10, 2026
58c435a
Merge branch 'master' into ivana/move-http-crumbs-1
sentrivana Aug 10, 2026
e31a3aa
Merge branch 'ivana/move-http-crumbs-1' into ivana/move-http-crumbs-2
sentrivana Aug 10, 2026
e32ac38
Merge branch 'ivana/move-http-crumbs-2' into ivana/move-http-crumbs-3
sentrivana Aug 10, 2026
9c2b924
tests
sentrivana Aug 10, 2026
0d8e4ca
ref(stdlib): Move crumbs to integration
sentrivana Aug 11, 2026
a640d16
remove util completely
sentrivana Aug 11, 2026
d68e635
fix mypy
sentrivana Aug 11, 2026
a793322
tests
sentrivana Aug 11, 2026
0db0743
.
sentrivana Aug 11, 2026
be9520a
requests tests
sentrivana Aug 11, 2026
a44e3e1
simplify
sentrivana Aug 11, 2026
1e27316
.
sentrivana Aug 11, 2026
5df68a9
smaller diff
sentrivana Aug 11, 2026
2af3694
36
sentrivana Aug 11, 2026
2e0281a
.
sentrivana Aug 11, 2026
60870a2
return maybe_create
sentrivana Aug 11, 2026
db40334
.
sentrivana Aug 11, 2026
33a8c0c
.
sentrivana Aug 11, 2026
0c564cc
ref(boto3): Move crumbs to integration
sentrivana Aug 11, 2026
7411726
.
sentrivana Aug 11, 2026
7bd4d0b
.
sentrivana Aug 11, 2026
68f6cd2
.
sentrivana Aug 11, 2026
5ece50c
Merge branch 'master' into ivana/move-http-crumbs-5
sentrivana Aug 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 62 additions & 31 deletions sentry_sdk/integrations/boto3.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from sentry_sdk.scope import should_send_default_pii
from sentry_sdk.traces import StreamedSpan
from sentry_sdk.tracing import Span
from sentry_sdk.tracing_utils import has_span_streaming_enabled
from sentry_sdk.tracing_utils import add_http_breadcrumb, has_span_streaming_enabled
from sentry_sdk.utils import (
capture_internal_exceptions,
parse_url,
Expand Down Expand Up @@ -64,54 +64,83 @@ def _sentry_request_created(
if client.get_integration(Boto3Integration) is None:
return

parsed_url = None
if request.url is not None:
with capture_internal_exceptions():
parsed_url = parse_url(request.url, sanitize=False)

breadcrumb: "dict[str, Any]" = {}

is_span_streaming_enabled = has_span_streaming_enabled(client.options)
span: "Union[Span, StreamedSpan]"
span: "Union[Span, StreamedSpan, None]" = None
if is_span_streaming_enabled:
if sentry_sdk.traces.get_current_span() is None:
return
span = sentry_sdk.traces.start_span(
name=description,
attributes={
"sentry.op": OP.HTTP_CLIENT,
"sentry.origin": Boto3Integration.origin,
SPANDATA.RPC_METHOD: f"{service_id}/{operation_name}",
},
)
if request.url is not None and should_send_default_pii():
with capture_internal_exceptions():
parsed_url = parse_url(request.url, sanitize=False)
span.set_attribute(SPANDATA.URL_FULL, parsed_url.url)
span.set_attribute(SPANDATA.URL_QUERY, parsed_url.query)
span.set_attribute(SPANDATA.URL_FRAGMENT, parsed_url.fragment)
if parsed_url and should_send_default_pii():
breadcrumb.update(
{
SPANDATA.URL_FULL: parsed_url.url,
SPANDATA.URL_QUERY: parsed_url.query,
SPANDATA.URL_FRAGMENT: parsed_url.fragment,
}
)

if request.method is not None:
span.set_attribute(SPANDATA.HTTP_REQUEST_METHOD, request.method)
breadcrumb[SPANDATA.HTTP_REQUEST_METHOD] = request.method

if sentry_sdk.traces.get_current_span() is not None:
span = sentry_sdk.traces.start_span(
name=description,
attributes={
"sentry.op": OP.HTTP_CLIENT,
"sentry.origin": Boto3Integration.origin,
SPANDATA.RPC_METHOD: f"{service_id}/{operation_name}",
},
)
if parsed_url and should_send_default_pii():
span.set_attributes(
{
SPANDATA.URL_FULL: parsed_url.url,
SPANDATA.URL_QUERY: parsed_url.query,
SPANDATA.URL_FRAGMENT: parsed_url.fragment,
}
)

if request.method is not None:
span.set_attribute(SPANDATA.HTTP_REQUEST_METHOD, request.method)
else:
span = sentry_sdk.start_span(
Comment thread
sentrivana marked this conversation as resolved.
op=OP.HTTP_CLIENT,
name=description,
origin=Boto3Integration.origin,
)

if request.url is not None:
with capture_internal_exceptions():
parsed_url = parse_url(request.url, sanitize=False)
span.set_data("aws.request.url", parsed_url.url)
span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query)
span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment)
if parsed_url:
span.set_data("aws.request.url", parsed_url.url)
span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query)
span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment)
breadcrumb.update(
{
"aws.request.url": parsed_url.url,
SPANDATA.HTTP_QUERY: parsed_url.query,
SPANDATA.HTTP_FRAGMENT: parsed_url.fragment,
}
)

span.set_tag("aws.service_id", service_id.hyphenize())
span.set_tag("aws.operation_name", operation_name)
if request.method is not None:
span.set_data(SPANDATA.HTTP_METHOD, request.method)
breadcrumb[SPANDATA.HTTP_METHOD] = request.method

# We do it in order for subsequent http calls/retries be
# attached to this span.
span.__enter__()
# We do it in order for subsequent http calls/retries be
# attached to this span.
span.__enter__()

# request.context is an open-ended data-structure
# where we can add anything useful in request life cycle.
request.context["_sentrysdk_span"] = span
add_http_breadcrumb(None, breadcrumb)

Comment thread
sentrivana marked this conversation as resolved.
if span is not None:
# request.context is an open-ended data-structure
# where we can add anything useful in request life cycle.
request.context["_sentrysdk_span"] = span


def _sentry_after_call(
Expand All @@ -122,6 +151,7 @@ def _sentry_after_call(
# Span could be absent if the integration is disabled.
if span is None:
return

span.__exit__(None, None, None)

body = parsed.get("Body")
Expand Down Expand Up @@ -186,4 +216,5 @@ def _sentry_after_call_error(
# Span could be absent if the integration is disabled.
if span is None:
return

span.__exit__(type(exception), exception, None)
3 changes: 0 additions & 3 deletions sentry_sdk/tracing.py
Original file line number Diff line number Diff line change
Expand Up @@ -700,8 +700,6 @@ def finish(
if has_ai_op or is_ai_span_op:
self.set_data("gen_ai.conversation.id", conversation_id)

maybe_create_breadcrumbs_from_span(scope, self)

return None

def to_json(self) -> "Dict[str, Any]":
Expand Down Expand Up @@ -1495,5 +1493,4 @@ def calculate_interest_rate(amount, rate, years):
extract_sentrytrace_data,
has_span_streaming_enabled,
has_tracing_enabled,
maybe_create_breadcrumbs_from_span,
)
26 changes: 0 additions & 26 deletions sentry_sdk/tracing_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,32 +225,6 @@ def add_http_breadcrumb(status_code: "Optional[int]", data: "dict[str, Any]") ->
sentry_sdk.add_breadcrumb(**kwargs)


def maybe_create_breadcrumbs_from_span(
scope: "sentry_sdk.Scope", span: "sentry_sdk.tracing.Span"
) -> None:
if span.op == OP.HTTP_CLIENT and span.origin not in (
"auto.http.aiohttp",
"auto.http.pyreqwest",
"auto.http.httpx",
"auto.http.httpx2",
"auto.http.stdlib.httplib",
):
level = None
status_code = span._data.get(SPANDATA.HTTP_STATUS_CODE)
if status_code:
if 500 <= status_code <= 599:
level = "error"
elif 400 <= status_code <= 499:
level = "warning"

if level:
scope.add_breadcrumb(
type="http", category="httplib", data=span._data, level=level
)
else:
scope.add_breadcrumb(type="http", category="httplib", data=span._data)


def _get_frame_module_abs_path(frame: "FrameType") -> "Optional[str]":
try:
return frame.f_code.co_filename
Expand Down
78 changes: 78 additions & 0 deletions tests/integrations/boto3/test_s3.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import pytest

import sentry_sdk
from sentry_sdk import capture_message
from sentry_sdk.consts import SPANDATA
from sentry_sdk.integrations.boto3 import Boto3Integration
from tests.conftest import ApproxDict
from tests.integrations.boto3 import read_fixture
Expand Down Expand Up @@ -360,3 +362,79 @@ def test_span_origin(

assert event["contexts"]["trace"]["origin"] == "manual"
assert event["spans"][0]["origin"] == "auto.http.boto3"


def test_breadcrumb(sentry_init, capture_events):
sentry_init(
integrations=[Boto3Integration()],
default_integrations=False,
)

s3 = session.resource("s3")
bucket = s3.Bucket("bucket")

events = capture_events()

with MockResponse(s3.meta.client, 200, {}, read_fixture("s3_list.xml")):
_ = [obj for obj in bucket.objects.all()]

capture_message("Testing!")

(event,) = events
(crumb,) = event["breadcrumbs"]["values"]
assert crumb["type"] == "http"
assert crumb["category"] == "httplib"
assert crumb["data"] == ApproxDict(
{
"aws.request.url": mock.ANY,
SPANDATA.HTTP_METHOD: "GET",
SPANDATA.HTTP_QUERY: mock.ANY,
SPANDATA.HTTP_FRAGMENT: "",
}
)


@pytest.mark.parametrize("send_default_pii", [True, False])
def test_breadcrumb_span_streaming(sentry_init, capture_events, send_default_pii):
sentry_init(
integrations=[Boto3Integration()],
default_integrations=False,
trace_lifecycle="stream",
send_default_pii=send_default_pii,
)

s3 = session.resource("s3")
bucket = s3.Bucket("bucket")

events = capture_events()

with sentry_sdk.traces.start_span(name="custom parent"), MockResponse(
s3.meta.client, 200, {}, read_fixture("s3_list.xml")
):
_ = [obj for obj in bucket.objects.all()]

capture_message("Testing!")

(event,) = events
(crumb,) = event["breadcrumbs"]["values"]
assert crumb["type"] == "http"
assert crumb["category"] == "httplib"

if send_default_pii:
assert crumb["data"] == ApproxDict(
{
SPANDATA.URL_FULL: mock.ANY,
SPANDATA.HTTP_REQUEST_METHOD: "GET",
SPANDATA.URL_QUERY: mock.ANY,
SPANDATA.URL_FRAGMENT: "",
}
)
else:
assert crumb["data"] == ApproxDict(
{
SPANDATA.HTTP_REQUEST_METHOD: "GET",
}
)
assert SPANDATA.URL_FULL not in crumb["data"]
assert SPANDATA.URL_QUERY not in crumb["data"]
assert SPANDATA.URL_FRAGMENT not in crumb["data"]
Loading