@@ -1131,3 +1131,155 @@ async def test_span_streaming_sensitive_header_passthrough_with_pii_and_no_data_
11311131 segment ["attributes" ]["http.request.header.authorization" ]
11321132 == "Bearer secret-token"
11331133 )
1134+
1135+
1136+ NO_QUERY_STRING = object ()
1137+ _QUERY_PARAM_DATA_COLLECTION_CASES = [
1138+ pytest .param (
1139+ {"send_default_pii" : True },
1140+ "toy=tennisball&color=red&auth=secret" ,
1141+ id = "send_default_pii_true" ,
1142+ ),
1143+ pytest .param (
1144+ {"send_default_pii" : False },
1145+ NO_QUERY_STRING ,
1146+ id = "send_default_pii_false" ,
1147+ ),
1148+ pytest .param (
1149+ {},
1150+ NO_QUERY_STRING ,
1151+ id = "defaults" ,
1152+ ),
1153+ pytest .param (
1154+ {"_experiments" : {"data_collection" : {}}},
1155+ "toy=tennisball&color=red&auth=[Filtered]" ,
1156+ id = "data_collection_denylist_default" ,
1157+ ),
1158+ pytest .param (
1159+ {
1160+ "_experiments" : {
1161+ "data_collection" : {
1162+ "url_query_params" : {"mode" : "denylist" , "terms" : ["toy" ]}
1163+ }
1164+ }
1165+ },
1166+ "toy=[Filtered]&color=red&auth=[Filtered]" ,
1167+ id = "data_collection_denylist_custom_terms" ,
1168+ ),
1169+ pytest .param (
1170+ {
1171+ "_experiments" : {
1172+ "data_collection" : {
1173+ "url_query_params" : {"mode" : "allowlist" , "terms" : ["toy" ]}
1174+ }
1175+ }
1176+ },
1177+ "toy=tennisball&color=[Filtered]&auth=[Filtered]" ,
1178+ id = "data_collection_allowlist" ,
1179+ ),
1180+ pytest .param (
1181+ {
1182+ "_experiments" : {
1183+ "data_collection" : {
1184+ "url_query_params" : {"mode" : "allowlist" , "terms" : ["auth" ]}
1185+ }
1186+ }
1187+ },
1188+ "toy=[Filtered]&color=[Filtered]&auth=[Filtered]" ,
1189+ id = "data_collection_allowlist_sensitive_term" ,
1190+ ),
1191+ pytest .param (
1192+ {"_experiments" : {"data_collection" : {"url_query_params" : {"mode" : "off" }}}},
1193+ NO_QUERY_STRING ,
1194+ id = "data_collection_off" ,
1195+ ),
1196+ pytest .param (
1197+ {
1198+ "send_default_pii" : True ,
1199+ "_experiments" : {"data_collection" : {"url_query_params" : {"mode" : "off" }}},
1200+ },
1201+ NO_QUERY_STRING ,
1202+ id = "data_collection_wins_over_send_default_pii" ,
1203+ ),
1204+ ]
1205+
1206+
1207+ @pytest .mark .asyncio
1208+ @pytest .mark .parametrize (
1209+ "init_kwargs, expected_query" , _QUERY_PARAM_DATA_COLLECTION_CASES
1210+ )
1211+ async def test_span_streaming_url_query_data_collection (
1212+ sentry_init , capture_items , init_kwargs , expected_query
1213+ ):
1214+ init_kwargs = dict (init_kwargs )
1215+ experiments = {"trace_lifecycle" : "stream" }
1216+ experiments .update (init_kwargs .pop ("_experiments" , {}))
1217+ sentry_init (
1218+ integrations = [quart_sentry .QuartIntegration ()],
1219+ traces_sample_rate = 1.0 ,
1220+ _experiments = experiments ,
1221+ ** init_kwargs ,
1222+ )
1223+ items = capture_items ("span" )
1224+
1225+ app = quart_app_factory ()
1226+ client = app .test_client ()
1227+ response = await client .get ("/message?toy=tennisball&color=red&auth=secret" )
1228+ assert response .status_code == 200
1229+
1230+ sentry_sdk .flush ()
1231+
1232+ spans = [item .payload for item in items ]
1233+ assert len (spans ) == 1
1234+
1235+ segment = spans [0 ]
1236+
1237+ data_collection_enabled = "data_collection" in experiments
1238+ url_attrs_expected = data_collection_enabled or init_kwargs .get (
1239+ "send_default_pii" , False
1240+ )
1241+
1242+ if expected_query is NO_QUERY_STRING :
1243+ assert "url.query" not in segment ["attributes" ]
1244+ if url_attrs_expected :
1245+ # When the filtered query string is empty, url.full carries the
1246+ # base URL only (no query).
1247+ assert segment ["attributes" ]["url.full" ] == "http://localhost/message"
1248+ else :
1249+ assert "url.full" not in segment ["attributes" ]
1250+ else :
1251+ assert segment ["attributes" ]["url.query" ] == expected_query
1252+ assert segment ["attributes" ]["url.full" ] == (
1253+ f"http://localhost/message?{ expected_query } "
1254+ )
1255+
1256+
1257+ @pytest .mark .asyncio
1258+ async def test_span_streaming_url_query_multi_and_blank_values (
1259+ sentry_init , capture_items
1260+ ):
1261+ sentry_init (
1262+ integrations = [quart_sentry .QuartIntegration ()],
1263+ traces_sample_rate = 1.0 ,
1264+ _experiments = {"trace_lifecycle" : "stream" , "data_collection" : {}},
1265+ )
1266+ items = capture_items ("span" )
1267+
1268+ app = quart_app_factory ()
1269+ client = app .test_client ()
1270+ response = await client .get ("/message?foo=1&foo=2&empty=" )
1271+ assert response .status_code == 200
1272+
1273+ sentry_sdk .flush ()
1274+
1275+ spans = [item .payload for item in items ]
1276+ assert len (spans ) == 1
1277+
1278+ segment = spans [0 ]
1279+ query = segment ["attributes" ]["url.query" ]
1280+ # Repeated keys are preserved and blank values are kept.
1281+ assert "foo=1" in query
1282+ assert "foo=2" in query
1283+ assert "empty=" in query
1284+ # url.full carries the same filtered query string.
1285+ assert segment ["attributes" ]["url.full" ] == f"http://localhost/message?{ query } "
0 commit comments