Skip to content

Commit 72a57de

Browse files
authored
fix(flask): Set user data on scope at request start (#6566)
Previously, Flask user data (id, email, username) was only set via the `_add_user_to_event` event processor, which runs at event capture time. Under span streaming, spans are sent before an error event is captured, so user attributes were missing from those spans. This refactors the user property logic into a shared `_get_flask_user_properties()` helper and calls it in `_request_started` to set user data directly on the isolation scope. The event processor continues to use the same helper to keep behavior consistent for non-streaming cases. Tests are extended to cover both span streaming and non-streaming paths. Fixes GH-6565 Refs PY-2528
1 parent 6a4c3a1 commit 72a57de

2 files changed

Lines changed: 82 additions & 38 deletions

File tree

sentry_sdk/integrations/flask.py

Lines changed: 46 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,13 @@ def _request_started(app: "Flask", **kwargs: "Any") -> None:
155155
)
156156

157157
scope = sentry_sdk.get_isolation_scope()
158+
159+
if should_send_default_pii():
160+
with capture_internal_exceptions():
161+
user_properties = _get_flask_user_properties()
162+
if user_properties:
163+
scope.set_user(user_properties)
164+
158165
evt_processor = _make_request_event_processor(app, request, integration)
159166
scope.add_event_processor(evt_processor)
160167

@@ -223,43 +230,52 @@ def _capture_exception(
223230
sentry_sdk.capture_event(event, hint=hint)
224231

225232

226-
def _add_user_to_event(event: "Event") -> None:
233+
def _get_flask_user_properties() -> "Dict[str, str]":
227234
if flask_login is None:
228-
return
235+
return {}
229236

230237
user = flask_login.current_user
231238
if user is None:
232-
return
239+
return {}
233240

234-
with capture_internal_exceptions():
235-
# Access this object as late as possible as accessing the user
236-
# is relatively costly
241+
properties = {}
237242

238-
user_info = event.setdefault("user", {})
243+
try:
244+
user_id = user.get_id()
245+
if user_id is not None:
246+
properties["id"] = user_id
247+
except AttributeError:
248+
# might happen if:
249+
# - flask_login could not be imported
250+
# - flask_login is not configured
251+
# - no user is logged in
252+
pass
239253

240-
try:
241-
user_info.setdefault("id", user.get_id())
242-
# TODO: more configurable user attrs here
243-
except AttributeError:
244-
# might happen if:
245-
# - flask_login could not be imported
246-
# - flask_login is not configured
247-
# - no user is logged in
248-
pass
254+
# The following attribute accesses are ineffective for the general
255+
# Flask-Login case, because the User interface of Flask-Login does not
256+
# care about anything but the ID. However, Flask-User (based on
257+
# Flask-Login) documents a few optional extra attributes.
258+
#
259+
# https://github.com/lingthio/Flask-User/blob/a379fa0a281789618c484b459cb41236779b95b1/docs/source/data_models.rst#fixed-data-model-property-names
260+
try:
261+
if user.email is not None:
262+
properties["email"] = user.email
263+
except Exception:
264+
pass
249265

250-
# The following attribute accesses are ineffective for the general
251-
# Flask-Login case, because the User interface of Flask-Login does not
252-
# care about anything but the ID. However, Flask-User (based on
253-
# Flask-Login) documents a few optional extra attributes.
254-
#
255-
# https://github.com/lingthio/Flask-User/blob/a379fa0a281789618c484b459cb41236779b95b1/docs/source/data_models.rst#fixed-data-model-property-names
266+
try:
267+
if user.username is not None:
268+
properties["username"] = user.username
269+
except Exception:
270+
pass
256271

257-
try:
258-
user_info.setdefault("email", user.email)
259-
except Exception:
260-
pass
272+
return properties
261273

262-
try:
263-
user_info.setdefault("username", user.username)
264-
except Exception:
265-
pass
274+
275+
def _add_user_to_event(event: "Event") -> None:
276+
with capture_internal_exceptions():
277+
user_properties = _get_flask_user_properties()
278+
if user_properties:
279+
user_info = event.setdefault("user", {})
280+
for key, value in user_properties.items():
281+
user_info.setdefault(key, value)

tests/integrations/flask/test_flask.py

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,7 @@ def test_flask_login_partially_configured(
219219
assert event.get("user", {}).get("id") is None
220220

221221

222+
@pytest.mark.parametrize("span_streaming", [True, False])
222223
@pytest.mark.parametrize("send_default_pii", [True, False])
223224
@pytest.mark.parametrize("user_id", [None, "42", 3])
224225
def test_flask_login_configured(
@@ -227,14 +228,26 @@ def test_flask_login_configured(
227228
app,
228229
user_id,
229230
capture_events,
231+
capture_items,
230232
monkeypatch,
231233
integration_enabled_params,
234+
span_streaming,
232235
):
233-
sentry_init(send_default_pii=send_default_pii, **integration_enabled_params)
236+
if span_streaming:
237+
sentry_init(
238+
integrations=[flask_sentry.FlaskIntegration()],
239+
send_default_pii=send_default_pii,
240+
traces_sample_rate=1.0,
241+
_experiments={"trace_lifecycle": "stream"},
242+
)
243+
else:
244+
sentry_init(send_default_pii=send_default_pii, **integration_enabled_params)
234245

235246
class User:
236247
is_authenticated = is_active = True
237248
is_anonymous = user_id is not None
249+
email = "user@example.com"
250+
username = "testuser"
238251

239252
def get_id(self):
240253
return str(user_id)
@@ -250,19 +263,34 @@ def login():
250263
login_user(User())
251264
return "ok"
252265

253-
events = capture_events()
266+
if span_streaming:
267+
items = capture_items("event", "span")
268+
else:
269+
events = capture_events()
254270

255271
client = app.test_client()
256272
assert client.get("/login").status_code == 200
257-
assert not events
258-
259273
assert client.get("/message").status_code == 200
260274

261-
(event,) = events
262-
if user_id is None or not send_default_pii:
263-
assert event.get("user", {}).get("id") is None
275+
if span_streaming:
276+
sentry_sdk.flush()
277+
spans = [i.payload for i in items if i.type == "span"]
278+
segment = next(s for s in spans if s["name"] == "hi")
279+
280+
if send_default_pii and user_id is not None:
281+
assert segment["attributes"]["user.id"] == str(user_id)
282+
assert segment["attributes"]["user.email"] == "user@example.com"
283+
assert segment["attributes"]["user.name"] == "testuser"
284+
else:
285+
assert "user.id" not in segment.get("attributes", {})
264286
else:
265-
assert event["user"]["id"] == str(user_id)
287+
(event,) = events
288+
if user_id is None or not send_default_pii:
289+
assert event.get("user", {}).get("id") is None
290+
else:
291+
assert event["user"]["id"] == str(user_id)
292+
assert event["user"]["email"] == "user@example.com"
293+
assert event["user"]["username"] == "testuser"
266294

267295

268296
@pytest.mark.parametrize("max_value_length", [1024, None])

0 commit comments

Comments
 (0)