Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 50 additions & 3 deletions sentry_sdk/integrations/_asgi_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@
from enum import Enum
from typing import TYPE_CHECKING

import sentry_sdk
from sentry_sdk.data_collection import _apply_data_collection_filtering_to_query_string
from sentry_sdk.integrations._wsgi_common import _filter_headers
from sentry_sdk.scope import should_send_default_pii
from sentry_sdk.utils import has_data_collection_enabled

if TYPE_CHECKING:
from typing import Any, Dict, Optional, Union
Expand Down Expand Up @@ -75,7 +78,7 @@ def _get_url(
return path


def _get_query(asgi_scope: "Any") -> "Any":
def _get_query(asgi_scope: "Any") -> "Optional[str]":
"""
Extract querystring from the ASGI scope, in the format that the Sentry protocol expects.
"""
Expand Down Expand Up @@ -122,7 +125,20 @@ def _get_request_data(
use_annotated_value=False,
)

request_data["query_string"] = _get_query(asgi_scope)
client_options = sentry_sdk.get_client().options
if has_data_collection_enabled(client_options):
qs = _get_query(asgi_scope)
if qs:
filtered_query_string = (
_apply_data_collection_filtering_to_query_string(
query_string=qs,
behaviour=client_options["data_collection"]["url_query_params"],
)
)
if filtered_query_string:
request_data["query_string"] = filtered_query_string
else:
request_data["query_string"] = _get_query(asgi_scope)

request_data["url"] = _get_url(
asgi_scope,
Expand Down Expand Up @@ -158,7 +174,38 @@ def _get_request_attributes(
for header, value in filtered_headers.items():
attributes[f"http.request.header.{header.lower()}"] = value

if should_send_default_pii():
client_options = sentry_sdk.get_client().options
if has_data_collection_enabled(client_options):
filtered_query_string = None
query = _get_query(asgi_scope)

if query:
filtered_query_string = (
_apply_data_collection_filtering_to_query_string(
query_string=query,
behaviour=client_options["data_collection"]["url_query_params"],
)
)
if filtered_query_string:
attributes["http.query"] = filtered_query_string

path = _get_path(asgi_scope=asgi_scope, root_path_in_path=root_path_in_path)
attributes["url.path"] = path

url_without_query_string = _get_url(
asgi_scope,
"http" if ty == "http" else "ws",
headers.get("host"),
path=path,
)

attributes["url.full"] = (
f"{url_without_query_string}?{filtered_query_string}"
if filtered_query_string is not None
else url_without_query_string
)

elif should_send_default_pii():
query = _get_query(asgi_scope)
if query:
attributes["http.query"] = query
Expand Down
210 changes: 210 additions & 0 deletions tests/integrations/asgi/test_asgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -918,6 +918,216 @@ def test_get_request_attributes_url_with_headers_off(sentry_init):
assert attributes["url.full"] == "http://example.com/foo?somevalue=123"


NO_QUERY_STRING = object()
QUERY_STRING = "token=abc&theme=dark&lang=en&session=xyz"


def _http_scope(query_string=QUERY_STRING):
return {
"type": "http",
"method": "GET",
"scheme": "http",
"server": ("example.com", 80),
"path": "/foo",
"query_string": query_string.encode("latin-1"),
"headers": [(b"host", b"example.com")],
}


@pytest.mark.parametrize(
"init_kwargs, expected_query_string",
[
pytest.param(
{"send_default_pii": True},
QUERY_STRING,
id="send_default_pii_true",
),
pytest.param(
{"send_default_pii": False},
QUERY_STRING,
id="send_default_pii_false",
),
pytest.param(
{},
QUERY_STRING,
id="defaults",
),
pytest.param(
{"_experiments": {"data_collection": {}}},
"token=[Filtered]&theme=dark&lang=en&session=[Filtered]",
id="data_collection_denylist_default",
),
pytest.param(
{
"_experiments": {
"data_collection": {
"url_query_params": {"mode": "denylist", "terms": ["theme"]}
}
}
},
"token=[Filtered]&theme=[Filtered]&lang=en&session=[Filtered]",
id="data_collection_denylist_custom_terms",
),
pytest.param(
{
"_experiments": {
"data_collection": {
"url_query_params": {"mode": "allowlist", "terms": ["theme"]}
}
}
},
"token=[Filtered]&theme=dark&lang=[Filtered]&session=[Filtered]",
id="data_collection_allowlist",
),
pytest.param(
{
"_experiments": {
"data_collection": {
"url_query_params": {"mode": "allowlist", "terms": ["token"]}
}
}
},
"token=[Filtered]&theme=[Filtered]&lang=[Filtered]&session=[Filtered]",
id="data_collection_allowlist_sensitive_term",
),
pytest.param(
{
"_experiments": {
"data_collection": {"url_query_params": {"mode": "off"}}
}
},
NO_QUERY_STRING,
id="data_collection_off",
),
# data_collection wins over send_default_pii: filtering still applies.
pytest.param(
{
"send_default_pii": True,
"_experiments": {
"data_collection": {"url_query_params": {"mode": "off"}}
},
},
NO_QUERY_STRING,
id="data_collection_wins_over_send_default_pii",
),
],
)
def test_get_request_data_query_string_data_collection(
sentry_init, init_kwargs, expected_query_string
):
sentry_init(**init_kwargs)

request_data = _get_request_data(_http_scope(), _RootPathInPath.EXCLUDED)

if expected_query_string is NO_QUERY_STRING:
assert "query_string" not in request_data
else:
assert request_data["query_string"] == expected_query_string


def test_get_request_data_query_string_empty_legacy_is_none(sentry_init):
# Legacy path: the query string is always set even when empty (``None``).
sentry_init(send_default_pii=True)

request_data = _get_request_data(
_http_scope(query_string=""), _RootPathInPath.EXCLUDED
)

assert request_data["query_string"] is None


def test_get_request_data_empty_query_string_dropped_with_data_collection(sentry_init):
sentry_init(_experiments={"data_collection": {}})

request_data = _get_request_data(
_http_scope(query_string=""), _RootPathInPath.EXCLUDED
)

assert "query_string" not in request_data


@pytest.mark.parametrize(
"init_kwargs, expected_query, expected_url_full",
[
pytest.param(
{"send_default_pii": True},
QUERY_STRING,
"http://example.com/foo?" + QUERY_STRING,
id="send_default_pii_true",
),
pytest.param(
{"send_default_pii": False},
NO_QUERY_STRING,
None,
id="send_default_pii_false",
),
pytest.param(
{},
NO_QUERY_STRING,
None,
id="defaults",
),
pytest.param(
{"_experiments": {"data_collection": {}}},
"token=[Filtered]&theme=dark&lang=en&session=[Filtered]",
"http://example.com/foo?token=[Filtered]&theme=dark&lang=en&session=[Filtered]",
id="data_collection_denylist_default",
),
pytest.param(
{
"_experiments": {
"data_collection": {
"url_query_params": {"mode": "allowlist", "terms": ["theme"]}
}
}
},
"token=[Filtered]&theme=dark&lang=[Filtered]&session=[Filtered]",
"http://example.com/foo?token=[Filtered]&theme=dark&lang=[Filtered]&session=[Filtered]",
id="data_collection_allowlist",
),
pytest.param(
{
"_experiments": {
"data_collection": {"url_query_params": {"mode": "off"}}
}
},
NO_QUERY_STRING,
"http://example.com/foo",
id="data_collection_off",
),
pytest.param(
{
"send_default_pii": True,
"_experiments": {
"data_collection": {"url_query_params": {"mode": "off"}}
},
},
NO_QUERY_STRING,
"http://example.com/foo",
id="data_collection_wins_over_send_default_pii",
),
],
)
def test_get_request_attributes_query_data_collection(
sentry_init, init_kwargs, expected_query, expected_url_full
):
sentry_init(**init_kwargs)

attributes = _get_request_attributes(_http_scope(), _RootPathInPath.EXCLUDED)

if expected_query is NO_QUERY_STRING:
assert "http.query" not in attributes
else:
assert attributes["http.query"] == expected_query

if expected_url_full is None:
assert "url.full" not in attributes
assert "url.path" not in attributes
else:
assert attributes["url.full"] == expected_url_full
assert attributes["url.path"] == "/foo"


@pytest.mark.asyncio
@pytest.mark.parametrize(
"request_url,transaction_style,expected_transaction_name,expected_transaction_source",
Expand Down
Loading
Loading