diff --git a/sentry_sdk/integrations/_asgi_common.py b/sentry_sdk/integrations/_asgi_common.py index 9b9d7a7441..0aa88b100d 100644 --- a/sentry_sdk/integrations/_asgi_common.py +++ b/sentry_sdk/integrations/_asgi_common.py @@ -133,6 +133,5 @@ def _get_request_attributes(asgi_scope: "Any") -> "dict[str, Any]": if client and should_send_default_pii(): ip = _get_ip(asgi_scope) attributes["client.address"] = ip - attributes["user.ip_address"] = ip return attributes diff --git a/sentry_sdk/integrations/aiohttp.py b/sentry_sdk/integrations/aiohttp.py index 7c1da02afe..f905420d87 100644 --- a/sentry_sdk/integrations/aiohttp.py +++ b/sentry_sdk/integrations/aiohttp.py @@ -165,14 +165,12 @@ async def sentry_app_handle( else {} ) - client_address_attributes = ( - { - "client.address": request.remote, - "user.ip_address": request.remote, - } - if should_send_default_pii() and request.remote - else {} - ) + client_address_attributes = {} + if should_send_default_pii() and request.remote: + client_address_attributes["client.address"] = request.remote + scope.set_attribute( + SPANDATA.USER_IP_ADDRESS, request.remote + ) span_ctx = sentry_sdk.traces.start_span( # If this name makes it to the UI, AIOHTTP's URL diff --git a/sentry_sdk/integrations/asgi.py b/sentry_sdk/integrations/asgi.py index e36ce4a435..f0470e33fc 100644 --- a/sentry_sdk/integrations/asgi.py +++ b/sentry_sdk/integrations/asgi.py @@ -12,9 +12,10 @@ import sentry_sdk from sentry_sdk.api import continue_trace -from sentry_sdk.consts import OP +from sentry_sdk.consts import OP, SPANDATA from sentry_sdk.integrations._asgi_common import ( _get_headers, + _get_ip, _get_request_attributes, _get_request_data, _get_url, @@ -23,7 +24,7 @@ DEFAULT_HTTP_METHODS_TO_CAPTURE, nullcontext, ) -from sentry_sdk.scope import Scope +from sentry_sdk.scope import Scope, should_send_default_pii from sentry_sdk.sessions import track_session from sentry_sdk.traces import ( SOURCE_FOR_STYLE as SEGMENT_SOURCE_FOR_STYLE, @@ -247,6 +248,11 @@ async def _run_app( "network.protocol.name": ty, } + if scope.get("client") and should_send_default_pii(): + sentry_scope.set_attribute( + SPANDATA.USER_IP_ADDRESS, _get_ip(scope) + ) + if ty in ("http", "websocket"): if ( ty == "websocket" diff --git a/sentry_sdk/integrations/sanic.py b/sentry_sdk/integrations/sanic.py index 391272a16d..0325e77879 100644 --- a/sentry_sdk/integrations/sanic.py +++ b/sentry_sdk/integrations/sanic.py @@ -181,6 +181,9 @@ async def _context_enter(request: "Request") -> None: sentry_sdk.traces.continue_trace(dict(request.headers)) scope.set_custom_sampling_context({"sanic_request": request}) + if should_send_default_pii() and request.remote_addr: + scope.set_attribute(SPANDATA.USER_IP_ADDRESS, request.remote_addr) + span = sentry_sdk.traces.start_span( # Unless the request results in a 404 error, the name and source # will get overwritten in _set_transaction @@ -375,7 +378,6 @@ def _get_request_attributes(request: "Request") -> "Dict[str, Any]": if should_send_default_pii() and request.remote_addr: attributes[SPANDATA.CLIENT_ADDRESS] = request.remote_addr - attributes[SPANDATA.USER_IP_ADDRESS] = request.remote_addr return attributes diff --git a/sentry_sdk/integrations/tornado.py b/sentry_sdk/integrations/tornado.py index f4dc212967..0e0d465dd7 100644 --- a/sentry_sdk/integrations/tornado.py +++ b/sentry_sdk/integrations/tornado.py @@ -130,6 +130,9 @@ def _handle_request_impl(self: "RequestHandler") -> "Generator[None, None, None] sentry_sdk.traces.continue_trace(dict(headers)) scope.set_custom_sampling_context({"tornado_request": self.request}) + if should_send_default_pii() and self.request.remote_ip: + scope.set_attribute(SPANDATA.USER_IP_ADDRESS, self.request.remote_ip) + span_ctx = sentry_sdk.traces.start_span( name=_DEFAULT_ROOT_SPAN_NAME, attributes={ @@ -204,7 +207,6 @@ def _get_request_attributes(request: "Any") -> "Dict[str, Any]": if should_send_default_pii() and request.remote_ip: attributes[SPANDATA.CLIENT_ADDRESS] = request.remote_ip - attributes[SPANDATA.USER_IP_ADDRESS] = request.remote_ip with capture_internal_exceptions(): raw_data = _get_tornado_request_data(request) diff --git a/sentry_sdk/integrations/wsgi.py b/sentry_sdk/integrations/wsgi.py index 32dcfd702b..ff27f86f1b 100644 --- a/sentry_sdk/integrations/wsgi.py +++ b/sentry_sdk/integrations/wsgi.py @@ -5,7 +5,7 @@ import sentry_sdk from sentry_sdk._werkzeug import _get_headers, get_host from sentry_sdk.api import continue_trace -from sentry_sdk.consts import OP +from sentry_sdk.consts import OP, SPANDATA from sentry_sdk.integrations._wsgi_common import ( DEFAULT_HTTP_METHODS_TO_CAPTURE, _filter_headers, @@ -134,6 +134,13 @@ def __call__( ) Scope.set_custom_sampling_context({"wsgi_environ": environ}) + if should_send_default_pii(): + client_ip = get_client_ip(environ) + if client_ip: + scope.set_attribute( + SPANDATA.USER_IP_ADDRESS, client_ip + ) + span_ctx = sentry_sdk.traces.start_span( name=_DEFAULT_TRANSACTION_NAME, attributes={ @@ -412,6 +419,5 @@ def _get_request_attributes( client_ip = get_client_ip(environ) if client_ip: attributes["client.address"] = client_ip - attributes["user.ip_address"] = client_ip return attributes diff --git a/tests/integrations/aiohttp/test_aiohttp.py b/tests/integrations/aiohttp/test_aiohttp.py index a69dccebea..70f76204cc 100644 --- a/tests/integrations/aiohttp/test_aiohttp.py +++ b/tests/integrations/aiohttp/test_aiohttp.py @@ -1583,3 +1583,40 @@ async def handler(request): span_id=client_span["span_id"], sampled=1, ) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("send_default_pii", [True, False]) +async def test_user_ip_address_on_all_spans( + sentry_init, aiohttp_client, capture_items, send_default_pii +): + sentry_init( + integrations=[AioHttpIntegration()], + traces_sample_rate=1.0, + send_default_pii=send_default_pii, + _experiments={"trace_lifecycle": "stream"}, + ) + + async def hello(request): + with sentry_sdk.traces.start_span(name="child-span"): + pass + return web.Response(text="hello") + + app = web.Application() + app.router.add_get("/", hello) + + items = capture_items("span") + + client = await aiohttp_client(app) + await client.get("/") + + sentry_sdk.flush() + + child_span, server_span, client_span = [item.payload for item in items] + + if send_default_pii: + assert server_span["attributes"]["user.ip_address"] == "127.0.0.1" + assert child_span["attributes"]["user.ip_address"] == "127.0.0.1" + else: + assert "user.ip_address" not in server_span["attributes"] + assert "user.ip_address" not in child_span["attributes"] diff --git a/tests/integrations/asgi/test_asgi.py b/tests/integrations/asgi/test_asgi.py index be3e851f4e..dd0bc8b59d 100644 --- a/tests/integrations/asgi/test_asgi.py +++ b/tests/integrations/asgi/test_asgi.py @@ -1002,3 +1002,59 @@ async def test_custom_transaction_name( assert transaction_event["type"] == "transaction" assert transaction_event["transaction"] == "foobar" assert transaction_event["transaction_info"] == {"source": "custom"} + + +@pytest.mark.asyncio +@pytest.mark.parametrize("send_default_pii", [True, False]) +async def test_user_ip_address_on_all_spans( + sentry_init, + capture_items, + send_default_pii, +): + async def app(scope, receive, send): + if scope["type"] == "lifespan": + while True: + message = await receive() + if message["type"] == "lifespan.startup": + await send({"type": "lifespan.startup.complete"}) + elif message["type"] == "lifespan.shutdown": + await send({"type": "lifespan.shutdown.complete"}) + return + + with sentry_sdk.traces.start_span(name="child-span"): + pass + + await send( + { + "type": "http.response.start", + "status": 200, + "headers": [[b"content-type", b"text/plain"]], + } + ) + await send({"type": "http.response.body", "body": b"Hello, world!"}) + + sentry_init( + send_default_pii=send_default_pii, + traces_sample_rate=1.0, + _experiments={"trace_lifecycle": "stream"}, + ) + sentry_app = SentryAsgiMiddleware(app) + + async def wrapped_app(scope, receive, send): + scope["client"] = ("127.0.0.1", 0) + await sentry_app(scope, receive, send) + + async with TestClient(wrapped_app) as client: + items = capture_items("span") + await client.get("/some_url") + + sentry_sdk.flush() + + child_span, server_span = [item.payload for item in items] + + if send_default_pii: + assert server_span["attributes"]["user.ip_address"] == "127.0.0.1" + assert child_span["attributes"]["user.ip_address"] == "127.0.0.1" + else: + assert "user.ip_address" not in server_span["attributes"] + assert "user.ip_address" not in child_span["attributes"] diff --git a/tests/integrations/sanic/test_sanic.py b/tests/integrations/sanic/test_sanic.py index d27adf9f91..08c83d4870 100644 --- a/tests/integrations/sanic/test_sanic.py +++ b/tests/integrations/sanic/test_sanic.py @@ -541,3 +541,47 @@ def test_span_origin(sentry_init, app, capture_events, capture_items, span_strea else: (_, event) = events assert event["contexts"]["trace"]["origin"] == "auto.http.sanic" + + +@pytest.mark.skipif( + not PERFORMANCE_SUPPORTED, reason="Performance not supported on this Sanic version" +) +@pytest.mark.parametrize("send_default_pii", [True, False]) +def test_user_ip_address_on_all_spans( + sentry_init, app, capture_items, send_default_pii +): + app.config.FORWARDED_SECRET = "test" + + @app.route("/child-span") + def child_span_handler(request): + with sentry_sdk.traces.start_span(name="child-span"): + pass + return response.text("ok") + + sentry_init( + integrations=[SanicIntegration()], + default_integrations=False, + traces_sample_rate=1.0, + send_default_pii=send_default_pii, + _experiments={"trace_lifecycle": "stream"}, + ) + + items = capture_items("span") + + c = get_client(app) + with c as client: + client.get( + "/child-span", + headers={"Forwarded": "for=127.0.0.1;secret=test"}, + ) + + sentry_sdk.flush() + + child_span, server_span = [item.payload for item in items] + + if send_default_pii: + assert server_span["attributes"]["user.ip_address"] == "127.0.0.1" + assert child_span["attributes"]["user.ip_address"] == "127.0.0.1" + else: + assert "user.ip_address" not in server_span["attributes"] + assert "user.ip_address" not in child_span["attributes"] diff --git a/tests/integrations/tornado/test_tornado.py b/tests/integrations/tornado/test_tornado.py index d72421b8d1..230168adbe 100644 --- a/tests/integrations/tornado/test_tornado.py +++ b/tests/integrations/tornado/test_tornado.py @@ -62,6 +62,13 @@ async def post(self): return b"hello" +class ChildSpanHandler(RequestHandler): + def get(self): + with sentry_sdk.traces.start_span(name="child-span"): + pass + self.write("ok") + + def test_basic(tornado_testcase, sentry_init, capture_events): sentry_init(integrations=[TornadoIntegration()], send_default_pii=True) events = capture_events() @@ -527,3 +534,31 @@ def test_span_origin( else: (_, event) = events assert event["contexts"]["trace"]["origin"] == "auto.http.tornado" + + +@pytest.mark.parametrize("send_default_pii", [True, False]) +def test_user_ip_address_on_all_spans( + tornado_testcase, sentry_init, capture_items, send_default_pii +): + sentry_init( + integrations=[TornadoIntegration()], + traces_sample_rate=1.0, + send_default_pii=send_default_pii, + _experiments={"trace_lifecycle": "stream"}, + ) + + items = capture_items("span") + + client = tornado_testcase(Application([(r"/hi", ChildSpanHandler)])) + client.fetch("/hi") + + sentry_sdk.flush() + + child_span, server_span = [item.payload for item in items] + + if send_default_pii: + assert server_span["attributes"]["user.ip_address"] == "127.0.0.1" + assert child_span["attributes"]["user.ip_address"] == "127.0.0.1" + else: + assert "user.ip_address" not in server_span["attributes"] + assert "user.ip_address" not in child_span["attributes"] diff --git a/tests/integrations/wsgi/test_wsgi.py b/tests/integrations/wsgi/test_wsgi.py index a95a1d63fa..43dc301470 100644 --- a/tests/integrations/wsgi/test_wsgi.py +++ b/tests/integrations/wsgi/test_wsgi.py @@ -841,3 +841,35 @@ def app(environ, start_response): ) def test_get_request_url_x_forwarded_proto(environ, use_x_forwarded_for, expected_url): assert get_request_url(environ, use_x_forwarded_for) == expected_url + + +@pytest.mark.parametrize("send_default_pii", [True, False]) +def test_user_ip_address_on_all_spans(sentry_init, capture_items, send_default_pii): + def dogpark(environ, start_response): + with sentry_sdk.traces.start_span(name="child-span"): + pass + start_response("200 OK", []) + return ["Go get the ball! Good dog!"] + + sentry_init( + send_default_pii=send_default_pii, + traces_sample_rate=1.0, + _experiments={"trace_lifecycle": "stream"}, + ) + app = SentryWsgiMiddleware(dogpark) + client = Client(app) + + items = capture_items("span") + + client.get("/dogs/are/great/", environ_base={"REMOTE_ADDR": "127.0.0.1"}) + + sentry_sdk.flush() + + child_span, server_span = [item.payload for item in items] + + if send_default_pii: + assert server_span["attributes"]["user.ip_address"] == "127.0.0.1" + assert child_span["attributes"]["user.ip_address"] == "127.0.0.1" + else: + assert "user.ip_address" not in server_span["attributes"] + assert "user.ip_address" not in child_span["attributes"]