From 5134b56fb3ddd0feddbaab9887f7149d33aa9b77 Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Fri, 14 Aug 2026 17:21:05 -0400 Subject: [PATCH] feat(tornado): Gate request body collection on data_collection option Refs PY-2419 Refs #6283 --- sentry_sdk/integrations/tornado.py | 21 +++- tests/integrations/tornado/test_tornado.py | 123 +++++++++++++++++++++ 2 files changed, 139 insertions(+), 5 deletions(-) diff --git a/sentry_sdk/integrations/tornado.py b/sentry_sdk/integrations/tornado.py index e71bf94d12..3ae20ef83f 100644 --- a/sentry_sdk/integrations/tornado.py +++ b/sentry_sdk/integrations/tornado.py @@ -243,11 +243,22 @@ def _get_request_attributes(request: "Any") -> "Dict[str, Any]": if request.protocol: attributes[SPANDATA.NETWORK_PROTOCOL_NAME] = request.protocol - with capture_internal_exceptions(): - raw_data = _get_tornado_request_data(request) - body_data = raw_data.value if isinstance(raw_data, AnnotatedValue) else raw_data - if body_data is not None: - attributes[SPANDATA.HTTP_REQUEST_BODY_DATA] = body_data + # The request data was unconditionally set pre-data collection which is + # why we're defaulting to True + record_incoming_request_data = True + if has_data_collection_enabled(client_options): + record_incoming_request_data = ( + "incoming_request" in client_options["data_collection"]["http_bodies"] + ) + + if record_incoming_request_data: + with capture_internal_exceptions(): + raw_data = _get_tornado_request_data(request) + body_data = ( + raw_data.value if isinstance(raw_data, AnnotatedValue) else raw_data + ) + if body_data is not None: + attributes[SPANDATA.HTTP_REQUEST_BODY_DATA] = body_data return attributes diff --git a/tests/integrations/tornado/test_tornado.py b/tests/integrations/tornado/test_tornado.py index 2268a791ee..b8582ae964 100644 --- a/tests/integrations/tornado/test_tornado.py +++ b/tests/integrations/tornado/test_tornado.py @@ -489,6 +489,129 @@ def test_url_query_data_collection_event_processor_repeated_and_blank_params( assert event["request"]["query_string"] == "a=1&a=2&b=" +@pytest.mark.parametrize( + "data_collection, expect_body", + [ + 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": ["outgoing_request"]}, + False, + id="data_collection_http_bodies_outgoing_request_only", + ), + pytest.param( + {"http_bodies": []}, False, id="data_collection_http_bodies_empty" + ), + ], +) +def test_request_body_data_collection_span_streaming( + tornado_testcase, sentry_init, capture_items, data_collection, expect_body +): + sentry_init( + integrations=[TornadoIntegration()], + traces_sample_rate=1.0, + trace_lifecycle="stream", + _experiments={"data_collection": data_collection}, + ) + + items = capture_items("span") + + client = tornado_testcase(Application([(r"/hi", HelloHandler)])) + response = client.fetch("/hi", method="POST", body=b"heyoo") + assert response.code == 200 + + sentry_sdk.flush() + + (server_span,) = [item.payload for item in items] + + if expect_body: + assert server_span["attributes"]["http.request.body.data"] == "heyoo" + else: + assert "http.request.body.data" not in server_span["attributes"] + + +@pytest.mark.parametrize( + "data_collection, expect_body", + [ + 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": ["outgoing_request"]}, + False, + id="data_collection_http_bodies_outgoing_request_only", + ), + pytest.param( + {"http_bodies": []}, False, id="data_collection_http_bodies_empty" + ), + ], +) +def test_request_body_data_collection_event_processor( + tornado_testcase, sentry_init, capture_events, data_collection, expect_body +): + sentry_init( + integrations=[TornadoIntegration()], + trace_lifecycle="static", + _experiments={"data_collection": data_collection}, + ) + + events = capture_events() + + data = {"hey": 42} + client = tornado_testcase(Application([(r"/hi", CrashingHandler)])) + response = client.fetch( + "/hi", + method="POST", + body=json.dumps(data), + headers={"Content-Type": "application/json"}, + ) + assert response.code == 500 + + sentry_sdk.flush() + + (event,) = events + + if expect_body: + assert event["request"]["data"] == data + else: + assert "data" not in event["request"] + + +def test_oversized_request_body_not_annotated_data_collection_span_streaming( + tornado_testcase, sentry_init, capture_items +): + """ + The gating happens before the size check, so an oversized body is dropped + outright instead of being reported as removed because of the size limit. + """ + sentry_init( + integrations=[TornadoIntegration()], + traces_sample_rate=1.0, + trace_lifecycle="stream", + max_request_body_size="small", + _experiments={"data_collection": {"http_bodies": []}}, + ) + + items = capture_items("span") + + client = tornado_testcase(Application([(r"/hi", HelloHandler)])) + response = client.fetch("/hi", method="POST", body=b"a" * 2000) + assert response.code == 200 + + sentry_sdk.flush() + + (server_span,) = [item.payload for item in items] + + assert "http.request.body.data" not in server_span["attributes"] + + @pytest.mark.parametrize("send_pii", [True, False]) @pytest.mark.parametrize("span_streaming", [True, False]) @pytest.mark.parametrize(