Skip to content
Draft
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
75 changes: 45 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})

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

Expand Down Expand Up @@ -223,43 +231,50 @@ 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:
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:
properties["username"] = user.username
except Exception:
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