diff --git a/sentry_sdk/data_collection.py b/sentry_sdk/data_collection.py index f02659046d..9d83e19819 100644 --- a/sentry_sdk/data_collection.py +++ b/sentry_sdk/data_collection.py @@ -39,11 +39,12 @@ # ``http_bodies`` defaults to this (collect everything the # platform supports); an empty list is the explicit opt-out. -# response bodyies are not included here because we don't -# currently capture them (as of Jul 7 2026) +# ``incoming_response`` is not included here because we don't +# currently capture incoming response bodies. _ALL_HTTP_BODY_TYPES = [ "incoming_request", "outgoing_request", + "outgoing_response", ] # Default number of source lines captured above and below a stack frame. diff --git a/sentry_sdk/integrations/ariadne.py b/sentry_sdk/integrations/ariadne.py index 11e0b605ca..2a3c405514 100644 --- a/sentry_sdk/integrations/ariadne.py +++ b/sentry_sdk/integrations/ariadne.py @@ -176,8 +176,17 @@ def _make_response_event_processor(response: "Dict[str, Any]") -> "EventProcesso """Add response data to the event's response context.""" def inner(event: "Event", hint: "dict[str, Any]") -> "Event": + client_options = sentry_sdk.get_client().options with capture_internal_exceptions(): - if should_send_default_pii() and response.get("errors"): + if has_data_collection_enabled(client_options): + collect_response = ( + "outgoing_response" + in client_options["data_collection"]["http_bodies"] + ) + else: + collect_response = should_send_default_pii() + + if collect_response and response.get("errors"): contexts = event.setdefault("contexts", {}) contexts["response"] = { "data": response, diff --git a/sentry_sdk/integrations/strawberry.py b/sentry_sdk/integrations/strawberry.py index d3cb77cc4f..a3fa59b489 100644 --- a/sentry_sdk/integrations/strawberry.py +++ b/sentry_sdk/integrations/strawberry.py @@ -532,8 +532,17 @@ def _make_response_event_processor( response_data: "GraphQLHTTPResponse", ) -> "EventProcessor": def inner(event: "Event", hint: "dict[str, Any]") -> "Event": + client_options = sentry_sdk.get_client().options with capture_internal_exceptions(): - if should_send_default_pii(): + if has_data_collection_enabled(client_options): + collect_response = ( + "outgoing_response" + in client_options["data_collection"]["http_bodies"] + ) + else: + collect_response = should_send_default_pii() + + if collect_response: contexts = event.setdefault("contexts", {}) contexts["response"] = {"data": response_data} diff --git a/tests/integrations/ariadne/test_ariadne.py b/tests/integrations/ariadne/test_ariadne.py index b66376314e..616a3b9746 100644 --- a/tests/integrations/ariadne/test_ariadne.py +++ b/tests/integrations/ariadne/test_ariadne.py @@ -411,10 +411,8 @@ def test_request_data_collection( assert ("query" in event["request"]["data"]) == expect_query assert ("variables" in event["request"]["data"]) == expect_variables - # Response body capture is intentionally tied to send_default_pii only. - assert ("response" in event["contexts"]) == bool( - init_kwargs.get("send_default_pii") - ) + # ``http_bodies`` defaults to collecting the outgoing response. + assert "response" in event["contexts"] def test_request_data_collection_body_out_of_bounds_still_collects_variables( @@ -443,3 +441,29 @@ def test_request_data_collection_body_out_of_bounds_still_collects_variables( assert "query" not in event["request"]["data"] assert event["request"]["data"]["variables"] == {"name": "some name"} + + +@pytest.mark.parametrize( + "http_bodies,expect_response", + [ + pytest.param(None, True, id="http_bodies_default"), + pytest.param(["outgoing_response"], True, id="outgoing_response"), + pytest.param(["incoming_request"], False, id="incoming_request_only"), + pytest.param([], False, id="http_bodies_off"), + ], +) +def test_response_data_collection( + sentry_init, capture_events, graphql_client, http_bodies, expect_response +): + data_collection = {} if http_bodies is None else {"http_bodies": http_bodies} + _init_all_integrations( + sentry_init, _experiments={"data_collection": data_collection} + ) + events = capture_events() + + graphql_client().post("/graphql", json={"query": "query ErrorQuery {error}"}) + + assert len(events) == 1 + (event,) = events + + assert ("response" in event["contexts"]) == expect_response diff --git a/tests/integrations/strawberry/test_strawberry.py b/tests/integrations/strawberry/test_strawberry.py index 0678985d33..69f45fd4b1 100644 --- a/tests/integrations/strawberry/test_strawberry.py +++ b/tests/integrations/strawberry/test_strawberry.py @@ -352,6 +352,50 @@ def test_event_processor_data_collection( assert "api_target" not in error_event.get("request", {}) +@pytest.mark.parametrize( + "http_bodies,expect_response", + [ + pytest.param(None, True, id="http_bodies_default"), + pytest.param(["outgoing_response"], True, id="outgoing_response"), + pytest.param(["incoming_request"], False, id="incoming_request_only"), + pytest.param([], False, id="http_bodies_off"), + ], +) +@parameterize_strawberry_test +def test_response_data_collection( + request, + sentry_init, + capture_events, + client_factory, + async_execution, + framework_integrations, + http_bodies, + expect_response, +): + data_collection = {} if http_bodies is None else {"http_bodies": http_bodies} + sentry_init( + integrations=[StrawberryIntegration(async_execution=async_execution)] + + framework_integrations, + _experiments={"data_collection": data_collection}, + ) + events = capture_events() + + schema = strawberry.Schema(Query) + + client_factory = request.getfixturevalue(client_factory) + client = client_factory(schema) + + client.post( + "/graphql", + json={"query": "query ErrorQuery { error }", "operationName": "ErrorQuery"}, + ) + + assert len(events) == 1 + (error_event,) = events + + assert ("response" in error_event["contexts"]) == expect_response + + @pytest.mark.parametrize( "data_collection,expect_query,expect_variables", [