Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
5a2c705
feat(django): Gate user identity behind data_collection config
ericapisani Jul 23, 2026
4ee42d0
test(django): Run user identity ASGI test in a forked process
ericapisani Jul 23, 2026
86566bd
ref(tests): Share data_collection user info test matrix across suites
ericapisani Jul 23, 2026
057caa4
feat(sanic): Gate user info behind data_collection config
ericapisani Jul 23, 2026
c7cb8f8
feat(tornado): Gate user info behind data_collection config
ericapisani Jul 23, 2026
6918597
fix unbound var
ericapisani Jul 23, 2026
9038fe5
lint
ericapisani Jul 23, 2026
7377073
feat(pyramid): Gate user info behind data_collection config
ericapisani Jul 23, 2026
1634927
feat(aiohttp): Gate user info behind data_collection config
ericapisani Jul 23, 2026
093a47e
feat(starlite,litestar): Gate user info behind data_collection config
ericapisani Jul 23, 2026
6e3c9ca
feat(asgi): Gate user info behind data_collection config
ericapisani Jul 23, 2026
64e29e9
feat(quart): Gate user info behind data_collection config
ericapisani Jul 23, 2026
f409f02
feat(starlette): Gate user info behind data_collection config
ericapisani Jul 23, 2026
11fb505
feat(aws_lambda): Gate user info behind data_collection config
ericapisani Jul 23, 2026
d28ee71
test(aws_lambda): Place identity at top level of test event payloads
ericapisani Jul 23, 2026
2fbf67a
fix lambda tests
ericapisani Jul 24, 2026
c210fa3
.
ericapisani Jul 24, 2026
236efc7
test(aws_lambda): Remove trailing commas from invoke payloads
ericapisani Jul 24, 2026
c265efd
.
ericapisani Jul 24, 2026
077e667
.
ericapisani Jul 24, 2026
350f483
.
ericapisani Jul 24, 2026
2dd69af
fix bug found in aws lambda
ericapisani Jul 24, 2026
72bc9db
Merge branch 'master' into py-2583-misc
ericapisani Jul 24, 2026
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
24 changes: 18 additions & 6 deletions sentry_sdk/integrations/aiohttp.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,8 @@ async def sentry_app_handle(
)

url_attributes = {}
client_address_attributes = {}

if has_data_collection_enabled(client.options):
url_attributes["url.full"] = "%s://%s%s" % (
request.scheme,
Expand All @@ -188,6 +190,15 @@ async def sentry_app_handle(
"?" + filtered_query_string
)

if request.remote:
if client.options["data_collection"]["user_info"]:
client_address_attributes["client.address"] = (
request.remote
)
scope.set_attribute(
SPANDATA.USER_IP_ADDRESS, request.remote
)

elif should_send_default_pii():
url_full = "%s://%s%s" % (
request.scheme,
Expand All @@ -201,12 +212,13 @@ async def sentry_app_handle(
url_attributes["url.full"] = url_full
url_attributes["url.path"] = request.path

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
)
if 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
Expand Down
16 changes: 12 additions & 4 deletions sentry_sdk/integrations/asgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
_get_installed_modules,
capture_internal_exceptions,
event_from_exception,
has_data_collection_enabled,
logger,
nullcontext,
qualname_from_function,
Expand Down Expand Up @@ -253,10 +254,17 @@ 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 scope.get("client"):
client_options = sentry_sdk.get_client().options
if has_data_collection_enabled(client_options):
if client_options["data_collection"]["user_info"]:
sentry_scope.set_attribute(
SPANDATA.USER_IP_ADDRESS, _get_ip(scope)
)
elif should_send_default_pii():
sentry_scope.set_attribute(
SPANDATA.USER_IP_ADDRESS, _get_ip(scope)
)

if ty in ("http", "websocket"):
if (
Expand Down
18 changes: 17 additions & 1 deletion sentry_sdk/integrations/aws_lambda.py
Original file line number Diff line number Diff line change
Expand Up @@ -437,7 +437,23 @@
if "headers" in aws_event:
request["headers"] = _filter_headers(aws_event["headers"])

if should_send_default_pii():
client_options = sentry_sdk.get_client().options
if has_data_collection_enabled(client_options):
if client_options["data_collection"]["user_info"]:
user_info = sentry_event.setdefault("user", {})

identity = aws_event.get("identity")
if identity is None:
identity = {}

id = identity.get("userArn")
if id is not None:
user_info.setdefault("id", id)

ip = identity.get("sourceIp")
if ip is not None:
user_info.setdefault("ip_address", ip)
elif should_send_default_pii():

Check warning on line 456 in sentry_sdk/integrations/aws_lambda.py

View check run for this annotation

@sentry/warden / warden: code-review

AWS Lambda body handling skipped when data_collection is enabled

When `has_data_collection_enabled` is true, the `elif should_send_default_pii()` branch is skipped, so request body is neither collected nor redacted.
Comment thread
ericapisani marked this conversation as resolved.
user_info = sentry_event.setdefault("user", {})

identity = aws_event.get("identity")
Expand Down
78 changes: 46 additions & 32 deletions sentry_sdk/integrations/django/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
capture_internal_exceptions,
ensure_integration_enabled,
event_from_exception,
has_data_collection_enabled,
logger,
transaction_from_function,
walk_exception_chain,
Expand Down Expand Up @@ -457,6 +458,39 @@ def _attempt_resolve_again(
_set_transaction_name_and_source(scope, transaction_style, request)


def _get_user_from_request_and_set_on_scope(request: "WSGIRequest") -> None:
user = getattr(request, "user", None)

# Evaluating a SimpleLazyObject in an async view can raise django.core.exceptions.SynchronousOnlyOperation.
# Exit early if the user has not been materialized yet.
is_lazy = isinstance(user, SimpleLazyObject)
if is_lazy and hasattr(request, "_cached_user"):
user = request._cached_user
elif is_lazy:
return

if user is None or not is_authenticated(user):
return

user_info = {}
try:
user_info["id"] = str(user.pk)
except Exception:
pass

try:
user_info["email"] = user.email
except Exception:
pass

try:
user_info["username"] = user.get_username()
except Exception:
pass

sentry_sdk.set_user(user_info)


def _after_get_response(request: "WSGIRequest") -> None:
client = sentry_sdk.get_client()
integration = client.get_integration(DjangoIntegration)
Expand All @@ -468,37 +502,12 @@ def _after_get_response(request: "WSGIRequest") -> None:
_attempt_resolve_again(request, scope, integration.transaction_style)

span_streaming = has_span_streaming_enabled(client.options)
if span_streaming and should_send_default_pii():
user = getattr(request, "user", None)

# Evaluating a SimpleLazyObject in an async view can raise django.core.exceptions.SynchronousOnlyOperation.
# Exit early if the user has not been materialized yet.
is_lazy = isinstance(user, SimpleLazyObject)
if is_lazy and hasattr(request, "_cached_user"):
user = request._cached_user
elif is_lazy:
return

if user is None or not is_authenticated(user):
return

user_info = {}
try:
user_info["id"] = str(user.pk)
except Exception:
pass

try:
user_info["email"] = user.email
except Exception:
pass

try:
user_info["username"] = user.get_username()
except Exception:
pass

sentry_sdk.set_user(user_info)
if span_streaming:
if has_data_collection_enabled(client.options):
if client.options["data_collection"]["user_info"]:
_get_user_from_request_and_set_on_scope(request)
elif should_send_default_pii():
_get_user_from_request_and_set_on_scope(request)


def _patch_get_response() -> None:
Expand Down Expand Up @@ -544,7 +553,12 @@ def wsgi_request_event_processor(event: "Event", hint: "dict[str, Any]") -> "Eve
with capture_internal_exceptions():
DjangoRequestExtractor(request).extract_into_event(event)

if should_send_default_pii():
client_options = sentry_sdk.get_client().options
if has_data_collection_enabled(client_options):
if client_options["data_collection"]["user_info"]:
with capture_internal_exceptions():
_set_user_info(request, event)
elif should_send_default_pii():
with capture_internal_exceptions():
_set_user_info(request, event)

Expand Down
8 changes: 7 additions & 1 deletion sentry_sdk/integrations/django/asgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from sentry_sdk.utils import (
capture_internal_exceptions,
ensure_integration_enabled,
has_data_collection_enabled,
)

if TYPE_CHECKING:
Expand Down Expand Up @@ -70,7 +71,12 @@ def asgi_request_event_processor(event: "Event", hint: "dict[str, Any]") -> "Eve
with capture_internal_exceptions():
DjangoRequestExtractor(request).extract_into_event(event)

if should_send_default_pii():
client_options = sentry_sdk.get_client().options
if has_data_collection_enabled(client_options):
if client_options["data_collection"]["user_info"]:
with capture_internal_exceptions():
_set_user_info(request, event)
elif should_send_default_pii():
with capture_internal_exceptions():
_set_user_info(request, event)

Expand Down
8 changes: 7 additions & 1 deletion sentry_sdk/integrations/litestar.py
Original file line number Diff line number Diff line change
Expand Up @@ -357,8 +357,14 @@ def retrieve_user_from_scope(scope: "LitestarScope") -> "Optional[dict[str, Any]
@ensure_integration_enabled(LitestarIntegration)
def exception_handler(exc: Exception, scope: "LitestarScope") -> None:
user_info: "Optional[dict[str, Any]]" = None
if should_send_default_pii():
client_options = sentry_sdk.get_client().options

if has_data_collection_enabled(client_options):
if client_options["data_collection"]["user_info"]:
user_info = retrieve_user_from_scope(scope)
elif should_send_default_pii():
user_info = retrieve_user_from_scope(scope)

if user_info and isinstance(user_info, dict):
sentry_scope = sentry_sdk.get_isolation_scope()
sentry_scope.set_user(user_info)
Expand Down
23 changes: 18 additions & 5 deletions sentry_sdk/integrations/pyramid.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
capture_internal_exceptions,
ensure_integration_enabled,
event_from_exception,
has_data_collection_enabled,
reraise,
)

Expand Down Expand Up @@ -86,10 +87,16 @@ def sentry_patched_call_view(

scope = sentry_sdk.get_isolation_scope()

if should_send_default_pii() and has_span_streaming_enabled(client.options):
user_id = authenticated_userid(request)
if user_id:
scope.set_user({"id": user_id})
if has_span_streaming_enabled(client.options):
if has_data_collection_enabled(client.options):
if client.options["data_collection"]["user_info"]:
user_id = authenticated_userid(request)
if user_id:
scope.set_user({"id": user_id})
elif should_send_default_pii():
user_id = authenticated_userid(request)
if user_id:
scope.set_user({"id": user_id})

scope.add_event_processor(
_make_event_processor(weakref.ref(request), integration)
Expand Down Expand Up @@ -229,7 +236,13 @@ def pyramid_event_processor(event: "Event", hint: "Dict[str, Any]") -> "Event":
with capture_internal_exceptions():
PyramidRequestExtractor(request).extract_into_event(event)

if should_send_default_pii():
client_options = sentry_sdk.get_client().options
if has_data_collection_enabled(client_options):
if client_options["data_collection"]["user_info"]:
with capture_internal_exceptions():
user_info = event.setdefault("user", {})
user_info.setdefault("id", authenticated_userid(request))
elif should_send_default_pii():
with capture_internal_exceptions():
user_info = event.setdefault("user", {})
user_info.setdefault("id", authenticated_userid(request))
Expand Down
35 changes: 31 additions & 4 deletions sentry_sdk/integrations/quart.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,9 +235,25 @@ async def _request_websocket_started(app: "Quart", **kwargs: "Any") -> None:
else parsed_url.url,
)

# TODO: Add the user properties that are seen in the branch below here once
# code is added to respect the `user_info` settings within the data collection
# configuration
if client_options["data_collection"]["user_info"]:
user_properties = {}

if len(request_websocket.access_route) >= 1:
segment.set_attribute(
"client.address", request_websocket.access_route[0]
)
user_properties["ip_address"] = request_websocket.access_route[
0
]

current_user_id = _get_current_user_id_from_quart()
if current_user_id:
user_properties["id"] = current_user_id

if user_properties:
existing_user_properties = scope._user or {}
scope.set_user({**existing_user_properties, **user_properties})

elif should_send_default_pii():
segment.set_attribute("url.full", request_websocket.url)
segment.set_attribute(
Expand Down Expand Up @@ -284,7 +300,18 @@ def inner(event: "Event", hint: "dict[str, Any]") -> "Event":
request_info["method"] = request.method
request_info["headers"] = _filter_headers(dict(request.headers))

if should_send_default_pii():
client_options = sentry_sdk.get_client().options
if has_data_collection_enabled(client_options):
if client_options["data_collection"]["user_info"]:
if len(request.access_route) >= 1:
request_info["env"] = {"REMOTE_ADDR": request.access_route[0]}

current_user_id = _get_current_user_id_from_quart()
if current_user_id:
user_info = event.setdefault("user", {})
user_info["id"] = current_user_id

elif should_send_default_pii():
if len(request.access_route) >= 1:
request_info["env"] = {"REMOTE_ADDR": request.access_route[0]}

Expand Down
18 changes: 13 additions & 5 deletions sentry_sdk/integrations/sanic.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,8 +196,12 @@ 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)
if request.remote_addr:
if has_data_collection_enabled(client.options):
if client.options["data_collection"]["user_info"]:
scope.set_attribute(SPANDATA.USER_IP_ADDRESS, request.remote_addr)
elif should_send_default_pii():
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
Expand Down Expand Up @@ -401,19 +405,23 @@ def _get_request_attributes(request: "Request") -> "Dict[str, Any]":
query=filtered_query or ""
).geturl()

if request.remote_addr:
if client_options["data_collection"]["user_info"]:
attributes[SPANDATA.CLIENT_ADDRESS] = request.remote_addr

elif should_send_default_pii():
attributes[SPANDATA.URL_FULL] = request.url
attributes["url.path"] = urlparts.path

if urlparts.query:
attributes[SPANDATA.HTTP_QUERY] = urlparts.query

if request.remote_addr:
attributes[SPANDATA.CLIENT_ADDRESS] = request.remote_addr

if urlparts.scheme:
attributes[SPANDATA.NETWORK_PROTOCOL_NAME] = urlparts.scheme

if should_send_default_pii() and request.remote_addr:
attributes[SPANDATA.CLIENT_ADDRESS] = request.remote_addr

return attributes


Expand Down
6 changes: 5 additions & 1 deletion sentry_sdk/integrations/starlette.py
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,11 @@ def _add_user_to_sentry_scope(scope: "Dict[str, Any]") -> None:
if "user" not in scope:
return

if not should_send_default_pii():
client_options = sentry_sdk.get_client().options
if has_data_collection_enabled(client_options):
if not client_options["data_collection"]["user_info"]:
return
elif not should_send_default_pii():
return

user_info: "Dict[str, Any]" = {}
Expand Down
Loading
Loading