From 697f282750a6a51cdc891450728ff9fd74b47252 Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Tue, 25 Aug 2026 16:13:44 -0400 Subject: [PATCH] fix(starlette): Gate request body collection on data_collection experiment Only attach request body data to events and spans when "incoming_request" is present in the data_collection.http_bodies experiment option (or when the experiment is unset, preserving default behavior). Refs PY-2419 Refs #6283 --- sentry_sdk/integrations/starlette.py | 32 +++-- .../integrations/starlette/test_starlette.py | 113 ++++++++++++++++++ 2 files changed, 137 insertions(+), 8 deletions(-) diff --git a/sentry_sdk/integrations/starlette.py b/sentry_sdk/integrations/starlette.py index 772ba7c29e..2eff9d2d2a 100644 --- a/sentry_sdk/integrations/starlette.py +++ b/sentry_sdk/integrations/starlette.py @@ -562,7 +562,15 @@ def event_processor(event: "Event", hint: "Dict[str, Any]") -> "Event": if "cookies" in info: request_info["cookies"] = info["cookies"] if "data" in info: - request_info["data"] = info["data"] + attach_request_data = True + if has_data_collection_enabled(client.options): + attach_request_data = ( + "incoming_request" + in client.options["data_collection"]["http_bodies"] + ) + + if attach_request_data: + request_info["data"] = info["data"] event["request"] = deepcopy(request_info) return event @@ -580,15 +588,23 @@ def event_processor(event: "Event", hint: "Dict[str, Any]") -> "Event": current_span = get_current_span() if type(current_span) is StreamedSpan: - request_body = _get_cached_request_body_attribute( - client=client, request=request - ) - if request_body: - current_span._segment.set_attribute( - SPANDATA.HTTP_REQUEST_BODY_DATA, - request_body, + attach_request_data = True + if has_data_collection_enabled(client.options): + attach_request_data = ( + "incoming_request" + in client.options["data_collection"]["http_bodies"] ) + if attach_request_data: + request_body = _get_cached_request_body_attribute( + client=client, request=request + ) + if request_body: + current_span._segment.set_attribute( + SPANDATA.HTTP_REQUEST_BODY_DATA, + request_body, + ) + def patch_request_response() -> None: old_request_response = starlette.routing.request_response diff --git a/tests/integrations/starlette/test_starlette.py b/tests/integrations/starlette/test_starlette.py index 3cf82e2bb0..2b1cedf1bb 100644 --- a/tests/integrations/starlette/test_starlette.py +++ b/tests/integrations/starlette/test_starlette.py @@ -510,6 +510,119 @@ async def test_request_body_too_big( } +@pytest.mark.asyncio +@pytest.mark.parametrize("span_streaming", [True, False]) +async def test_formdata_request_body_data_collection_http_bodies_empty( + sentry_init, capture_events, capture_items, span_streaming +): + sentry_init( + traces_sample_rate=1.0, + max_request_body_size="always", + integrations=[StarletteIntegration()], + trace_lifecycle="stream" if span_streaming else "static", + _experiments={"data_collection": {"http_bodies": []}}, + ) + + starlette_app = starlette_app_factory() + client = TestClient(starlette_app) + + headers = {"content-type": "multipart/form-data; boundary=fd721ef49ea403a6"} + + if span_streaming: + items = capture_items("event", "span") + + client.post("/body/form", data=BODY_FORM.encode("utf-8"), headers=headers) + + (event,) = (item.payload for item in items if item.type == "event") + assert "data" not in event["request"] + + sentry_sdk.flush() + spans = [item.payload for item in items if item.type == "span"] + server_span = next( + span for span in spans if span["attributes"]["sentry.op"] == "http.server" + ) + assert SPANDATA.HTTP_REQUEST_BODY_DATA not in server_span["attributes"] + else: + events = capture_events() + + client.post("/body/form", data=BODY_FORM.encode("utf-8"), headers=headers) + + (event, _) = events + assert "data" not in event["request"] + + +@pytest.mark.asyncio +@pytest.mark.parametrize("span_streaming", [True, False]) +@pytest.mark.parametrize( + "data_collection, expect_body", + [ + pytest.param(None, True, id="no_data_collection_experiment"), + pytest.param({}, True, id="data_collection_http_bodies_default"), + pytest.param( + {"http_bodies": ["incoming_request"]}, + True, + id="data_collection_http_bodies_incoming_request", + ), + pytest.param( + {"http_bodies": []}, False, id="data_collection_http_bodies_empty" + ), + ], +) +async def test_request_body_data_collection( + sentry_init, + capture_events, + capture_items, + span_streaming, + data_collection, + expect_body, +): + sentry_init( + traces_sample_rate=1.0, + integrations=[StarletteIntegration()], + trace_lifecycle="stream" if span_streaming else "static", + _experiments=( + {} if data_collection is None else {"data_collection": data_collection} + ), + ) + + starlette_app = starlette_app_factory() + client = TestClient(starlette_app) + + if span_streaming: + items = capture_items("event", "span") + + client.post("/body/json", json=BODY_JSON) + + (event,) = (item.payload for item in items if item.type == "event") + + sentry_sdk.flush() + spans = [item.payload for item in items if item.type == "span"] + server_span = next( + span for span in spans if span["attributes"]["sentry.op"] == "http.server" + ) + + if expect_body: + assert event["request"]["data"] == BODY_JSON + assert ( + json.loads(server_span["attributes"][SPANDATA.HTTP_REQUEST_BODY_DATA]) + == BODY_JSON + ) + else: + assert "data" not in event["request"] + assert SPANDATA.HTTP_REQUEST_BODY_DATA not in server_span["attributes"] + else: + events = capture_events() + + client.post("/body/json", json=BODY_JSON) + + (event, _) = events + + if expect_body: + assert event["request"]["data"] == BODY_JSON + else: + assert "data" not in event["request"] + + @pytest.mark.asyncio async def test_request_info_no_pii(sentry_init, capture_events): sentry_init(