Skip to content

Commit 8d442df

Browse files
sentrivanaclaude
andauthored
fix(span-first): Set user.ip_address on all streamed spans (#6434)
`user.ip_address` should be attached to all spans as per https://develop.sentry.dev/sdk/telemetry/spans/span-protocol/#common-attribute-keys, not just segments. Set it on the isolation scope so that it gets applied to all spans active within it. Had to move setting the attr in a couple integrations so that it's set early enough for the child spans to also get it. --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent e8d67d8 commit 8d442df

11 files changed

Lines changed: 232 additions & 15 deletions

File tree

sentry_sdk/integrations/_asgi_common.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,5 @@ def _get_request_attributes(asgi_scope: "Any") -> "dict[str, Any]":
133133
if client and should_send_default_pii():
134134
ip = _get_ip(asgi_scope)
135135
attributes["client.address"] = ip
136-
attributes["user.ip_address"] = ip
137136

138137
return attributes

sentry_sdk/integrations/aiohttp.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -165,14 +165,12 @@ async def sentry_app_handle(
165165
else {}
166166
)
167167

168-
client_address_attributes = (
169-
{
170-
"client.address": request.remote,
171-
"user.ip_address": request.remote,
172-
}
173-
if should_send_default_pii() and request.remote
174-
else {}
175-
)
168+
client_address_attributes = {}
169+
if should_send_default_pii() and request.remote:
170+
client_address_attributes["client.address"] = request.remote
171+
scope.set_attribute(
172+
SPANDATA.USER_IP_ADDRESS, request.remote
173+
)
176174

177175
span_ctx = sentry_sdk.traces.start_span(
178176
# If this name makes it to the UI, AIOHTTP's URL

sentry_sdk/integrations/asgi.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@
1212

1313
import sentry_sdk
1414
from sentry_sdk.api import continue_trace
15-
from sentry_sdk.consts import OP
15+
from sentry_sdk.consts import OP, SPANDATA
1616
from sentry_sdk.integrations._asgi_common import (
1717
_get_headers,
18+
_get_ip,
1819
_get_request_attributes,
1920
_get_request_data,
2021
_get_url,
@@ -23,7 +24,7 @@
2324
DEFAULT_HTTP_METHODS_TO_CAPTURE,
2425
nullcontext,
2526
)
26-
from sentry_sdk.scope import Scope
27+
from sentry_sdk.scope import Scope, should_send_default_pii
2728
from sentry_sdk.sessions import track_session
2829
from sentry_sdk.traces import (
2930
SOURCE_FOR_STYLE as SEGMENT_SOURCE_FOR_STYLE,
@@ -247,6 +248,11 @@ async def _run_app(
247248
"network.protocol.name": ty,
248249
}
249250

251+
if scope.get("client") and should_send_default_pii():
252+
sentry_scope.set_attribute(
253+
SPANDATA.USER_IP_ADDRESS, _get_ip(scope)
254+
)
255+
250256
if ty in ("http", "websocket"):
251257
if (
252258
ty == "websocket"

sentry_sdk/integrations/sanic.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,9 @@ async def _context_enter(request: "Request") -> None:
181181
sentry_sdk.traces.continue_trace(dict(request.headers))
182182
scope.set_custom_sampling_context({"sanic_request": request})
183183

184+
if should_send_default_pii() and request.remote_addr:
185+
scope.set_attribute(SPANDATA.USER_IP_ADDRESS, request.remote_addr)
186+
184187
span = sentry_sdk.traces.start_span(
185188
# Unless the request results in a 404 error, the name and source
186189
# will get overwritten in _set_transaction
@@ -375,7 +378,6 @@ def _get_request_attributes(request: "Request") -> "Dict[str, Any]":
375378

376379
if should_send_default_pii() and request.remote_addr:
377380
attributes[SPANDATA.CLIENT_ADDRESS] = request.remote_addr
378-
attributes[SPANDATA.USER_IP_ADDRESS] = request.remote_addr
379381

380382
return attributes
381383

sentry_sdk/integrations/tornado.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,9 @@ def _handle_request_impl(self: "RequestHandler") -> "Generator[None, None, None]
130130
sentry_sdk.traces.continue_trace(dict(headers))
131131
scope.set_custom_sampling_context({"tornado_request": self.request})
132132

133+
if should_send_default_pii() and self.request.remote_ip:
134+
scope.set_attribute(SPANDATA.USER_IP_ADDRESS, self.request.remote_ip)
135+
133136
span_ctx = sentry_sdk.traces.start_span(
134137
name=_DEFAULT_ROOT_SPAN_NAME,
135138
attributes={
@@ -204,7 +207,6 @@ def _get_request_attributes(request: "Any") -> "Dict[str, Any]":
204207

205208
if should_send_default_pii() and request.remote_ip:
206209
attributes[SPANDATA.CLIENT_ADDRESS] = request.remote_ip
207-
attributes[SPANDATA.USER_IP_ADDRESS] = request.remote_ip
208210

209211
with capture_internal_exceptions():
210212
raw_data = _get_tornado_request_data(request)

sentry_sdk/integrations/wsgi.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import sentry_sdk
66
from sentry_sdk._werkzeug import _get_headers, get_host
77
from sentry_sdk.api import continue_trace
8-
from sentry_sdk.consts import OP
8+
from sentry_sdk.consts import OP, SPANDATA
99
from sentry_sdk.integrations._wsgi_common import (
1010
DEFAULT_HTTP_METHODS_TO_CAPTURE,
1111
_filter_headers,
@@ -134,6 +134,13 @@ def __call__(
134134
)
135135
Scope.set_custom_sampling_context({"wsgi_environ": environ})
136136

137+
if should_send_default_pii():
138+
client_ip = get_client_ip(environ)
139+
if client_ip:
140+
scope.set_attribute(
141+
SPANDATA.USER_IP_ADDRESS, client_ip
142+
)
143+
137144
span_ctx = sentry_sdk.traces.start_span(
138145
name=_DEFAULT_TRANSACTION_NAME,
139146
attributes={
@@ -412,6 +419,5 @@ def _get_request_attributes(
412419
client_ip = get_client_ip(environ)
413420
if client_ip:
414421
attributes["client.address"] = client_ip
415-
attributes["user.ip_address"] = client_ip
416422

417423
return attributes

tests/integrations/aiohttp/test_aiohttp.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1583,3 +1583,40 @@ async def handler(request):
15831583
span_id=client_span["span_id"],
15841584
sampled=1,
15851585
)
1586+
1587+
1588+
@pytest.mark.asyncio
1589+
@pytest.mark.parametrize("send_default_pii", [True, False])
1590+
async def test_user_ip_address_on_all_spans(
1591+
sentry_init, aiohttp_client, capture_items, send_default_pii
1592+
):
1593+
sentry_init(
1594+
integrations=[AioHttpIntegration()],
1595+
traces_sample_rate=1.0,
1596+
send_default_pii=send_default_pii,
1597+
_experiments={"trace_lifecycle": "stream"},
1598+
)
1599+
1600+
async def hello(request):
1601+
with sentry_sdk.traces.start_span(name="child-span"):
1602+
pass
1603+
return web.Response(text="hello")
1604+
1605+
app = web.Application()
1606+
app.router.add_get("/", hello)
1607+
1608+
items = capture_items("span")
1609+
1610+
client = await aiohttp_client(app)
1611+
await client.get("/")
1612+
1613+
sentry_sdk.flush()
1614+
1615+
child_span, server_span, client_span = [item.payload for item in items]
1616+
1617+
if send_default_pii:
1618+
assert server_span["attributes"]["user.ip_address"] == "127.0.0.1"
1619+
assert child_span["attributes"]["user.ip_address"] == "127.0.0.1"
1620+
else:
1621+
assert "user.ip_address" not in server_span["attributes"]
1622+
assert "user.ip_address" not in child_span["attributes"]

tests/integrations/asgi/test_asgi.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1002,3 +1002,59 @@ async def test_custom_transaction_name(
10021002
assert transaction_event["type"] == "transaction"
10031003
assert transaction_event["transaction"] == "foobar"
10041004
assert transaction_event["transaction_info"] == {"source": "custom"}
1005+
1006+
1007+
@pytest.mark.asyncio
1008+
@pytest.mark.parametrize("send_default_pii", [True, False])
1009+
async def test_user_ip_address_on_all_spans(
1010+
sentry_init,
1011+
capture_items,
1012+
send_default_pii,
1013+
):
1014+
async def app(scope, receive, send):
1015+
if scope["type"] == "lifespan":
1016+
while True:
1017+
message = await receive()
1018+
if message["type"] == "lifespan.startup":
1019+
await send({"type": "lifespan.startup.complete"})
1020+
elif message["type"] == "lifespan.shutdown":
1021+
await send({"type": "lifespan.shutdown.complete"})
1022+
return
1023+
1024+
with sentry_sdk.traces.start_span(name="child-span"):
1025+
pass
1026+
1027+
await send(
1028+
{
1029+
"type": "http.response.start",
1030+
"status": 200,
1031+
"headers": [[b"content-type", b"text/plain"]],
1032+
}
1033+
)
1034+
await send({"type": "http.response.body", "body": b"Hello, world!"})
1035+
1036+
sentry_init(
1037+
send_default_pii=send_default_pii,
1038+
traces_sample_rate=1.0,
1039+
_experiments={"trace_lifecycle": "stream"},
1040+
)
1041+
sentry_app = SentryAsgiMiddleware(app)
1042+
1043+
async def wrapped_app(scope, receive, send):
1044+
scope["client"] = ("127.0.0.1", 0)
1045+
await sentry_app(scope, receive, send)
1046+
1047+
async with TestClient(wrapped_app) as client:
1048+
items = capture_items("span")
1049+
await client.get("/some_url")
1050+
1051+
sentry_sdk.flush()
1052+
1053+
child_span, server_span = [item.payload for item in items]
1054+
1055+
if send_default_pii:
1056+
assert server_span["attributes"]["user.ip_address"] == "127.0.0.1"
1057+
assert child_span["attributes"]["user.ip_address"] == "127.0.0.1"
1058+
else:
1059+
assert "user.ip_address" not in server_span["attributes"]
1060+
assert "user.ip_address" not in child_span["attributes"]

tests/integrations/sanic/test_sanic.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -541,3 +541,47 @@ def test_span_origin(sentry_init, app, capture_events, capture_items, span_strea
541541
else:
542542
(_, event) = events
543543
assert event["contexts"]["trace"]["origin"] == "auto.http.sanic"
544+
545+
546+
@pytest.mark.skipif(
547+
not PERFORMANCE_SUPPORTED, reason="Performance not supported on this Sanic version"
548+
)
549+
@pytest.mark.parametrize("send_default_pii", [True, False])
550+
def test_user_ip_address_on_all_spans(
551+
sentry_init, app, capture_items, send_default_pii
552+
):
553+
app.config.FORWARDED_SECRET = "test"
554+
555+
@app.route("/child-span")
556+
def child_span_handler(request):
557+
with sentry_sdk.traces.start_span(name="child-span"):
558+
pass
559+
return response.text("ok")
560+
561+
sentry_init(
562+
integrations=[SanicIntegration()],
563+
default_integrations=False,
564+
traces_sample_rate=1.0,
565+
send_default_pii=send_default_pii,
566+
_experiments={"trace_lifecycle": "stream"},
567+
)
568+
569+
items = capture_items("span")
570+
571+
c = get_client(app)
572+
with c as client:
573+
client.get(
574+
"/child-span",
575+
headers={"Forwarded": "for=127.0.0.1;secret=test"},
576+
)
577+
578+
sentry_sdk.flush()
579+
580+
child_span, server_span = [item.payload for item in items]
581+
582+
if send_default_pii:
583+
assert server_span["attributes"]["user.ip_address"] == "127.0.0.1"
584+
assert child_span["attributes"]["user.ip_address"] == "127.0.0.1"
585+
else:
586+
assert "user.ip_address" not in server_span["attributes"]
587+
assert "user.ip_address" not in child_span["attributes"]

tests/integrations/tornado/test_tornado.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,13 @@ async def post(self):
6262
return b"hello"
6363

6464

65+
class ChildSpanHandler(RequestHandler):
66+
def get(self):
67+
with sentry_sdk.traces.start_span(name="child-span"):
68+
pass
69+
self.write("ok")
70+
71+
6572
def test_basic(tornado_testcase, sentry_init, capture_events):
6673
sentry_init(integrations=[TornadoIntegration()], send_default_pii=True)
6774
events = capture_events()
@@ -527,3 +534,31 @@ def test_span_origin(
527534
else:
528535
(_, event) = events
529536
assert event["contexts"]["trace"]["origin"] == "auto.http.tornado"
537+
538+
539+
@pytest.mark.parametrize("send_default_pii", [True, False])
540+
def test_user_ip_address_on_all_spans(
541+
tornado_testcase, sentry_init, capture_items, send_default_pii
542+
):
543+
sentry_init(
544+
integrations=[TornadoIntegration()],
545+
traces_sample_rate=1.0,
546+
send_default_pii=send_default_pii,
547+
_experiments={"trace_lifecycle": "stream"},
548+
)
549+
550+
items = capture_items("span")
551+
552+
client = tornado_testcase(Application([(r"/hi", ChildSpanHandler)]))
553+
client.fetch("/hi")
554+
555+
sentry_sdk.flush()
556+
557+
child_span, server_span = [item.payload for item in items]
558+
559+
if send_default_pii:
560+
assert server_span["attributes"]["user.ip_address"] == "127.0.0.1"
561+
assert child_span["attributes"]["user.ip_address"] == "127.0.0.1"
562+
else:
563+
assert "user.ip_address" not in server_span["attributes"]
564+
assert "user.ip_address" not in child_span["attributes"]

0 commit comments

Comments
 (0)