@@ -406,13 +406,18 @@ def index():
406406 assert len (event ["request" ]["data" ]["foo" ]) == DEFAULT_MAX_VALUE_LENGTH
407407
408408
409- def test_flask_formdata_request_appear_transaction_body (
410- sentry_init , capture_events , app
409+ @pytest .mark .parametrize ("span_streaming" , [True , False ])
410+ def test_flask_formdata_request_appear_in_segment_or_transaction_body (
411+ sentry_init , capture_events , capture_items , app , span_streaming
411412):
412413 """
413- Test that ensures that transaction request data contains body, even if no exception was raised
414+ Test that ensures that transaction/segment request data contains body, even if no exception was raised
414415 """
415- sentry_init (integrations = [flask_sentry .FlaskIntegration ()], traces_sample_rate = 1.0 )
416+ sentry_init (
417+ integrations = [flask_sentry .FlaskIntegration ()],
418+ traces_sample_rate = 1.0 ,
419+ _experiments = {"trace_lifecycle" : "stream" } if span_streaming else {},
420+ )
416421
417422 data = {"username" : "sentry-user" , "age" : "26" }
418423
@@ -430,17 +435,29 @@ def index():
430435 capture_message ("hi" )
431436 return "ok"
432437
433- events = capture_events ()
438+ if span_streaming :
439+ items = capture_items ("event" , "span" )
440+ else :
441+ events = capture_events ()
434442
435443 client = app .test_client ()
436444 response = client .post ("/" , data = data )
437445 assert response .status_code == 200
438446
439- event , transaction_event = events
447+ if span_streaming :
448+ sentry_sdk .flush ()
449+
450+ spans = [i for i in items if i .type == "span" ]
451+ assert len (spans ) == 1
452+ span = spans [0 ].payload
453+
454+ assert span ["attributes" ]["http.request.body.data" ] == json .dumps (data )
455+ else :
456+ event , transaction_event = events
440457
441- assert "request" in transaction_event
442- assert "data" in transaction_event ["request" ]
443- assert transaction_event ["request" ]["data" ] == data
458+ assert "request" in transaction_event
459+ assert "data" in transaction_event ["request" ]
460+ assert transaction_event ["request" ]["data" ] == data
444461
445462
446463@pytest .mark .parametrize ("input_char" , ["a" , b"a" ])
@@ -1357,6 +1374,86 @@ def test_sensitive_header_scrubbing_span_streaming(sentry_init, capture_items, a
13571374 assert span ["attributes" ]["http.request.header.x-custom-header" ] == "passthrough"
13581375
13591376
1377+ def test_request_body_captured_when_route_ignores_body_span_streaming (
1378+ sentry_init , capture_items , app
1379+ ):
1380+ """
1381+ When the route handler never reads the request body, the SDK should
1382+ still capture it in _request_finished via request.get_data().
1383+ """
1384+ sentry_init (
1385+ integrations = [flask_sentry .FlaskIntegration ()],
1386+ traces_sample_rate = 1.0 ,
1387+ max_request_body_size = "always" ,
1388+ _experiments = {"trace_lifecycle" : "stream" },
1389+ )
1390+
1391+ body = {"key" : "value" }
1392+
1393+ @app .route ("/ignore-body" , methods = ["POST" ])
1394+ def ignore_body_endpoint ():
1395+ return "ok"
1396+
1397+ items = capture_items ("span" )
1398+
1399+ client = app .test_client ()
1400+ response = client .post ("/ignore-body" , json = body )
1401+ assert response .status_code == 200
1402+
1403+ sentry_sdk .flush ()
1404+
1405+ assert len (items ) == 1
1406+ span = items [0 ].payload
1407+ assert span ["attributes" ]["http.request.body.data" ] == json .dumps (body )
1408+
1409+
1410+ def test_client_disconnected_handled_gracefully_span_streaming (
1411+ sentry_init , capture_items , app
1412+ ):
1413+ from unittest .mock import patch
1414+
1415+ from werkzeug .exceptions import ClientDisconnected
1416+
1417+ sentry_init (
1418+ integrations = [flask_sentry .FlaskIntegration ()],
1419+ traces_sample_rate = 1.0 ,
1420+ max_request_body_size = "always" ,
1421+ _experiments = {"trace_lifecycle" : "stream" },
1422+ )
1423+
1424+ @app .route ("/disconnect" , methods = ["POST" ])
1425+ def disconnect_endpoint ():
1426+ # Simulate a client that disconnected: patch get_data so that
1427+ # when _request_finished tries to read the body it raises.
1428+ request ._cached_data = None
1429+ request .__dict__ .pop ("form" , None )
1430+ patch .object (
1431+ type (request ._get_current_object ()),
1432+ "get_data" ,
1433+ side_effect = ClientDisconnected (),
1434+ ).start ()
1435+ return "ok"
1436+
1437+ items = capture_items ("span" )
1438+
1439+ client = app .test_client ()
1440+ try :
1441+ response = client .post (
1442+ "/disconnect" ,
1443+ data = b'{"key": "value"}' ,
1444+ content_type = "application/json" ,
1445+ )
1446+ assert response .status_code == 200
1447+ finally :
1448+ patch .stopall ()
1449+
1450+ sentry_sdk .flush ()
1451+
1452+ assert len (items ) == 1
1453+ span = items [0 ].payload
1454+ assert "http.request.body.data" not in span .get ("attributes" , {})
1455+
1456+
13601457def test_wsgi_input_direct_read_does_not_hang_span_streaming (
13611458 sentry_init , capture_items , app
13621459):
0 commit comments