diff --git a/sentry_sdk/integrations/flask.py b/sentry_sdk/integrations/flask.py index c85bf5be6e..8e8b970838 100644 --- a/sentry_sdk/integrations/flask.py +++ b/sentry_sdk/integrations/flask.py @@ -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) @@ -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) diff --git a/tests/integrations/flask/test_flask.py b/tests/integrations/flask/test_flask.py index 5d0cf96b08..87e0b959cc 100644 --- a/tests/integrations/flask/test_flask.py +++ b/tests/integrations/flask/test_flask.py @@ -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( @@ -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) @@ -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])