Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
77 changes: 47 additions & 30 deletions sentry_sdk/integrations/flask.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,14 @@ def _request_started(app: "Flask", **kwargs: "Any") -> None:
)

scope = sentry_sdk.get_isolation_scope()

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale user after auth changes

Medium Severity

Flask login user fields are copied onto the isolation scope only at request_started and are not updated when authentication changes later in the same request. After logout_user() (or similar), current_user is anonymous but scope user data remains, so streamed spans and events can still show the previous user.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 3e6a0ee. Configure here.


evt_processor = _make_request_event_processor(app, request, integration)
scope.add_event_processor(evt_processor)

Expand Down Expand Up @@ -223,43 +231,52 @@ def _capture_exception(
sentry_sdk.capture_event(event, hint=hint)


def _add_user_to_event(event: "Event") -> None:
def _get_flask_user_properties() -> "Dict[str, str]":
if flask_login is None:
return
return {}

user = flask_login.current_user
if user is None:
return
return {}

with capture_internal_exceptions():
# Access this object as late as possible as accessing the user
# is relatively costly
properties = {}

user_info = event.setdefault("user", {})
try:
user_id = user.get_id()
if user_id is not None:
properties["id"] = user_id
except AttributeError:
# might happen if:
# - flask_login could not be imported
# - flask_login is not configured
# - no user is logged in
pass

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

# The following attribute accesses are ineffective for the general
# Flask-Login case, because the User interface of Flask-Login does not
# care about anything but the ID. However, Flask-User (based on
# Flask-Login) documents a few optional extra attributes.
#
# https://github.com/lingthio/Flask-User/blob/a379fa0a281789618c484b459cb41236779b95b1/docs/source/data_models.rst#fixed-data-model-property-names
try:
if user.username is not None:
properties["username"] = user.username
except Exception:
Comment thread
sentry[bot] marked this conversation as resolved.
pass

try:
user_info.setdefault("email", user.email)
except Exception:
pass
return properties

try:
user_info.setdefault("username", user.username)
except Exception:
pass

def _add_user_to_event(event: "Event") -> None:
with capture_internal_exceptions():
user_properties = _get_flask_user_properties()
if user_properties:
user_info = event.setdefault("user", {})
for key, value in user_properties.items():
user_info.setdefault(key, value)
44 changes: 36 additions & 8 deletions tests/integrations/flask/test_flask.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ def test_flask_login_partially_configured(
assert event.get("user", {}).get("id") is None


@pytest.mark.parametrize("span_streaming", [True, False])
@pytest.mark.parametrize("send_default_pii", [True, False])
@pytest.mark.parametrize("user_id", [None, "42", 3])
def test_flask_login_configured(
Expand All @@ -227,14 +228,26 @@ def test_flask_login_configured(
app,
user_id,
capture_events,
capture_items,
monkeypatch,
integration_enabled_params,
span_streaming,
):
sentry_init(send_default_pii=send_default_pii, **integration_enabled_params)
if span_streaming:
sentry_init(
integrations=[flask_sentry.FlaskIntegration()],
send_default_pii=send_default_pii,
traces_sample_rate=1.0,
_experiments={"trace_lifecycle": "stream"},
)
else:
sentry_init(send_default_pii=send_default_pii, **integration_enabled_params)

class User:
is_authenticated = is_active = True
is_anonymous = user_id is not None
email = "user@example.com"
username = "testuser"

def get_id(self):
return str(user_id)
Expand All @@ -250,19 +263,34 @@ def login():
login_user(User())
return "ok"

events = capture_events()
if span_streaming:
items = capture_items("event", "span")
else:
events = capture_events()

client = app.test_client()
assert client.get("/login").status_code == 200
assert not events

assert client.get("/message").status_code == 200

(event,) = events
if user_id is None or not send_default_pii:
assert event.get("user", {}).get("id") is None
if span_streaming:
sentry_sdk.flush()
spans = [i.payload for i in items if i.type == "span"]
segment = next(s for s in spans if s["name"] == "hi")

if send_default_pii and user_id is not None:
assert segment["attributes"]["user.id"] == str(user_id)
assert segment["attributes"]["user.email"] == "user@example.com"
assert segment["attributes"]["user.name"] == "testuser"
else:
assert "user.id" not in segment.get("attributes", {})
else:
assert event["user"]["id"] == str(user_id)
(event,) = events
if user_id is None or not send_default_pii:
assert event.get("user", {}).get("id") is None
else:
assert event["user"]["id"] == str(user_id)
assert event["user"]["email"] == "user@example.com"
assert event["user"]["username"] == "testuser"


@pytest.mark.parametrize("max_value_length", [1024, None])
Expand Down
Loading