From 058e04827827d24a424411004cc1f6b5214d07be Mon Sep 17 00:00:00 2001 From: Erica Pisani Date: Fri, 24 Jul 2026 15:33:51 -0400 Subject: [PATCH] feat(gql): Gate GraphQL data collection behind data_collection option Attach the GraphQL document and variable definitions to error events based on the structured data_collection configuration (graphql.document / graphql.variables), falling back to send_default_pii when data_collection is not set. --- sentry_sdk/integrations/gql.py | 34 +++++++- tests/integrations/gql/test_gql.py | 126 +++++++++++++++++++++++++++++ 2 files changed, 158 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/integrations/gql.py b/sentry_sdk/integrations/gql.py index dcb60e9561..95dbb5c1a0 100644 --- a/sentry_sdk/integrations/gql.py +++ b/sentry_sdk/integrations/gql.py @@ -4,6 +4,7 @@ from sentry_sdk.utils import ( ensure_integration_enabled, event_from_exception, + has_data_collection_enabled, parse_version, ) @@ -54,12 +55,18 @@ def setup_once() -> None: def _data_from_document(document: "DocumentNode") -> "EventDataType": + client_options = sentry_sdk.get_client().options try: operation_ast = get_operation_ast(document) data: "EventDataType" = {"query": print_ast(document)} if operation_ast is not None: - data["variables"] = operation_ast.variable_definitions + if has_data_collection_enabled(client_options): + if client_options["data_collection"]["graphql"]["variables"]: + data["variables"] = operation_ast.variable_definitions + elif should_send_default_pii(): + data["variables"] = operation_ast.variable_definitions + if operation_ast.name is not None: data["operationName"] = operation_ast.name.value @@ -147,7 +154,30 @@ def processor(event: "Event", hint: "dict[str, Any]") -> "Event": } ) - if should_send_default_pii(): + client_options = sentry_sdk.get_client().options + if has_data_collection_enabled(client_options): + if client_options["data_collection"]["graphql"]["document"]: + if GraphQLRequest is not None and isinstance( + document_or_request, GraphQLRequest + ): + # In v4.0.0, gql moved to using GraphQLRequest instead of + # DocumentNode in execute + # https://github.com/graphql-python/gql/pull/556 + document = document_or_request.document + else: + document = document_or_request + + request["data"] = _data_from_document(document) + contexts = event.setdefault("contexts", {}) + response = contexts.setdefault("response", {}) + response.update( + { + "data": {"errors": errors}, + "type": response, + } + ) + + elif should_send_default_pii(): if GraphQLRequest is not None and isinstance( document_or_request, GraphQLRequest ): diff --git a/tests/integrations/gql/test_gql.py b/tests/integrations/gql/test_gql.py index 5ec794da19..77787e696d 100644 --- a/tests/integrations/gql/test_gql.py +++ b/tests/integrations/gql/test_gql.py @@ -58,6 +58,30 @@ def _execute_mock_query_with_keyword_document(response_json): return client.execute(document=query) +@responses.activate +def _execute_mock_query_with_variables(response_json): + url = "http://example.com/graphql" + query_string = """ + query Example($id: ID!) { + example(id: $id) + } + """ + + # Mock the GraphQL server response + responses.add( + method=responses.POST, + url=url, + json=response_json, + status=200, + ) + + transport = RequestsHTTPTransport(url=url) + client = Client(transport=transport) + query = gql(query_string) + + return client.execute(query, variable_values={"id": "1"}) + + _execute_query_funcs = [_execute_mock_query] if GQL_VERSION < (4,): _execute_query_funcs.append(_execute_mock_query_with_keyword_document) @@ -147,3 +171,105 @@ def test_real_gql_request_with_error_with_pii( assert "data" in event["request"] assert "response" in event["contexts"] + + +@pytest.mark.parametrize("execute_query", _execute_query_funcs) +@pytest.mark.parametrize( + "init_kwargs,expect_data", + [ + pytest.param({}, False, id="no_pii_no_data_collection"), + pytest.param({"send_default_pii": True}, True, id="legacy_pii_on"), + pytest.param( + {"_experiments": {"data_collection": {}}}, + True, + id="data_collection_defaults", + ), + pytest.param( + {"_experiments": {"data_collection": {"graphql": {"document": True}}}}, + True, + id="data_collection_document_on", + ), + pytest.param( + {"_experiments": {"data_collection": {"graphql": {"document": False}}}}, + False, + id="data_collection_document_off", + ), + pytest.param( + { + "send_default_pii": True, + "_experiments": {"data_collection": {"graphql": {"document": False}}}, + }, + False, + id="data_collection_takes_precedence_over_send_default_pii_on", + ), + pytest.param( + { + "send_default_pii": False, + "_experiments": {"data_collection": {"graphql": {"document": True}}}, + }, + True, + id="data_collection_takes_precedence_over_send_default_pii_off", + ), + ], +) +def test_real_gql_request_with_error_data_collection( + sentry_init, capture_events, execute_query, init_kwargs, expect_data +): + """ + Integration test verifying that the GQLIntegration honours the + ``data_collection`` configuration when deciding whether to attach the + GraphQL document to the event. + """ + sentry_init(integrations=[GQLIntegration()], **init_kwargs) + + event = _make_erroneous_query(capture_events, execute_query) + + if expect_data: + assert "data" in event["request"] + assert "query" in event["request"]["data"] + assert "response" in event["contexts"] + else: + assert "data" not in event["request"] + assert "response" not in event["contexts"] + + +@pytest.mark.parametrize( + "init_kwargs,expect_variables", + [ + pytest.param({"send_default_pii": True}, True, id="legacy_pii_on"), + pytest.param( + {"_experiments": {"data_collection": {}}}, + True, + id="data_collection_defaults", + ), + pytest.param( + {"_experiments": {"data_collection": {"graphql": {"variables": True}}}}, + True, + id="data_collection_variables_on", + ), + pytest.param( + {"_experiments": {"data_collection": {"graphql": {"variables": False}}}}, + False, + id="data_collection_variables_off", + ), + ], +) +def test_real_gql_request_with_error_data_collection_variables( + sentry_init, capture_events, init_kwargs, expect_variables +): + """ + Integration test verifying that the GQLIntegration honours the + ``data_collection`` ``graphql.variables`` toggle for queries that + define variables. + """ + sentry_init(integrations=[GQLIntegration()], **init_kwargs) + + event = _make_erroneous_query(capture_events, _execute_mock_query_with_variables) + + assert "data" in event["request"] + assert "query" in event["request"]["data"] + + if expect_variables: + assert event["request"]["data"].get("variables") + else: + assert not event["request"]["data"].get("variables")