@@ -529,6 +529,255 @@ def test_include_source_context_when_serializing_frame(
529529 assert ("post_context" in result ) is expected_source_context
530530
531531
532+ def _frame_with_locals ():
533+ safe_value = "not sensitive" # noqa: F841
534+ password = "ada123" # noqa: F841
535+ api_key = "abc123" # noqa: F841
536+ nickname = "Beans" # noqa: F841
537+ return sys ._getframe ()
538+
539+
540+ @pytest .mark .parametrize (
541+ "data_collection,include_local_variables,expected_vars" ,
542+ [
543+ pytest .param (
544+ {"stack_frame_variables" : True },
545+ False ,
546+ True ,
547+ id = "data_collection_stack_frame_variables_true_overrides_include_false" ,
548+ ),
549+ pytest .param (
550+ {"stack_frame_variables" : False },
551+ True ,
552+ False ,
553+ id = "data_collection_stack_frame_variables_false_overrides_include_true" ,
554+ ),
555+ pytest .param (
556+ {},
557+ False ,
558+ True ,
559+ id = "data_collection_stack_frame_variables_spec_default_is_true" ,
560+ ),
561+ ],
562+ )
563+ def test_stack_frame_variables_bool_when_serializing_frame (
564+ sentry_init , data_collection , include_local_variables , expected_vars
565+ ):
566+ sentry_init (_experiments = {"data_collection" : data_collection })
567+
568+ result = serialize_frame (
569+ _frame_with_locals (), include_local_variables = include_local_variables
570+ )
571+
572+ assert ("vars" in result ) is expected_vars
573+
574+
575+ def test_stack_frame_variables_true_does_not_filter_sensitive_locals (sentry_init ):
576+ sentry_init (_experiments = {"data_collection" : {"stack_frame_variables" : True }})
577+
578+ result = serialize_frame (_frame_with_locals ())
579+
580+ assert result ["vars" ]["safe_value" ] == "'not sensitive'"
581+ assert result ["vars" ]["password" ] == "'ada123'"
582+
583+
584+ @pytest .mark .parametrize (
585+ "behaviour,expected_vars" ,
586+ [
587+ pytest .param (
588+ {"mode" : "denylist" },
589+ {
590+ "safe_value" : "'not sensitive'" ,
591+ "password" : "'[Filtered]'" ,
592+ "api_key" : "'[Filtered]'" ,
593+ "nickname" : "'Beans'" ,
594+ },
595+ id = "data_collection_stack_frame_variables_denylist_builtin_terms_only" ,
596+ ),
597+ pytest .param (
598+ {"mode" : "denylist" , "terms" : ["nickname" ]},
599+ {
600+ "safe_value" : "'not sensitive'" ,
601+ "password" : "'[Filtered]'" ,
602+ "api_key" : "'[Filtered]'" ,
603+ "nickname" : "'[Filtered]'" ,
604+ },
605+ id = "data_collection_stack_frame_variables_denylist_user_terms" ,
606+ ),
607+ pytest .param (
608+ {"mode" : "allowlist" , "terms" : ["safe" ]},
609+ {
610+ "safe_value" : "'not sensitive'" ,
611+ "password" : "'[Filtered]'" ,
612+ "api_key" : "'[Filtered]'" ,
613+ "nickname" : "'[Filtered]'" ,
614+ },
615+ id = "data_collection_stack_frame_variables_allowlist_user_terms" ,
616+ ),
617+ pytest .param (
618+ {"mode" : "allowlist" , "terms" : ["safe" , "api_key" ]},
619+ {
620+ "safe_value" : "'not sensitive'" ,
621+ "password" : "'[Filtered]'" ,
622+ "api_key" : "'[Filtered]'" ,
623+ "nickname" : "'[Filtered]'" ,
624+ },
625+ id = "data_collection_stack_frame_variables_allowlist_cannot_allow_sensitive_term" ,
626+ ),
627+ ],
628+ )
629+ def test_stack_frame_variables_filtering_when_serializing_frame (
630+ sentry_init , behaviour , expected_vars
631+ ):
632+ sentry_init (_experiments = {"data_collection" : {"stack_frame_variables" : behaviour }})
633+
634+ result = serialize_frame (_frame_with_locals ())
635+
636+ assert result ["vars" ] == expected_vars
637+
638+
639+ def test_stack_frame_variables_off_omits_vars (sentry_init ):
640+ sentry_init (
641+ _experiments = {"data_collection" : {"stack_frame_variables" : {"mode" : "off" }}}
642+ )
643+
644+ result = serialize_frame (_frame_with_locals ())
645+
646+ assert "vars" not in result
647+
648+
649+ def test_stack_frame_variables_omits_vars_when_frame_has_no_locals (sentry_init ):
650+ def _frame_without_locals ():
651+ return sys ._getframe ()
652+
653+ sentry_init (
654+ _experiments = {
655+ "data_collection" : {"stack_frame_variables" : {"mode" : "denylist" }}
656+ }
657+ )
658+
659+ result = serialize_frame (_frame_without_locals ())
660+
661+ assert "vars" not in result
662+
663+
664+ def test_stack_frame_variables_filtering_uses_custom_repr (sentry_init ):
665+ sentry_init (
666+ _experiments = {
667+ "data_collection" : {"stack_frame_variables" : {"mode" : "denylist" }}
668+ }
669+ )
670+
671+ def custom_repr (value ):
672+ return "CUSTOM" if value == "not sensitive" else None
673+
674+ result = serialize_frame (_frame_with_locals (), custom_repr = custom_repr )
675+
676+ assert result ["vars" ]["safe_value" ] == "CUSTOM"
677+ assert result ["vars" ]["password" ] == "'[Filtered]'"
678+
679+
680+ @pytest .mark .parametrize (
681+ "options,include_local_variables,expected_vars" ,
682+ [
683+ pytest .param (
684+ {},
685+ True ,
686+ True ,
687+ id = "no_data_collection-include_local_variables_true" ,
688+ ),
689+ pytest .param (
690+ {},
691+ False ,
692+ False ,
693+ id = "no_data_collection-include_local_variables_false" ,
694+ ),
695+ ],
696+ )
697+ def test_include_local_variables_when_data_collection_is_unset (
698+ sentry_init , options , include_local_variables , expected_vars
699+ ):
700+ sentry_init (** options )
701+
702+ result = serialize_frame (
703+ _frame_with_locals (), include_local_variables = include_local_variables
704+ )
705+
706+ assert ("vars" in result ) is expected_vars
707+
708+
709+ def test_data_collection_stack_frame_variables_overrides_include_local_variables_option (
710+ sentry_init , capture_events
711+ ):
712+ sentry_init (
713+ include_local_variables = False ,
714+ _experiments = {"data_collection" : {"stack_frame_variables" : True }},
715+ )
716+ events = capture_events ()
717+
718+ def raise_with_locals ():
719+ safe_value = "not sensitive" # noqa: F841
720+ raise ValueError ("boom" )
721+
722+ try :
723+ raise_with_locals ()
724+ except ValueError :
725+ sentry_sdk .capture_exception ()
726+
727+ (event ,) = events
728+ frame = event ["exception" ]["values" ][0 ]["stacktrace" ]["frames" ][- 1 ]
729+ assert frame ["vars" ]["safe_value" ] == "'not sensitive'"
730+
731+
732+ def test_data_collection_stack_frame_variables_filtering_applies_to_captured_exception (
733+ sentry_init , capture_events
734+ ):
735+ sentry_init (
736+ _experiments = {
737+ "data_collection" : {
738+ "stack_frame_variables" : {"mode" : "denylist" , "terms" : ["nickname" ]}
739+ }
740+ }
741+ )
742+ events = capture_events ()
743+
744+ def raise_with_locals ():
745+ safe_value = "not sensitive" # noqa: F841
746+ password = "hunter2" # noqa: F841
747+ nickname = "Bugsy" # noqa: F841
748+ raise ValueError ("boom" )
749+
750+ try :
751+ raise_with_locals ()
752+ except ValueError :
753+ sentry_sdk .capture_exception ()
754+
755+ (event ,) = events
756+ frame = event ["exception" ]["values" ][0 ]["stacktrace" ]["frames" ][- 1 ]
757+
758+ assert frame ["vars" ]["safe_value" ] == "'not sensitive'"
759+ assert frame ["vars" ]["password" ] == "'[Filtered]'"
760+ assert frame ["vars" ]["nickname" ] == "'[Filtered]'"
761+
762+
763+ def test_serialize_frame_variables_serializer_failure (sentry_init ):
764+ sentry_init (
765+ _experiments = {
766+ "data_collection" : {
767+ "stack_frame_variables" : {"mode" : "denylist" , "terms" : ["password" ]}
768+ }
769+ }
770+ )
771+
772+ failure_message = "<failed to serialize, use init(debug=True) to see error logs>"
773+
774+ frame = sys ._getframe ()
775+ with mock .patch ("sentry_sdk.serializer.serialize" , return_value = failure_message ):
776+ result = serialize_frame (frame )
777+
778+ assert result ["vars" ] == failure_message
779+
780+
532781@pytest .mark .parametrize (
533782 "item,regex_list,expected_result" ,
534783 [
0 commit comments