Skip to content

Commit c3baf08

Browse files
committed
feat(sanic): Apply data_collection filtering to URL query strings
Filter query string parameters in both span attributes (url.full, http.query) and event request_info according to the configured data_collection.url_query_params behaviour, matching the pattern already applied to wsgi, asgi, aiohttp, and tornado. Refs PY-2583 Refs #6743
1 parent 96e0ac0 commit c3baf08

2 files changed

Lines changed: 191 additions & 2 deletions

File tree

sentry_sdk/integrations/sanic.py

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
import sentry_sdk
99
from sentry_sdk import continue_trace
1010
from sentry_sdk.consts import OP, SPANDATA
11+
from sentry_sdk.data_collection import (
12+
_apply_data_collection_filtering_to_query_string,
13+
)
1114
from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version
1215
from sentry_sdk.integrations._wsgi_common import RequestExtractor, _filter_headers
1316
from sentry_sdk.integrations.logging import ignore_logger
@@ -21,6 +24,7 @@
2124
capture_internal_exceptions,
2225
ensure_integration_enabled,
2326
event_from_exception,
27+
has_data_collection_enabled,
2428
parse_version,
2529
reraise,
2630
)
@@ -379,8 +383,25 @@ def _get_request_attributes(request: "Request") -> "Dict[str, Any]":
379383
attributes[f"{SPANDATA.HTTP_REQUEST_HEADER}.{header.lower()}"] = value
380384

381385
urlparts = urlsplit(request.url)
386+
client_options = sentry_sdk.get_client().options
387+
388+
if has_data_collection_enabled(client_options):
389+
attributes["url.path"] = urlparts.path
382390

383-
if should_send_default_pii():
391+
filtered_query = None
392+
if urlparts.query:
393+
filtered_query = _apply_data_collection_filtering_to_query_string(
394+
query_string=urlparts.query,
395+
behaviour=client_options["data_collection"]["url_query_params"],
396+
)
397+
if filtered_query:
398+
attributes[SPANDATA.HTTP_QUERY] = filtered_query
399+
400+
attributes[SPANDATA.URL_FULL] = urlparts._replace(
401+
query=filtered_query or ""
402+
).geturl()
403+
404+
elif should_send_default_pii():
384405
attributes[SPANDATA.URL_FULL] = request.url
385406
attributes["url.path"] = urlparts.path
386407

@@ -421,7 +442,18 @@ def sanic_processor(event: "Event", hint: "Optional[Hint]") -> "Optional[Event]"
421442
urlparts.path,
422443
)
423444

424-
request_info["query_string"] = urlparts.query
445+
client_options = sentry_sdk.get_client().options
446+
if has_data_collection_enabled(client_options):
447+
if urlparts.query:
448+
filtered_query = _apply_data_collection_filtering_to_query_string(
449+
query_string=urlparts.query,
450+
behaviour=client_options["data_collection"]["url_query_params"],
451+
)
452+
if filtered_query:
453+
request_info["query_string"] = filtered_query
454+
else:
455+
request_info["query_string"] = urlparts.query
456+
425457
request_info["method"] = request.method
426458
request_info["env"] = {"REMOTE_ADDR": request.remote_addr}
427459
request_info["headers"] = _filter_headers(dict(request.headers))

tests/integrations/sanic/test_sanic.py

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -598,3 +598,160 @@ def child_span_handler(request):
598598
else:
599599
assert "user.ip_address" not in server_span["attributes"]
600600
assert "user.ip_address" not in child_span["attributes"]
601+
602+
603+
NO_QUERY_STRING = object()
604+
605+
_QUERY_PARAM_DATA_COLLECTION_CASES = [
606+
pytest.param(
607+
{"send_default_pii": True},
608+
"toy=tennisball&color=red&auth=secret",
609+
id="send_default_pii_true",
610+
),
611+
pytest.param(
612+
{"send_default_pii": False},
613+
NO_QUERY_STRING,
614+
id="send_default_pii_false",
615+
),
616+
pytest.param(
617+
{},
618+
NO_QUERY_STRING,
619+
id="defaults",
620+
),
621+
pytest.param(
622+
{"_experiments": {"data_collection": {}}},
623+
"toy=tennisball&color=red&auth=[Filtered]",
624+
id="data_collection_denylist_default",
625+
),
626+
pytest.param(
627+
{
628+
"_experiments": {
629+
"data_collection": {
630+
"url_query_params": {"mode": "denylist", "terms": ["toy"]}
631+
}
632+
}
633+
},
634+
"toy=[Filtered]&color=red&auth=[Filtered]",
635+
id="data_collection_denylist_custom_terms",
636+
),
637+
pytest.param(
638+
{
639+
"_experiments": {
640+
"data_collection": {
641+
"url_query_params": {"mode": "allowlist", "terms": ["toy"]}
642+
}
643+
}
644+
},
645+
"toy=tennisball&color=[Filtered]&auth=[Filtered]",
646+
id="data_collection_allowlist",
647+
),
648+
pytest.param(
649+
{
650+
"_experiments": {
651+
"data_collection": {
652+
"url_query_params": {"mode": "allowlist", "terms": ["auth"]}
653+
}
654+
}
655+
},
656+
"toy=[Filtered]&color=[Filtered]&auth=[Filtered]",
657+
id="data_collection_allowlist_sensitive_term",
658+
),
659+
pytest.param(
660+
{"_experiments": {"data_collection": {"url_query_params": {"mode": "off"}}}},
661+
NO_QUERY_STRING,
662+
id="data_collection_off",
663+
),
664+
pytest.param(
665+
{
666+
"send_default_pii": True,
667+
"_experiments": {"data_collection": {"url_query_params": {"mode": "off"}}},
668+
},
669+
NO_QUERY_STRING,
670+
id="data_collection_wins_over_send_default_pii",
671+
),
672+
]
673+
674+
675+
@pytest.mark.skipif(
676+
not PERFORMANCE_SUPPORTED, reason="Performance not supported on this Sanic version"
677+
)
678+
@pytest.mark.parametrize(
679+
"init_kwargs, expected_query", _QUERY_PARAM_DATA_COLLECTION_CASES
680+
)
681+
def test_url_query_data_collection_span_streaming(
682+
sentry_init, app, capture_items, init_kwargs, expected_query
683+
):
684+
init_kwargs = dict(init_kwargs)
685+
experiments = dict(init_kwargs.pop("_experiments", {}))
686+
experiments["trace_lifecycle"] = "stream"
687+
sentry_init(
688+
integrations=[SanicIntegration()],
689+
traces_sample_rate=1.0,
690+
_experiments=experiments,
691+
**init_kwargs,
692+
)
693+
694+
items = capture_items("span")
695+
696+
c = get_client(app)
697+
with c as client:
698+
_, response = client.get("/message?toy=tennisball&color=red&auth=secret")
699+
assert response.status == 200
700+
701+
sentry_sdk.flush()
702+
703+
(server_span,) = [
704+
i.payload
705+
for i in items
706+
if i.payload["attributes"].get("sentry.origin") == "auto.http.sanic"
707+
and i.payload["is_segment"]
708+
]
709+
710+
data_collection_enabled = "data_collection" in experiments
711+
url_attrs_expected = data_collection_enabled or init_kwargs.get(
712+
"send_default_pii", False
713+
)
714+
715+
if expected_query is NO_QUERY_STRING:
716+
assert "http.query" not in server_span["attributes"]
717+
if url_attrs_expected:
718+
assert server_span["attributes"]["url.full"].endswith("/message")
719+
assert server_span["attributes"]["url.path"].endswith("/message")
720+
else:
721+
assert "url.full" not in server_span["attributes"]
722+
assert "url.path" not in server_span["attributes"]
723+
else:
724+
assert server_span["attributes"]["http.query"] == expected_query
725+
assert server_span["attributes"]["url.full"].endswith(
726+
f"/message?{expected_query}"
727+
)
728+
assert server_span["attributes"]["url.path"].endswith("/message")
729+
730+
731+
@pytest.mark.parametrize(
732+
"init_kwargs, expected_query", _QUERY_PARAM_DATA_COLLECTION_CASES
733+
)
734+
def test_url_query_data_collection_event_processor(
735+
sentry_init, app, capture_events, init_kwargs, expected_query
736+
):
737+
sentry_init(integrations=[SanicIntegration()], **init_kwargs)
738+
739+
events = capture_events()
740+
741+
c = get_client(app)
742+
with c as client:
743+
_, response = client.get("/message?toy=tennisball&color=red&auth=secret")
744+
assert response.status == 200
745+
746+
(event,) = events
747+
748+
assert event["request"]["url"].endswith("/message")
749+
assert event["request"]["method"] == "GET"
750+
if "data_collection" not in init_kwargs.get("_experiments", {}):
751+
assert (
752+
event["request"]["query_string"] == "toy=tennisball&color=red&auth=secret"
753+
)
754+
elif expected_query is NO_QUERY_STRING:
755+
assert "query_string" not in event["request"]
756+
else:
757+
assert event["request"]["query_string"] == expected_query

0 commit comments

Comments
 (0)