@@ -238,6 +238,256 @@ def test_cookie_data_collection(
238238 assert event ["request" ]["cookies" ] == expected_cookies
239239
240240
241+ class QueryHandler (RequestHandler ):
242+ async def get (self ):
243+ self .write ("ok" )
244+
245+
246+ _QUERY_PARAM_DATA_COLLECTION_CASES = [
247+ pytest .param (
248+ {"send_default_pii" : True },
249+ "toy=tennisball&color=red&auth=secret" ,
250+ id = "send_default_pii_true" ,
251+ ),
252+ pytest .param (
253+ {"send_default_pii" : False },
254+ None ,
255+ id = "send_default_pii_false" ,
256+ ),
257+ pytest .param (
258+ {},
259+ None ,
260+ id = "defaults" ,
261+ ),
262+ pytest .param (
263+ {"_experiments" : {"data_collection" : {}}},
264+ "toy=tennisball&color=red&auth=%5BFiltered%5D" ,
265+ id = "data_collection_denylist_default" ,
266+ ),
267+ pytest .param (
268+ {
269+ "_experiments" : {
270+ "data_collection" : {
271+ "url_query_params" : {"mode" : "denylist" , "terms" : ["toy" ]}
272+ }
273+ }
274+ },
275+ "toy=%5BFiltered%5D&color=red&auth=%5BFiltered%5D" ,
276+ id = "data_collection_denylist_custom_terms" ,
277+ ),
278+ pytest .param (
279+ {
280+ "_experiments" : {
281+ "data_collection" : {
282+ "url_query_params" : {"mode" : "allowlist" , "terms" : ["toy" ]}
283+ }
284+ }
285+ },
286+ "toy=tennisball&color=%5BFiltered%5D&auth=%5BFiltered%5D" ,
287+ id = "data_collection_allowlist" ,
288+ ),
289+ pytest .param (
290+ {
291+ "_experiments" : {
292+ "data_collection" : {
293+ "url_query_params" : {"mode" : "allowlist" , "terms" : ["auth" ]}
294+ }
295+ }
296+ },
297+ "toy=%5BFiltered%5D&color=%5BFiltered%5D&auth=%5BFiltered%5D" ,
298+ id = "data_collection_allowlist_sensitive_term" ,
299+ ),
300+ pytest .param (
301+ {"_experiments" : {"data_collection" : {"url_query_params" : {"mode" : "off" }}}},
302+ None ,
303+ id = "data_collection_off" ,
304+ ),
305+ pytest .param (
306+ {
307+ "send_default_pii" : True ,
308+ "_experiments" : {"data_collection" : {"url_query_params" : {"mode" : "off" }}},
309+ },
310+ None ,
311+ id = "data_collection_wins_over_send_default_pii" ,
312+ ),
313+ ]
314+
315+
316+ @pytest .mark .parametrize (
317+ "init_kwargs, expected_query" , _QUERY_PARAM_DATA_COLLECTION_CASES
318+ )
319+ def test_url_query_data_collection_span_streaming (
320+ tornado_testcase , sentry_init , capture_items , init_kwargs , expected_query
321+ ):
322+ init_kwargs = dict (init_kwargs )
323+ sentry_init (
324+ integrations = [TornadoIntegration ()],
325+ traces_sample_rate = 1.0 ,
326+ trace_lifecycle = "stream" ,
327+ ** init_kwargs ,
328+ )
329+
330+ items = capture_items ("span" )
331+
332+ client = tornado_testcase (Application ([(r"/hi" , QueryHandler )]))
333+ response = client .fetch ("/hi?toy=tennisball&color=red&auth=secret" )
334+ assert response .code == 200
335+
336+ sentry_sdk .flush ()
337+
338+ (server_span ,) = [item .payload for item in items ]
339+
340+ data_collection_enabled = "data_collection" in init_kwargs .get ("_experiments" , {})
341+ url_attrs_expected = data_collection_enabled or init_kwargs .get (
342+ "send_default_pii" , False
343+ )
344+
345+ if expected_query is None :
346+ assert "url.query" not in server_span ["attributes" ]
347+ if url_attrs_expected :
348+ assert server_span ["attributes" ]["url.full" ].endswith ("/hi" )
349+ assert server_span ["attributes" ]["url.path" ] == "/hi"
350+ else :
351+ assert "url.full" not in server_span ["attributes" ]
352+ assert "url.path" not in server_span ["attributes" ]
353+ else :
354+ assert server_span ["attributes" ]["url.query" ] == expected_query
355+ assert server_span ["attributes" ]["url.full" ].endswith (f"/hi?{ expected_query } " )
356+ assert server_span ["attributes" ]["url.full" ].startswith ("http://" )
357+ assert server_span ["attributes" ]["url.path" ] == "/hi"
358+
359+
360+ @pytest .mark .parametrize (
361+ "init_kwargs, expected_query" , _QUERY_PARAM_DATA_COLLECTION_CASES
362+ )
363+ def test_url_query_data_collection_event_processor (
364+ tornado_testcase , sentry_init , capture_events , init_kwargs , expected_query
365+ ):
366+ sentry_init (
367+ integrations = [TornadoIntegration ()],
368+ traces_sample_rate = 1.0 ,
369+ trace_lifecycle = "static" ,
370+ ** init_kwargs ,
371+ )
372+
373+ events = capture_events ()
374+
375+ client = tornado_testcase (Application ([(r"/hi" , QueryHandler )]))
376+ response = client .fetch ("/hi?toy=tennisball&color=red&auth=secret" )
377+ assert response .code == 200
378+
379+ sentry_sdk .flush ()
380+
381+ (event ,) = events
382+
383+ assert event ["request" ]["url" ].endswith ("/hi" )
384+ assert event ["request" ]["method" ] == "GET"
385+ if "data_collection" not in init_kwargs .get ("_experiments" , {}):
386+ assert (
387+ event ["request" ]["query_string" ] == "toy=tennisball&color=red&auth=secret"
388+ )
389+ elif expected_query is None :
390+ assert "query_string" not in event ["request" ]
391+ else :
392+ assert event ["request" ]["query_string" ] == expected_query
393+
394+
395+ def test_url_query_data_collection_no_query_string (
396+ tornado_testcase , sentry_init , capture_items
397+ ):
398+ sentry_init (
399+ integrations = [TornadoIntegration ()],
400+ traces_sample_rate = 1.0 ,
401+ trace_lifecycle = "stream" ,
402+ _experiments = {"data_collection" : {}},
403+ )
404+
405+ items = capture_items ("span" )
406+
407+ client = tornado_testcase (Application ([(r"/hi" , QueryHandler )]))
408+ response = client .fetch ("/hi" )
409+ assert response .code == 200
410+
411+ sentry_sdk .flush ()
412+
413+ (server_span ,) = [item .payload for item in items ]
414+
415+ assert "url.query" not in server_span ["attributes" ]
416+ assert server_span ["attributes" ]["url.full" ].endswith ("/hi" )
417+ assert server_span ["attributes" ]["url.path" ] == "/hi"
418+
419+
420+ def test_url_query_data_collection_repeated_and_blank_params (
421+ tornado_testcase , sentry_init , capture_items
422+ ):
423+ sentry_init (
424+ integrations = [TornadoIntegration ()],
425+ traces_sample_rate = 1.0 ,
426+ trace_lifecycle = "stream" ,
427+ _experiments = {"data_collection" : {}},
428+ )
429+
430+ items = capture_items ("span" )
431+
432+ client = tornado_testcase (Application ([(r"/hi" , QueryHandler )]))
433+ response = client .fetch ("/hi?a=1&a=2&b=" )
434+ assert response .code == 200
435+
436+ sentry_sdk .flush ()
437+
438+ (server_span ,) = [item .payload for item in items ]
439+
440+ assert server_span ["attributes" ]["url.query" ] == "a=1&a=2&b="
441+
442+
443+ def test_url_query_data_collection__event_processor_no_query_string (
444+ tornado_testcase , sentry_init , capture_events
445+ ):
446+ sentry_init (
447+ integrations = [TornadoIntegration ()],
448+ traces_sample_rate = 1.0 ,
449+ trace_lifecycle = "static" ,
450+ _experiments = {"data_collection" : {}},
451+ )
452+
453+ events = capture_events ()
454+
455+ client = tornado_testcase (Application ([(r"/hi" , QueryHandler )]))
456+ response = client .fetch ("/hi" )
457+ assert response .code == 200
458+
459+ sentry_sdk .flush ()
460+
461+ (event ,) = events
462+
463+ assert "query_string" not in event ["request" ]
464+ assert event ["request" ]["url" ].endswith ("/hi" )
465+ assert event ["request" ]["method" ] == "GET"
466+
467+
468+ def test_url_query_data_collection_event_processor_repeated_and_blank_params (
469+ tornado_testcase , sentry_init , capture_events
470+ ):
471+ sentry_init (
472+ integrations = [TornadoIntegration ()],
473+ traces_sample_rate = 1.0 ,
474+ trace_lifecycle = "static" ,
475+ _experiments = {"data_collection" : {}},
476+ )
477+
478+ events = capture_events ()
479+
480+ client = tornado_testcase (Application ([(r"/hi" , QueryHandler )]))
481+ response = client .fetch ("/hi?a=1&a=2&b=" )
482+ assert response .code == 200
483+
484+ sentry_sdk .flush ()
485+
486+ (event ,) = events
487+
488+ assert event ["request" ]["query_string" ] == "a=1&a=2&b="
489+
490+
241491@pytest .mark .parametrize ("send_pii" , [True , False ])
242492@pytest .mark .parametrize ("span_streaming" , [True , False ])
243493@pytest .mark .parametrize (
0 commit comments