Skip to content

feat(django): Add user attributes in span streaming - #6541

Merged
alexander-alderman-webb merged 10 commits into
masterfrom
webb/django/user
Jun 10, 2026
Merged

feat(django): Add user attributes in span streaming#6541
alexander-alderman-webb merged 10 commits into
masterfrom
webb/django/user

use sentry_sdk.set_user()

a1e04a9
Select commit
Loading
Failed to load commit list.
@sentry/warden / warden completed Jun 10, 2026 in 11m 41s

5 issues

Medium

Span-streaming path uses `set_user()` which overwrites custom user fields, diverging from static-mode `setdefault` behavior - `sentry_sdk/integrations/django/__init__.py:499`

In _after_get_response, the span-streaming branch calls sentry_sdk.set_user(user_info), which routes to Scope.set_user and does a full replacement (self._user = value). Because _after_get_response runs after old_get_response() (the view/middleware), any user fields the application set during the request (e.g. segment, custom attributes, or an ip_address set elsewhere) are discarded. This diverges from the static-mode event processor _set_user_info(), which uses event.setdefault("user", {}) and user_info.setdefault(...) so application-provided fields win. Consider merging into the existing scope user (or matching the setdefault pattern) so span-streaming and static modes behave consistently.

No test covering the early return for un-materialized `SimpleLazyObject` user - `sentry_sdk/integrations/django/__init__.py:476`

The fix for issue #5274 (returning early when is_lazy=True and _cached_user is absent) has no test, so it could silently regress and re-introduce the SynchronousOnlyOperation crash in async views.

Also found at:

  • sentry_sdk/integrations/django/__init__.py:43

Low

SimpleLazyObject early-exit misses already-evaluated users when `_cached_user` is absent - `sentry_sdk/integrations/django/__init__.py:43`

The guard in _after_get_response checks hasattr(request, '_cached_user') to decide whether the lazy user has been materialized, but a SimpleLazyObject can be fully evaluated (its _wrapped attribute set) without _cached_user ever being set on the request — for example, with custom auth backends that wrap the user in their own lazy object. In that case elif is_lazy: return bails early and silently drops user attributes for an already-safe user object. Consider checking user._wrapped is not empty (importable from django.utils.functional) instead, which can be read without triggering evaluation.

`sentry_sdk.set_user({})` overwrites isolation-scope user when all attribute extractions fail - `sentry_sdk/integrations/django/__init__.py:500`

In _after_get_response, when span streaming and PII are enabled, the integration extracts user.pk, user.email, and user.get_username(), each wrapped in a silent try/except. If all three raise (e.g. a custom user model, deferred fields, or a SynchronousOnlyOperation when lazily evaluating attributes in an async context), user_info stays {} and sentry_sdk.set_user(user_info) is still called unconditionally. Unlike the event-based _set_user_info, which merges via event.setdefault/user_info.setdefault, this path calls Scope.set_user, which does self._user = value, fully replacing any user context an application previously set on the isolation scope with an empty dict. Adding an if user_info: guard before sentry_sdk.set_user would avoid clobbering existing user data.

Early return skips user attributes for an evaluated `SimpleLazyObject` lacking `request._cached_user` - `sentry_sdk/integrations/django/__init__.py:476`

In _after_get_response, when span streaming and PII are enabled, the code guards against evaluating an unmaterialized SimpleLazyObject (which can raise SynchronousOnlyOperation in async views) by checking hasattr(request, "_cached_user"). That attribute is set by Django's standard django.contrib.auth.middleware.get_user(). If a custom auth middleware sets request.user = SimpleLazyObject(custom_fn) where custom_fn never writes request._cached_user, then even after the object is fully materialized the elif is_lazy: return branch fires and silently skips setting user attributes on the span. Impact is limited to missing id/email/username attributes in non-standard configurations; there is no exception or security impact. In the standard Django auth flow _cached_user is populated, so the common case behaves correctly.

4 skills analyzed
Skill Findings Duration Cost
security-review 0 18.8s $0.04
code-review 3 10m 26s $2.27
find-bugs 2 7m 4s $1.75
skill-scanner 0 19.5s $0.01

⏱ 18m 9s · 4.6M in / 102.3k out · $4.07