From 96638ee5b6866660ffdc8dbf2882742ef29153ba Mon Sep 17 00:00:00 2001 From: Pablo Deputter Date: Fri, 2 Oct 2026 12:59:37 +0200 Subject: [PATCH 1/2] ref(flask): Drop `send_default_pii` support --- sentry_sdk/integrations/flask.py | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/sentry_sdk/integrations/flask.py b/sentry_sdk/integrations/flask.py index e671afb5d5..be1f958548 100644 --- a/sentry_sdk/integrations/flask.py +++ b/sentry_sdk/integrations/flask.py @@ -8,13 +8,11 @@ RequestExtractor, ) from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware -from sentry_sdk.scope import should_send_default_pii from sentry_sdk.traces import SegmentNameSource from sentry_sdk.utils import ( capture_internal_exceptions, ensure_integration_enabled, event_from_exception, - has_data_collection_enabled, package_version, ) @@ -146,13 +144,7 @@ def _request_started(app: "Flask", **kwargs: "Any") -> None: scope = sentry_sdk.get_isolation_scope() - if has_data_collection_enabled(client.options): - if client.options["data_collection"]["user_info"]: - with capture_internal_exceptions(): - user_properties = _get_flask_user_properties() - if user_properties: - scope.set_user(user_properties) - elif should_send_default_pii(): + if client.options["data_collection"]["user_info"]: with capture_internal_exceptions(): user_properties = _get_flask_user_properties() if user_properties: @@ -205,11 +197,7 @@ def inner(event: "Event", hint: "dict[str, Any]") -> "Event": FlaskRequestExtractor(request).extract_into_event(event) client_options = sentry_sdk.get_client().options - if has_data_collection_enabled(client_options): - if client_options["data_collection"]["user_info"]: - with capture_internal_exceptions(): - _add_user_to_event(event) - elif should_send_default_pii(): + if client_options["data_collection"]["user_info"]: with capture_internal_exceptions(): _add_user_to_event(event) From e099a9351380196716310596644973d03708c0b9 Mon Sep 17 00:00:00 2001 From: Pablo Deputter Date: Fri, 2 Oct 2026 13:25:17 +0200 Subject: [PATCH 2/2] tests --- tests/integrations/flask/test_flask.py | 168 ++++++++++++------------- 1 file changed, 80 insertions(+), 88 deletions(-) diff --git a/tests/integrations/flask/test_flask.py b/tests/integrations/flask/test_flask.py index 667e4ad6eb..b9eced48a4 100644 --- a/tests/integrations/flask/test_flask.py +++ b/tests/integrations/flask/test_flask.py @@ -38,8 +38,6 @@ # built-in sensitive term, so it is redacted by the default denylist. QUERY_STRING = "toy=tennisball&color=red&auth=secret" -login_manager = LoginManager() - @pytest.fixture def app(): @@ -47,8 +45,15 @@ def app(): app.config["TESTING"] = True app.secret_key = "haha" + login_manager = LoginManager() login_manager.init_app(app) + # install `LoginManager` with user loader to avoid Flask-Login's + # "Missing user_loader or request_loader" error. + @login_manager.user_loader + def load_user(user_id): + return None + @app.route("/message") def hi(): capture_message("hi") @@ -77,7 +82,10 @@ def integration_enabled_params(request): def test_has_context(sentry_init, app, capture_events): - sentry_init(integrations=[flask_sentry.FlaskIntegration()]) + sentry_init( + data_collection={}, + integrations=[flask_sentry.FlaskIntegration()], + ) events = capture_events() client = app.test_client() @@ -107,6 +115,7 @@ def test_segment_name_and_source( expected_source, ): sentry_init( + data_collection={}, integrations=[flask_sentry.FlaskIntegration()], traces_sample_rate=1.0, ) @@ -132,6 +141,7 @@ def test_http_route( capture_items, ): sentry_init( + data_collection={}, integrations=[flask_sentry.FlaskIntegration()], traces_sample_rate=1.0, ) @@ -157,7 +167,7 @@ def test_errors( testing, integration_enabled_params, ): - sentry_init(**integration_enabled_params) + sentry_init(data_collection={}, **integration_enabled_params) app.debug = debug app.testing = testing @@ -185,7 +195,7 @@ def index(): def test_flask_login_not_installed( sentry_init, app, capture_events, monkeypatch, integration_enabled_params ): - sentry_init(**integration_enabled_params) + sentry_init(data_collection={}, **integration_enabled_params) monkeypatch.setattr(flask_sentry, "flask_login", None) @@ -201,10 +211,13 @@ def test_flask_login_not_installed( def test_flask_login_not_configured( sentry_init, app, capture_events, monkeypatch, integration_enabled_params ): - sentry_init(**integration_enabled_params) + sentry_init(data_collection={"user_info": False}, **integration_enabled_params) assert flask_sentry.flask_login + login_manager = LoginManager() + login_manager.init_app(app) + events = capture_events() client = app.test_client() client.get("/message") @@ -216,7 +229,7 @@ def test_flask_login_not_configured( def test_flask_login_partially_configured( sentry_init, app, capture_events, monkeypatch, integration_enabled_params ): - sentry_init(**integration_enabled_params) + sentry_init(data_collection={"user_info": False}, **integration_enabled_params) events = capture_events() @@ -230,10 +243,17 @@ def test_flask_login_partially_configured( assert event.get("user", {}).get("id") is None -@pytest.mark.parametrize("send_default_pii", [True, False]) +@pytest.mark.parametrize( + "data_collection, expect_user", + [ + pytest.param({}, True, id="data_collection_default"), + pytest.param({"user_info": False}, False, id="data_collection_user_info_false"), + ], +) @pytest.mark.parametrize("user_id", [None, "42", 3]) def test_flask_login_configured( - send_default_pii, + data_collection, + expect_user, sentry_init, app, user_id, @@ -244,7 +264,7 @@ def test_flask_login_configured( ): sentry_init( integrations=[flask_sentry.FlaskIntegration()], - send_default_pii=send_default_pii, + data_collection=data_collection, traces_sample_rate=1.0, ) @@ -257,7 +277,7 @@ class User: def get_id(self): return str(user_id) - @login_manager.user_loader + @app.login_manager.user_loader def load_user(user_id): if user_id is not None: return User() @@ -279,7 +299,7 @@ def login(): spans = [i.payload for i in items if i.type == "span"] segment = next(s for s in spans if s["name"] == "/message") - if send_default_pii and user_id is not None: + if expect_user 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" @@ -290,6 +310,7 @@ def login(): @pytest.mark.parametrize("max_value_length", [1024, None]) def test_flask_large_json_request(sentry_init, capture_events, app, max_value_length): sentry_init( + data_collection={}, integrations=[flask_sentry.FlaskIntegration()], max_request_body_size="always", max_value_length=max_value_length, @@ -326,6 +347,7 @@ def index(): def test_flask_session_tracking(sentry_init, capture_envelopes, app): sentry_init( + data_collection={}, integrations=[ flask_sentry.FlaskIntegration(), LoggingIntegration(level=None, event_level=logging.ERROR), @@ -369,7 +391,7 @@ def index(): @pytest.mark.parametrize("data", [{}, []], ids=["empty-dict", "empty-list"]) def test_flask_empty_json_request(sentry_init, capture_events, app, data): - sentry_init(integrations=[flask_sentry.FlaskIntegration()]) + sentry_init(data_collection={}, integrations=[flask_sentry.FlaskIntegration()]) @app.route("/", methods=["POST"]) def index(): @@ -394,6 +416,7 @@ def test_flask_medium_formdata_request( sentry_init, capture_events, app, max_value_length ): sentry_init( + data_collection={}, integrations=[flask_sentry.FlaskIntegration()], max_request_body_size="always", max_value_length=max_value_length, @@ -435,7 +458,9 @@ def index(): @pytest.mark.parametrize("input_char", ["a", b"a"]) def test_flask_too_large_raw_request(sentry_init, input_char, capture_events, app): sentry_init( - integrations=[flask_sentry.FlaskIntegration()], max_request_body_size="small" + data_collection={}, + integrations=[flask_sentry.FlaskIntegration()], + max_request_body_size="small", ) data = input_char * 2000 @@ -469,6 +494,7 @@ def index(): @pytest.mark.parametrize("max_value_length", [1024, None]) def test_flask_files_and_form(sentry_init, capture_events, app, max_value_length): sentry_init( + data_collection={}, integrations=[flask_sentry.FlaskIntegration()], max_request_body_size="always", max_value_length=max_value_length, @@ -517,7 +543,9 @@ def test_json_not_truncated_if_max_request_body_size_is_always( sentry_init, capture_events, app ): sentry_init( - integrations=[flask_sentry.FlaskIntegration()], max_request_body_size="always" + data_collection={}, + integrations=[flask_sentry.FlaskIntegration()], + max_request_body_size="always", ) data = { @@ -549,7 +577,7 @@ def index(): ], ) def test_errors_not_reported_twice(sentry_init, integrations, capture_events, app): - sentry_init(integrations=integrations) + sentry_init(data_collection={}, integrations=integrations) @app.route("/") def index(): @@ -571,10 +599,11 @@ def index(): def test_logging(sentry_init, capture_events, app): # ensure that Flask's logger magic doesn't break ours sentry_init( + data_collection={}, integrations=[ flask_sentry.FlaskIntegration(), LoggingIntegration(event_level="ERROR"), - ] + ], ) @app.route("/") @@ -592,7 +621,7 @@ def index(): def test_no_errors_without_request(app, sentry_init): - sentry_init(integrations=[flask_sentry.FlaskIntegration()]) + sentry_init(data_collection={}, integrations=[flask_sentry.FlaskIntegration()]) with app.app_context(): capture_exception(ValueError()) @@ -619,7 +648,7 @@ def create_app(*_): def test_wsgi_level_error_is_caught( app, capture_exceptions, capture_events, sentry_init ): - sentry_init(integrations=[flask_sentry.FlaskIntegration()]) + sentry_init(data_collection={}, integrations=[flask_sentry.FlaskIntegration()]) def wsgi_app(environ, start_response): 1 / 0 @@ -643,7 +672,7 @@ def wsgi_app(environ, start_response): def test_500(sentry_init, app): - sentry_init(integrations=[flask_sentry.FlaskIntegration()]) + sentry_init(data_collection={}, integrations=[flask_sentry.FlaskIntegration()]) app.debug = False app.testing = False @@ -663,7 +692,7 @@ def error_handler(err): def test_error_in_errorhandler(sentry_init, capture_events, app): - sentry_init(integrations=[flask_sentry.FlaskIntegration()]) + sentry_init(data_collection={}, integrations=[flask_sentry.FlaskIntegration()]) app.debug = False app.testing = False @@ -693,7 +722,7 @@ def error_handler(err): def test_bad_request_not_captured(sentry_init, capture_events, app): - sentry_init(integrations=[flask_sentry.FlaskIntegration()]) + sentry_init(data_collection={}, integrations=[flask_sentry.FlaskIntegration()]) events = capture_events() @app.route("/") @@ -708,7 +737,7 @@ def index(): def test_does_not_leak_scope(sentry_init, capture_events, app): - sentry_init(integrations=[flask_sentry.FlaskIntegration()]) + sentry_init(data_collection={}, integrations=[flask_sentry.FlaskIntegration()]) events = capture_events() sentry_sdk.get_isolation_scope().set_tag("request_data", False) @@ -734,7 +763,7 @@ def generate(): def test_scoped_test_client(sentry_init, app): - sentry_init(integrations=[flask_sentry.FlaskIntegration()]) + sentry_init(data_collection={}, integrations=[flask_sentry.FlaskIntegration()]) @app.route("/") def index(): @@ -752,7 +781,7 @@ def test_errorhandler_for_exception_swallows_exception( # In contrast to error handlers for a status code, error # handlers for exceptions can swallow the exception (this is # just how the Flask signal works) - sentry_init(integrations=[flask_sentry.FlaskIntegration()]) + sentry_init(data_collection={}, integrations=[flask_sentry.FlaskIntegration()]) events = capture_events() @app.route("/") @@ -777,6 +806,7 @@ def test_tracing_success( app, ): sentry_init( + data_collection={}, traces_sample_rate=1.0, integrations=[flask_sentry.FlaskIntegration()], ) @@ -819,6 +849,7 @@ def hi_tx(): def test_tracing_error(sentry_init, capture_events, capture_items, app): sentry_init( + data_collection={}, traces_sample_rate=1.0, integrations=[flask_sentry.FlaskIntegration()], ) @@ -854,7 +885,7 @@ def error(): def test_error_has_trace_context_if_tracing_disabled(sentry_init, capture_events, app): - sentry_init(integrations=[flask_sentry.FlaskIntegration()]) + sentry_init(data_collection={}, integrations=[flask_sentry.FlaskIntegration()]) events = capture_events() @@ -873,7 +904,7 @@ def error(): def test_class_based_views(sentry_init, app, capture_events): - sentry_init(integrations=[flask_sentry.FlaskIntegration()]) + sentry_init(data_collection={}, integrations=[flask_sentry.FlaskIntegration()]) events = capture_events() @app.route("/") @@ -898,7 +929,7 @@ def dispatch_request(self): "template_string", ["{{ sentry_trace }}", "{{ sentry_trace_meta }}"] ) def test_template_tracing_meta(sentry_init, app, capture_events, template_string): - sentry_init(integrations=[flask_sentry.FlaskIntegration()]) + sentry_init(data_collection={}, integrations=[flask_sentry.FlaskIntegration()]) events = capture_events() @app.route("/") @@ -927,7 +958,7 @@ def index(): def test_dont_override_sentry_trace_context(sentry_init, app): - sentry_init(integrations=[flask_sentry.FlaskIntegration()]) + sentry_init(data_collection={}, integrations=[flask_sentry.FlaskIntegration()]) @app.route("/") def index(): @@ -982,6 +1013,7 @@ def index(): def test_span_origin(sentry_init, app, capture_events, capture_items): sentry_init( + data_collection={}, integrations=[flask_sentry.FlaskIntegration()], traces_sample_rate=1.0, ) @@ -1009,6 +1041,7 @@ def test_segment_http_method_default( By default OPTIONS and HEAD requests do not create a transaction or segment. """ sentry_init( + data_collection={}, traces_sample_rate=1.0, integrations=[flask_sentry.FlaskIntegration()], ) @@ -1043,6 +1076,7 @@ def test_segment_http_method_custom( Configure FlaskIntegration to ONLY capture OPTIONS and HEAD requests. """ sentry_init( + data_collection={}, traces_sample_rate=1.0, integrations=[ flask_sentry.FlaskIntegration( @@ -1081,34 +1115,20 @@ def test_segment_http_method_custom( @pytest.mark.parametrize( - "init_kwargs, expected_query_string", + "data_collection, expected_query_string", [ pytest.param( - {"send_default_pii": True}, - "toy=tennisball&color=red&auth=secret", - id="legacy_send_default_pii_true", - ), - pytest.param( - {"send_default_pii": False}, - "toy=tennisball&color=red&auth=secret", - id="legacy_send_default_pii_false", - ), - pytest.param( - {"data_collection": {}}, + {}, "toy=tennisball&color=red&auth=%5BFiltered%5D", id="data_collection_denylist_default", ), pytest.param( - { - "data_collection": { - "url_query_params": {"mode": "allowlist", "terms": ["toy"]} - } - }, + {"url_query_params": {"mode": "allowlist", "terms": ["toy"]}}, "toy=tennisball&color=%5BFiltered%5D&auth=%5BFiltered%5D", id="data_collection_allowlist", ), pytest.param( - {"data_collection": {"url_query_params": {"mode": "off"}}}, + {"url_query_params": {"mode": "off"}}, None, id="data_collection_off", ), @@ -1119,13 +1139,13 @@ def test_query_string_data_collection( app, capture_events, monkeypatch, - init_kwargs, + data_collection, expected_query_string, ): - sentry_init(integrations=[flask_sentry.FlaskIntegration()], **init_kwargs) - # This test is about query-string filtering, not user data. Disable - # flask_login so the module-level login manager (which has no user_loader) - # does not raise when send_default_pii is on. + sentry_init( + integrations=[flask_sentry.FlaskIntegration()], data_collection=data_collection + ) + # Disable Flask-Login to isolate query-string filtering from user collection. monkeypatch.setattr(flask_sentry, "flask_login", None) events = capture_events() @@ -1141,34 +1161,20 @@ def test_query_string_data_collection( @pytest.mark.parametrize( - "init_kwargs, expected_query", + "data_collection, expected_query", [ pytest.param( - {"send_default_pii": True}, - "toy=tennisball&color=red&auth=secret", - id="legacy_send_default_pii_true", - ), - pytest.param( - {"send_default_pii": False}, - None, - id="legacy_send_default_pii_false", - ), - pytest.param( - {"data_collection": {}}, + {}, "toy=tennisball&color=red&auth=%5BFiltered%5D", id="data_collection_denylist_default", ), pytest.param( - { - "data_collection": { - "url_query_params": {"mode": "allowlist", "terms": ["toy"]} - } - }, + {"url_query_params": {"mode": "allowlist", "terms": ["toy"]}}, "toy=tennisball&color=%5BFiltered%5D&auth=%5BFiltered%5D", id="data_collection_allowlist", ), pytest.param( - {"data_collection": {"url_query_params": {"mode": "off"}}}, + {"url_query_params": {"mode": "off"}}, None, id="data_collection_off", ), @@ -1179,13 +1185,13 @@ def test_span_http_query_data_collection( app, capture_items, monkeypatch, - init_kwargs, + data_collection, expected_query, ): sentry_init( integrations=[flask_sentry.FlaskIntegration()], traces_sample_rate=1.0, - **init_kwargs, + data_collection=data_collection, ) monkeypatch.setattr(flask_sentry, "flask_login", None) @@ -1205,20 +1211,6 @@ def test_span_http_query_data_collection( assert segment["attributes"][SPANDATA.HTTP_QUERY] == expected_query -def test_query_string_empty_legacy_emits_empty_string( - sentry_init, app, capture_events, monkeypatch -): - sentry_init(integrations=[flask_sentry.FlaskIntegration()], send_default_pii=True) - monkeypatch.setattr(flask_sentry, "flask_login", None) - events = capture_events() - - client = app.test_client() - client.get("/message") - - (event,) = events - assert event["request"]["query_string"] == "" - - def test_empty_query_string_is_dropped_with_data_collection( sentry_init, app, capture_events ): @@ -1338,7 +1330,7 @@ class User: def get_id(self): return "42" - @login_manager.user_loader + @app.login_manager.user_loader def load_user(user_id): return User() @@ -1392,7 +1384,7 @@ class User: def get_id(self): return "42" - @login_manager.user_loader + @app.login_manager.user_loader def load_user(user_id): return User()