diff --git a/sentry_sdk/integrations/_asgi_common.py b/sentry_sdk/integrations/_asgi_common.py index f1c660b6fb..020189acea 100644 --- a/sentry_sdk/integrations/_asgi_common.py +++ b/sentry_sdk/integrations/_asgi_common.py @@ -177,7 +177,7 @@ def _get_request_attributes( filtered_headers = _filter_headers(headers) for header, value in filtered_headers.items(): - attributes[f"http.request.header.{header.lower()}"] = value + attributes[f"http.request.header.{header.lower()}"] = [value] if has_data_collection_enabled(client_options): filtered_query_string = None diff --git a/sentry_sdk/integrations/aiohttp.py b/sentry_sdk/integrations/aiohttp.py index dbecb182f1..82a7707872 100644 --- a/sentry_sdk/integrations/aiohttp.py +++ b/sentry_sdk/integrations/aiohttp.py @@ -150,9 +150,9 @@ async def sentry_app_handle( header_attributes: "dict[str, Any]" = {} for header, header_value in _filter_headers(headers).items(): - header_attributes[f"http.request.header.{header.lower()}"] = ( + header_attributes[f"http.request.header.{header.lower()}"] = [ header_value - ) + ] url_attributes = {} client_address_attributes = {} diff --git a/sentry_sdk/integrations/aws_lambda.py b/sentry_sdk/integrations/aws_lambda.py index aed1c95c8f..8f2676ff52 100644 --- a/sentry_sdk/integrations/aws_lambda.py +++ b/sentry_sdk/integrations/aws_lambda.py @@ -167,9 +167,9 @@ def sentry_handler( header_attributes: "dict[str, Any]" = {} for header, header_value in _filter_headers(headers).items(): - header_attributes[f"http.request.header.{header.lower()}"] = ( + header_attributes[f"http.request.header.{header.lower()}"] = [ header_value - ) + ] additional_attributes: "dict[str, Any]" = {} if "httpMethod" in request_data: diff --git a/sentry_sdk/integrations/chalice.py b/sentry_sdk/integrations/chalice.py index 453b245b41..57e6e06f46 100644 --- a/sentry_sdk/integrations/chalice.py +++ b/sentry_sdk/integrations/chalice.py @@ -95,7 +95,7 @@ def wrapped_view_function(**function_args: "Any") -> "Any": header_attrs: "Dict[str, Any]" = {} for header, value in _filter_headers(headers).items(): - header_attrs[f"http.request.header.{header.lower()}"] = value + header_attrs[f"http.request.header.{header.lower()}"] = [value] additional_attrs: "Dict[str, Any]" = {} if "method" in request_dict: diff --git a/sentry_sdk/integrations/gcp.py b/sentry_sdk/integrations/gcp.py index 9862cb73fd..db1a24af92 100644 --- a/sentry_sdk/integrations/gcp.py +++ b/sentry_sdk/integrations/gcp.py @@ -87,9 +87,9 @@ def sentry_func( if hasattr(gcp_event, "headers"): headers = gcp_event.headers for header, header_value in _filter_headers(headers).items(): - header_attributes[f"http.request.header.{header.lower()}"] = ( + header_attributes[f"http.request.header.{header.lower()}"] = [ header_value - ) + ] additional_attributes = {} if hasattr(gcp_event, "method"): diff --git a/sentry_sdk/integrations/quart.py b/sentry_sdk/integrations/quart.py index 05083f7926..23ce60b75e 100644 --- a/sentry_sdk/integrations/quart.py +++ b/sentry_sdk/integrations/quart.py @@ -188,7 +188,7 @@ async def _request_websocket_started(app: "Quart", **kwargs: "Any") -> None: for header, header_value in _filter_headers( dict(request_websocket.headers) ).items(): - header_attributes[f"http.request.header.{header.lower()}"] = header_value + header_attributes[f"http.request.header.{header.lower()}"] = [header_value] segment.set_attributes(header_attributes) diff --git a/sentry_sdk/integrations/sanic.py b/sentry_sdk/integrations/sanic.py index e18678aac9..a0279e9be4 100644 --- a/sentry_sdk/integrations/sanic.py +++ b/sentry_sdk/integrations/sanic.py @@ -262,7 +262,7 @@ def _get_request_attributes(request: "Request") -> "Dict[str, Any]": headers = _filter_headers(dict(request.headers)) for header, value in headers.items(): - attributes[f"{SPANDATA.HTTP_REQUEST_HEADER}.{header.lower()}"] = value + attributes[f"{SPANDATA.HTTP_REQUEST_HEADER}.{header.lower()}"] = [value] urlparts = urlsplit(request.url) client_options = sentry_sdk.get_client().options diff --git a/sentry_sdk/integrations/tornado.py b/sentry_sdk/integrations/tornado.py index af32b56039..369b1983cb 100644 --- a/sentry_sdk/integrations/tornado.py +++ b/sentry_sdk/integrations/tornado.py @@ -166,7 +166,7 @@ def _get_request_attributes(request: "Any") -> "Dict[str, Any]": headers = _filter_headers(dict(request.headers)) for header, value in headers.items(): - attributes[f"{SPANDATA.HTTP_REQUEST_HEADER}.{header.lower()}"] = value + attributes[f"{SPANDATA.HTTP_REQUEST_HEADER}.{header.lower()}"] = [value] attributes["url.path"] = request.path diff --git a/sentry_sdk/integrations/wsgi.py b/sentry_sdk/integrations/wsgi.py index 9ab596078b..b63d0335fd 100644 --- a/sentry_sdk/integrations/wsgi.py +++ b/sentry_sdk/integrations/wsgi.py @@ -417,7 +417,7 @@ def _get_request_attributes( headers = _filter_headers(dict(_get_headers(environ))) for header, value in headers.items(): - attributes[f"http.request.header.{header.lower()}"] = value + attributes[f"http.request.header.{header.lower()}"] = [value] url_scheme = environ.get("wsgi.url_scheme") if url_scheme: diff --git a/tests/integrations/aiohttp/test_aiohttp.py b/tests/integrations/aiohttp/test_aiohttp.py index 3d21ae798e..7537cdaf34 100644 --- a/tests/integrations/aiohttp/test_aiohttp.py +++ b/tests/integrations/aiohttp/test_aiohttp.py @@ -1438,15 +1438,13 @@ async def hello(request): # Data collection always substitutes sensitive headers with # SENSITIVE_DATA_SUBSTITUTE ("[Filtered]"). The original token must not leak. - assert ( - server_span["attributes"]["http.request.header.authorization"] - == SENSITIVE_DATA_SUBSTITUTE - ) + assert server_span["attributes"]["http.request.header.authorization"] == [ + SENSITIVE_DATA_SUBSTITUTE + ] # Non-sensitive headers pass through untouched. - assert ( - server_span["attributes"]["http.request.header.x-custom-header"] - == "passthrough" - ) + assert server_span["attributes"]["http.request.header.x-custom-header"] == [ + "passthrough" + ] @pytest.mark.asyncio diff --git a/tests/integrations/asgi/test_asgi.py b/tests/integrations/asgi/test_asgi.py index bb2c364807..86024744a2 100644 --- a/tests/integrations/asgi/test_asgi.py +++ b/tests/integrations/asgi/test_asgi.py @@ -227,8 +227,8 @@ async def test_capture_transaction( assert span["attributes"]["network.protocol.name"] == "http" assert span["attributes"]["http.request.method"] == "GET" - assert span["attributes"]["http.request.header.host"] == "localhost" - assert span["attributes"]["http.request.header.user-agent"] == "ASGI-Test-Client" + assert span["attributes"]["http.request.header.host"] == ["localhost"] + assert span["attributes"]["http.request.header.user-agent"] == ["ASGI-Test-Client"] if should_send_pii: assert ( @@ -707,8 +707,8 @@ def test_get_headers(): pytest.param( {}, { - "http.request.header.authorization": "[Filtered]", - "http.request.header.x-custom-header": "passthrough", + "http.request.header.authorization": ["[Filtered]"], + "http.request.header.x-custom-header": ["passthrough"], }, id="default_redacts_sensitive_headers", ), @@ -720,18 +720,18 @@ def test_get_headers(): pytest.param( {"http_headers": {"request": {"mode": "allowlist", "terms": ["custom"]}}}, { - "http.request.header.x-custom-header": "passthrough", - "http.request.header.x-forwarded-for": "[Filtered]", - "http.request.header.host": "[Filtered]", + "http.request.header.x-custom-header": ["passthrough"], + "http.request.header.x-forwarded-for": ["[Filtered]"], + "http.request.header.host": ["[Filtered]"], }, id="allowlist_redacts_all_but_allowed_terms", ), pytest.param( {"http_headers": {"request": {"mode": "denylist", "terms": ["custom"]}}}, { - "http.request.header.x-custom-header": "[Filtered]", - "http.request.header.x-forwarded-for": "1.2.3.4", - "http.request.header.host": "localhost", + "http.request.header.x-custom-header": ["[Filtered]"], + "http.request.header.x-forwarded-for": ["1.2.3.4"], + "http.request.header.host": ["localhost"], }, id="denylist_redacts_only_matched_terms", ), @@ -798,8 +798,8 @@ async def test_request_headers_data_collection_cookie_always_redacted( (span,) = [item.payload for item in items] attributes = span["attributes"] - assert attributes["http.request.header.cookie"] == "[Filtered]" - assert attributes["http.request.header.x-custom-header"] == "passthrough" + assert attributes["http.request.header.cookie"] == ["[Filtered]"] + assert attributes["http.request.header.x-custom-header"] == ["passthrough"] @pytest.mark.asyncio @@ -828,7 +828,7 @@ async def test_get_request_attributes_url_with_filtered_host( assert len(items) == 1 attributes = items[0].payload["attributes"] - assert attributes["http.request.header.host"] == "[Filtered]" + assert attributes["http.request.header.host"] == ["[Filtered]"] assert attributes["url.full"] == "http://example.com/foo?somevalue=123" diff --git a/tests/integrations/aws_lambda/test_aws_lambda.py b/tests/integrations/aws_lambda/test_aws_lambda.py index da1d9bb60b..2af9432cb7 100644 --- a/tests/integrations/aws_lambda/test_aws_lambda.py +++ b/tests/integrations/aws_lambda/test_aws_lambda.py @@ -376,17 +376,17 @@ def test_request_data_with_data_collection_allowlist(lambda_client, test_environ assert _get_span_attr(attrs, "http.request.method") == "GET" # Allowlisted, non-sensitive headers pass through. - assert _get_span_attr(attrs, "http.request.header.user-agent") == "custom" - assert _get_span_attr(attrs, "http.request.header.x-allow-me") == "yes" + assert _get_span_attr(attrs, "http.request.header.user-agent") == ["custom"] + assert _get_span_attr(attrs, "http.request.header.x-allow-me") == ["yes"] # Not allowlisted -> filtered. - assert _get_span_attr(attrs, "http.request.header.host") == "[Filtered]" - assert ( - _get_span_attr(attrs, "http.request.header.x-forwarded-proto") == "[Filtered]" - ) + assert _get_span_attr(attrs, "http.request.header.host") == ["[Filtered]"] + assert _get_span_attr(attrs, "http.request.header.x-forwarded-proto") == [ + "[Filtered]" + ] # Allowlisted but sensitive -> still filtered. - assert _get_span_attr(attrs, "http.request.header.authorization") == "[Filtered]" + assert _get_span_attr(attrs, "http.request.header.authorization") == ["[Filtered]"] # Not allowlisted, and cookies are always filtered. - assert _get_span_attr(attrs, "http.request.header.cookie") == "[Filtered]" + assert _get_span_attr(attrs, "http.request.header.cookie") == ["[Filtered]"] def test_request_data_with_data_collection_denylist(lambda_client, test_environment): @@ -431,20 +431,19 @@ def test_request_data_with_data_collection_denylist(lambda_client, test_environm assert _get_span_attr(attrs, "http.request.method") == "GET" # Not denied by any term -> pass through. - assert ( - _get_span_attr(attrs, "http.request.header.host") - == "iwsz2c7uwi.execute-api.us-east-1.amazonaws.com" - ) - assert _get_span_attr(attrs, "http.request.header.x-custom") == "keep-me" + assert _get_span_attr(attrs, "http.request.header.host") == [ + "iwsz2c7uwi.execute-api.us-east-1.amazonaws.com" + ] + assert _get_span_attr(attrs, "http.request.header.x-custom") == ["keep-me"] # Denied by custom terms. - assert _get_span_attr(attrs, "http.request.header.user-agent") == "[Filtered]" - assert ( - _get_span_attr(attrs, "http.request.header.x-forwarded-proto") == "[Filtered]" - ) + assert _get_span_attr(attrs, "http.request.header.user-agent") == ["[Filtered]"] + assert _get_span_attr(attrs, "http.request.header.x-forwarded-proto") == [ + "[Filtered]" + ] # Denied by the built-in sensitive denylist. - assert _get_span_attr(attrs, "http.request.header.authorization") == "[Filtered]" + assert _get_span_attr(attrs, "http.request.header.authorization") == ["[Filtered]"] # Cookies are always filtered. - assert _get_span_attr(attrs, "http.request.header.cookie") == "[Filtered]" + assert _get_span_attr(attrs, "http.request.header.cookie") == ["[Filtered]"] def test_request_data_with_data_collection_off(lambda_client, test_environment): @@ -841,10 +840,10 @@ def test_request_attributes(lambda_client, test_environment): _get_span_attr(attrs, "url.query") == "foo=bar&a-complicated-value=a%3Db%26c%3Dd" ) - assert ( - _get_span_attr(attrs, "http.request.header.content-type") == "application/json" - ) - assert _get_span_attr(attrs, "http.request.header.accept") == "text/html" + assert _get_span_attr(attrs, "http.request.header.content-type") == [ + "application/json" + ] + assert _get_span_attr(attrs, "http.request.header.accept") == ["text/html"] assert _get_span_attr(attrs, "faas.name") == "BasicOkSpanStreamingDataCollection" assert _get_span_attr(attrs, "cloud.provider") == "aws" assert _get_span_attr(attrs, "cloud.platform") == "aws_lambda" diff --git a/tests/integrations/gcp/test_gcp.py b/tests/integrations/gcp/test_gcp.py index be2cd13aaf..c95122faf1 100644 --- a/tests/integrations/gcp/test_gcp.py +++ b/tests/integrations/gcp/test_gcp.py @@ -625,8 +625,8 @@ def cloud_function(functionhandler, event): assert attrs["http.request.method"] == "POST" assert attrs["url.query"] == "foo=bar" - assert attrs["http.request.header.content-type"] == "application/json" - assert attrs["http.request.header.accept"] == "text/html" + assert attrs["http.request.header.content-type"] == ["application/json"] + assert attrs["http.request.header.accept"] == ["text/html"] assert attrs["faas.name"] == "Google Cloud function" assert attrs["gcp.project.id"] == "serverless_project" assert attrs["faas.identity"] == "func_ID" diff --git a/tests/integrations/quart/test_quart.py b/tests/integrations/quart/test_quart.py index b5039e3dde..219133fffd 100644 --- a/tests/integrations/quart/test_quart.py +++ b/tests/integrations/quart/test_quart.py @@ -792,9 +792,9 @@ async def test_request_attributes( pytest.param( {}, { - "authorization": "[Filtered]", - "custom": "passthrough", - "cookie": "[Filtered]", + "authorization": ["[Filtered]"], + "custom": ["passthrough"], + "cookie": ["[Filtered]"], }, id="data_collection_default_redacts_auth_header", ), @@ -806,9 +806,9 @@ async def test_request_attributes( pytest.param( {"http_headers": {"request": {"mode": "allowlist"}}}, { - "authorization": "[Filtered]", - "custom": "[Filtered]", - "cookie": "[Filtered]", + "authorization": ["[Filtered]"], + "custom": ["[Filtered]"], + "cookie": ["[Filtered]"], }, id="data_collection_allow_list_redacts_terms_that_do_not_appear", ), @@ -819,36 +819,36 @@ async def test_request_attributes( } }, { - "authorization": "[Filtered]", - "custom": "[Filtered]", - "cookie": "[Filtered]", + "authorization": ["[Filtered]"], + "custom": ["[Filtered]"], + "cookie": ["[Filtered]"], }, id="data_collection_allow_list_redacts_sensitive_terms_even_when_provided_by_user", ), pytest.param( {"http_headers": {"request": {"mode": "allowlist", "terms": ["custom"]}}}, { - "authorization": "[Filtered]", - "custom": "passthrough", - "cookie": "[Filtered]", + "authorization": ["[Filtered]"], + "custom": ["passthrough"], + "cookie": ["[Filtered]"], }, id="data_collection_allow_list_does_not_redact_provided_term", ), pytest.param( {"http_headers": {"request": {"mode": "denylist", "terms": ["custom"]}}}, { - "authorization": "[Filtered]", - "custom": "[Filtered]", - "cookie": "[Filtered]", + "authorization": ["[Filtered]"], + "custom": ["[Filtered]"], + "cookie": ["[Filtered]"], }, id="data_collection_deny_list_redacts_sensitive_terms_when_provided_by_user", ), pytest.param( {"http_headers": {"request": {"mode": "allowlist", "terms": ["cookie"]}}}, { - "authorization": "[Filtered]", - "custom": "[Filtered]", - "cookie": "[Filtered]", + "authorization": ["[Filtered]"], + "custom": ["[Filtered]"], + "cookie": ["[Filtered]"], }, id="data_collection_cookie_is_always_redacted_even_when_allow_listed", ),