chore(boto3): Remove transaction-based tracing - #7304
chore(boto3): Remove transaction-based tracing#7304alexander-alderman-webb wants to merge 1 commit into
Conversation
Codecov Results 📊✅ 118702 passed | ❌ 13 failed | ⏭️ 6755 skipped | Total: 125470 | Pass Rate: 94.61% | Execution Time: 408m 25s 📊 Comparison with Base Branch
➕ New Tests (13)View new tests
❌ Failed Tests
|
| File | Patch % | Lines |
|---|---|---|
| sentry_sdk/integrations/boto3.py | 87.50% |
Coverage diff
@@ Coverage Diff @@
## main #PR +/-##
==========================================
- Coverage 90.30% 89.85% -0.45%
==========================================
Files 193 193 —
Lines 25601 25579 -22
Branches 9436 9422 -14
==========================================
+ Hits 23116 22982 -134
- Misses 2485 2597 +112
- Partials 1435 1420 -15Generated by Codecov Action
| 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: | ||
| breadcrumb[SPANDATA.HTTP_REQUEST_METHOD] = request.method |
There was a problem hiding this comment.
Boto3 breadcrumbs drop URL data and change keys outside stream mode
This path always emits stream-style breadcrumb keys and only includes URL fields when should_send_default_pii() is true, so default non-stream users lose aws.request.url/http.method/http.query and often get method-only crumbs. Keep the legacy breadcrumb shape when span streaming is off, or document/migrate the contract and update test_breadcrumb.
Evidence
_sentry_request_created()now always builds crumbs withSPANDATA.HTTP_REQUEST_METHODand PII-gatedURL_FULL/URL_QUERY/URL_FRAGMENT.- The removed non-stream branch previously set
aws.request.url,SPANDATA.HTTP_METHOD,SPANDATA.HTTP_QUERY, andSPANDATA.HTTP_FRAGMENTwheneverparsed_urlexisted, without a PII gate. tests/integrations/boto3/test_s3.py::test_breadcrumbstill expects those legacy keys under default (non-stream) init.- Stdlib keeps the dual-path crumb contract (
HTTP_METHOD/urlvs stream keys), so this is a boto3-only break for existing consumers.
Identified by Warden · code-review · APS-2P9
|
|
||
| with sentry_sdk.traces.start_span(name="custom parent") as span, MockResponse( | ||
| s3.meta.client, 200, {}, b"hello" | ||
| ): | ||
| body = obj.get()["Body"] | ||
| assert body.read(1) == b"h" | ||
| assert body.read(2) == b"el" | ||
| assert body.read(3) == b"lo" | ||
| assert body.read(1) == b"" | ||
| span.end() | ||
| if not send_default_pii: | ||
| assert "url.full" not in span1["attributes"] | ||
| assert "url.fragment" not in span1["attributes"] | ||
| assert "url.query" not in span1["attributes"] | ||
|
|
||
| sentry_sdk.flush() | ||
| spans = [item.payload for item in items] | ||
| assert len(spans) == 3 | ||
|
|
||
| span1 = spans[0] | ||
| assert span1["attributes"]["sentry.op"] == "http.client" | ||
| assert span1["name"] == "aws.s3.GetObject" | ||
|
|
||
| expected_attrs = { | ||
| "http.request.method": "GET", | ||
| "rpc.method": "S3/GetObject", | ||
| "sentry.environment": "production", | ||
| "sentry.op": "http.client", | ||
| "sentry.origin": "auto.http.boto3", | ||
| "sentry.release": mock.ANY, | ||
| "sentry.sdk.name": "sentry.python", | ||
| "sentry.sdk.version": mock.ANY, | ||
| "sentry.segment.id": mock.ANY, | ||
| "sentry.segment.name": "custom parent", | ||
| "server.address": mock.ANY, | ||
| "thread.id": mock.ANY, | ||
| "thread.name": mock.ANY, | ||
| } | ||
| if send_default_pii: | ||
| expected_attrs["url.full"] = "https://bucket.s3.amazonaws.com/foo.pdf" | ||
| expected_attrs["url.fragment"] = "" | ||
| expected_attrs["url.query"] = "" | ||
| assert span1["attributes"] == ApproxDict(expected_attrs) | ||
|
|
||
| if not send_default_pii: | ||
| assert "url.full" not in span1["attributes"] | ||
| assert "url.fragment" not in span1["attributes"] | ||
| assert "url.query" not in span1["attributes"] | ||
|
|
||
| span2 = spans[1] | ||
| assert span2["attributes"]["sentry.op"] == "http.client.stream" | ||
| assert span2["name"] == "aws.s3.GetObject" | ||
| assert span2["parent_span_id"] == span1["span_id"] | ||
| else: | ||
| events = capture_events() | ||
|
|
||
| with sentry_sdk.start_transaction() as transaction, MockResponse( | ||
| s3.meta.client, 200, {}, b"hello" | ||
| ): | ||
| body = obj.get()["Body"] | ||
| assert body.read(1) == b"h" | ||
| assert body.read(2) == b"el" | ||
| assert body.read(3) == b"lo" | ||
| assert body.read(1) == b"" | ||
| transaction.finish() | ||
|
|
||
| (event,) = events | ||
| assert event["type"] == "transaction" | ||
| assert len(event["spans"]) == 2 | ||
|
|
||
| span1 = event["spans"][0] | ||
| assert span1["op"] == "http.client" | ||
| assert span1["description"] == "aws.s3.GetObject" | ||
| assert span1["data"] == ApproxDict( | ||
| { | ||
| "http.method": "GET", | ||
| "aws.request.url": "https://bucket.s3.amazonaws.com/foo.pdf", | ||
| "http.fragment": "", | ||
| "http.query": "", | ||
| } | ||
| ) | ||
|
|
||
| span2 = event["spans"][1] | ||
| assert span2["op"] == "http.client.stream" | ||
| assert span2["description"] == "aws.s3.GetObject" | ||
| assert span2["parent_span_id"] == span1["span_id"] | ||
| span2 = spans[1] | ||
| assert span2["attributes"]["sentry.op"] == "http.client.stream" | ||
| assert span2["name"] == "aws.s3.GetObject" | ||
| assert span2["parent_span_id"] == span1["span_id"] | ||
|
|
||
|
|
||
| @pytest.mark.parametrize("span_streaming", [True, False]) | ||
| def test_streaming_close( | ||
| sentry_init, | ||
| capture_events, | ||
| capture_items, | ||
| span_streaming, | ||
| ): | ||
| sentry_init( | ||
| traces_sample_rate=1.0, | ||
| integrations=[Boto3Integration()], | ||
| trace_lifecycle="stream" if span_streaming else "static", | ||
| trace_lifecycle="stream", | ||
| ) | ||
|
|
||
| s3 = session.resource("s3") | ||
| obj = s3.Bucket("bucket").Object("foo.pdf") | ||
| items = capture_items("span") | ||
|
|
||
| if span_streaming: | ||
| items = capture_items("span") | ||
|
|
||
| with sentry_sdk.traces.start_span(name="custom parent") as span, MockResponse( | ||
| s3.meta.client, 200, {}, b"hello" | ||
| ): | ||
| body = obj.get()["Body"] | ||
| assert body.read(1) == b"h" | ||
| body.close() # close partially-read stream | ||
| span.end() | ||
|
|
||
| sentry_sdk.flush() | ||
| spans = [item.payload for item in items] | ||
| assert len(spans) == 3 | ||
| span1 = spans[0] | ||
| assert span1["attributes"]["sentry.op"] == "http.client" | ||
| span2 = spans[1] | ||
| assert span2["attributes"]["sentry.op"] == "http.client.stream" | ||
| else: | ||
| events = capture_events() | ||
|
|
||
| with sentry_sdk.start_transaction() as transaction, MockResponse( | ||
| s3.meta.client, 200, {}, b"hello" | ||
| ): | ||
| body = obj.get()["Body"] | ||
| assert body.read(1) == b"h" | ||
| body.close() # close partially-read stream | ||
| transaction.finish() | ||
| with sentry_sdk.traces.start_span(name="custom parent") as span, MockResponse( | ||
| s3.meta.client, 200, {}, b"hello" | ||
| ): | ||
| body = obj.get()["Body"] | ||
| assert body.read(1) == b"h" | ||
| body.close() # close partially-read stream | ||
| span.end() | ||
|
|
||
| (event,) = events | ||
| assert event["type"] == "transaction" | ||
| assert len(event["spans"]) == 2 | ||
| span1 = event["spans"][0] | ||
| assert span1["op"] == "http.client" | ||
| span2 = event["spans"][1] | ||
| assert span2["op"] == "http.client.stream" | ||
| sentry_sdk.flush() | ||
| spans = [item.payload for item in items] | ||
| assert len(spans) == 3 | ||
| span1 = spans[0] | ||
| assert span1["attributes"]["sentry.op"] == "http.client" | ||
| span2 = spans[1] | ||
| assert span2["attributes"]["sentry.op"] == "http.client.stream" |
There was a problem hiding this comment.
Boto3 instrumentation drops AWS operation spans in static lifecycle
The Boto3 integration now records its AWS operation span only through the span-streaming API. With the default or trace_lifecycle="static" lifecycle, sentry_sdk.traces.get_current_span() is always None, so _sentrysdk_span is never stored and the integration emits no aws.<service>.<operation> span. If removing static transaction tracing is intentional, please document this compatibility change and the required migration to trace_lifecycle="stream"; otherwise retain a static start_span/start_transaction path.
Evidence
_sentry_request_created()only checkssentry_sdk.traces.get_current_span()before callingsentry_sdk.traces.start_span().traces.get_current_span()returns a value only for the stream lifecycle; default/static scopes expose no streamed span.- Consequently, static requests never receive
_sentrysdk_spaninrequest.context, and_sentry_after_call()/_sentry_after_call_error()return without emitting an AWS operation span. - The SDK documents
trace_lifecycle=None/"static"as a supported completed-transaction lifecycle, while peer HTTP integrations retain a static fallback.
Also found at 1 additional location
tests/integrations/boto3/test_s3.py:161-213
Identified by Warden · code-review · XZY-VT9
Description
Issues
Closes #7079
Reminders
uv run ruff.feat:,fix:,ref:,meta:)