diff --git a/sentry_sdk/integrations/gql.py b/sentry_sdk/integrations/gql.py index 37759b72d9..a05eeaa202 100644 --- a/sentry_sdk/integrations/gql.py +++ b/sentry_sdk/integrations/gql.py @@ -1,10 +1,15 @@ import sentry_sdk +from sentry_sdk.data_collection import ( + _apply_data_collection_filtering_to_query_string, +) from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version from sentry_sdk.scope import should_send_default_pii from sentry_sdk.utils import ( + capture_internal_exceptions, ensure_integration_enabled, event_from_exception, has_data_collection_enabled, + parse_url, parse_version, ) @@ -36,7 +41,7 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: - from typing import Any, Dict, Tuple, Union + from typing import Any, Dict, Optional, Tuple, Union from sentry_sdk._types import Event, EventProcessor @@ -99,9 +104,29 @@ def _request_info_from_transport( } try: - request_info["url"] = transport.url + url = transport.url except AttributeError: - pass + return request_info + + with capture_internal_exceptions(): + client_options = sentry_sdk.get_client().options + use_data_collection = has_data_collection_enabled(client_options) + + # Some transports accept a URL object as well as a string. + # Credentials are always removed from the URL. Query values are + # filtered here only when data_collection is not configured, since + # its url_query_params setting does its own filtering. + parsed_url = parse_url(str(url), sanitize=not use_data_collection) + request_info["url"] = parsed_url.url + + query_string: "Optional[str]" = parsed_url.query + if use_data_collection and query_string: + query_string = _apply_data_collection_filtering_to_query_string( + query_string=query_string, + behaviour=client_options["data_collection"]["url_query_params"], + ) + if query_string: + request_info["query_string"] = query_string return request_info diff --git a/tests/integrations/gql/test_gql.py b/tests/integrations/gql/test_gql.py index dcaee5be10..9c0c1c94ed 100644 --- a/tests/integrations/gql/test_gql.py +++ b/tests/integrations/gql/test_gql.py @@ -1,3 +1,5 @@ +import re + import pytest import responses from gql import Client, __version__, gql @@ -273,3 +275,84 @@ def test_real_gql_request_with_error_data_collection_variables( assert event["request"]["data"].get("variables") else: assert not event["request"]["data"].get("variables") + + +@pytest.mark.parametrize( + "init_kwargs,expected_query_string", + [ + pytest.param( + {}, + "access_token=[Filtered]&v=[Filtered]&page=[Filtered]", + id="no_pii_no_data_collection", + ), + pytest.param( + {"send_default_pii": True}, + "access_token=[Filtered]&v=[Filtered]&page=[Filtered]", + id="legacy_pii_on", + ), + pytest.param( + {"data_collection": {}}, + "access_token=%5BFiltered%5D&v=1&page=2", + id="data_collection_defaults", + ), + pytest.param( + {"data_collection": {"url_query_params": {"mode": "off"}}}, + None, + id="data_collection_query_params_off", + ), + pytest.param( + { + "data_collection": { + "url_query_params": {"mode": "denylist", "terms": ["page"]} + } + }, + "access_token=%5BFiltered%5D&v=1&page=%5BFiltered%5D", + id="data_collection_denylist", + ), + pytest.param( + { + "data_collection": { + "url_query_params": {"mode": "allowlist", "terms": ["page"]} + } + }, + "access_token=%5BFiltered%5D&v=%5BFiltered%5D&page=2", + id="data_collection_allowlist", + ), + pytest.param( + { + "send_default_pii": True, + "data_collection": {"url_query_params": {"mode": "off"}}, + }, + None, + id="data_collection_takes_precedence_over_send_default_pii", + ), + ], +) +@responses.activate +def test_transport_url_is_sanitized( + sentry_init, capture_events, init_kwargs, expected_query_string +): + sentry_init(integrations=[GQLIntegration()], **init_kwargs) + events = capture_events() + + responses.add( + method=responses.POST, + url=re.compile(r"https://.*api\.example\.com/graphql.*"), + json={"errors": [{"message": "some error"}]}, + status=200, + ) + transport = RequestsHTTPTransport( + url="https://user:password@api.example.com/graphql?access_token=secret&v=1&page=2#frag" + ) + client = Client(transport=transport) + + with pytest.raises(TransportQueryError): + client.execute(gql("query Example { example }")) + + (event,) = events + assert event["request"]["url"] == ( + "https://[Filtered]:[Filtered]@api.example.com/graphql" + ) + assert event["request"].get("query_string") == expected_query_string + assert "password" not in str(event["request"]) + assert "secret" not in str(event["request"])