We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Make EventScrubber recursive
### Problem Statement

We have a custom `before_send` implementation that scrubs data recursively. I was hopping to replace the custom implementation with the built-in EventScrubber but I found out that it doesn't scrub `vars` recursively.

As far as I can tell this was a consistency, perf trade-off thing but it would be nice to have a built-in option to make it recursive.

Thank you!

### Solution Brainstorm

`EventScrubber(recursive=True)`

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `sentry_sdk/scrubber.py`
Content:
```
1 from sentry_sdk.utils import (
2     capture_internal_exceptions,
3     AnnotatedValue,
4     iter_event_frames,
5 )
6 from sentry_sdk._compat import string_types
7 from sentry_sdk._types import TYPE_CHECKING
8 
9 if TYPE_CHECKING:
10     from sentry_sdk._types import Event
11     from typing import Any
12     from typing import Dict
13     from typing import List
14     from typing import Optional
15 
16 
17 DEFAULT_DENYLIST = [
18     # stolen from relay
19     "password",
20     "passwd",
21     "secret",
22     "api_key",
23     "apikey",
24     "auth",
25     "credentials",
26     "mysql_pwd",
27     "privatekey",
28     "private_key",
29     "token",
30     "ip_address",
31     "session",
32     # django
33     "csrftoken",
34     "sessionid",
35     # wsgi
36     "remote_addr",
37     "x_csrftoken",
38     "x_forwarded_for",
39     "set_cookie",
40     "cookie",
41     "authorization",
42     "x_api_key",
43     "x_forwarded_for",
44     "x_real_ip",
45     # other common names used in the wild
46     "aiohttp_session",  # aiohttp
47     "connect.sid",  # Express
48     "csrf_token",  # Pyramid
49     "csrf",  # (this is a cookie name used in accepted answers on stack overflow)
50     "_csrf",  # Express
51     "_csrf_token",  # Bottle
52     "PHPSESSID",  # PHP
53     "_session",  # Sanic
54     "symfony",  # Symfony
55     "user_session",  # Vue
56     "_xsrf",  # Tornado
57     "XSRF-TOKEN",  # Angular, Laravel
58 ]
59 
60 
61 class EventScrubber(object):
62     def __init__(self, denylist=None):
63         # type: (Optional[List[str]]) -> None
64         self.denylist = DEFAULT_DENYLIST if denylist is None else denylist
65         self.denylist = [x.lower() for x in self.denylist]
66 
67     def scrub_dict(self, d):
68         # type: (Dict[str, Any]) -> None
69         if not isinstance(d, dict):
70             return
71 
72         for k in d.keys():
73             if isinstance(k, string_types) and k.lower() in self.denylist:
74                 d[k] = AnnotatedValue.substituted_because_contains_sensitive_data()
75 
76     def scrub_request(self, event):
77         # type: (Event) -> None
78         with capture_internal_exceptions():
79             if "request" in event:
80                 if "headers" in event["request"]:
81                     self.scrub_dict(event["request"]["headers"])
82                 if "cookies" in event["request"]:
83                     self.scrub_dict(event["request"]["cookies"])
84                 if "data" in event["request"]:
85                     self.scrub_dict(event["request"]["data"])
86 
87     def scrub_extra(self, event):
88         # type: (Event) -> None
89         with capture_internal_exceptions():
90             if "extra" in event:
91                 self.scrub_dict(event["extra"])
92 
93     def scrub_user(self, event):
94         # type: (Event) -> None
95         with capture_internal_exceptions():
96             if "user" in event:
97                 self.scrub_dict(event["user"])
98 
99     def scrub_breadcrumbs(self, event):
100         # type: (Event) -> None
101         with capture_internal_exceptions():
102             if "breadcrumbs" in event:
103                 if "values" in event["breadcrumbs"]:
104                     for value in event["breadcrumbs"]["values"]:
105                         if "data" in value:
106                             self.scrub_dict(value["data"])
107 
108     def scrub_frames(self, event):
109         # type: (Event) -> None
110         with capture_internal_exceptions():
111             for frame in iter_event_frames(event):
112                 if "vars" in frame:
113                     self.scrub_dict(frame["vars"])
114 
115     def scrub_spans(self, event):
116         # type: (Event) -> None
117         with capture_internal_exceptions():
118             if "spans" in event:
119                 for span in event["spans"]:
120                     if "data" in span:
121                         self.scrub_dict(span["data"])
122 
123     def scrub_event(self, event):
124         # type: (Event) -> None
125         self.scrub_request(event)
126         self.scrub_extra(event)
127         self.scrub_user(event)
128         self.scrub_breadcrumbs(event)
129         self.scrub_frames(event)
130         self.scrub_spans(event)
131
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/sentry_sdk/scrubber.py b/sentry_sdk/scrubber.py
--- a/sentry_sdk/scrubber.py
+++ b/sentry_sdk/scrubber.py
@@ -59,19 +59,36 @@
 
 
 class EventScrubber(object):
-    def __init__(self, denylist=None):
-        # type: (Optional[List[str]]) -> None
+    def __init__(self, denylist=None, recursive=False):
+        # type: (Optional[List[str]], bool) -> None
         self.denylist = DEFAULT_DENYLIST if denylist is None else denylist
         self.denylist = [x.lower() for x in self.denylist]
+        self.recursive = recursive
+
+    def scrub_list(self, lst):
+        # type: (List[Any]) -> None
+        if not isinstance(lst, list):
+            return
+
+        for v in lst:
+            if isinstance(v, dict):
+                self.scrub_dict(v)
+            elif isinstance(v, list):
+                self.scrub_list(v)
 
     def scrub_dict(self, d):
         # type: (Dict[str, Any]) -> None
         if not isinstance(d, dict):
             return
 
-        for k in d.keys():
+        for k, v in d.items():
             if isinstance(k, string_types) and k.lower() in self.denylist:
                 d[k] = AnnotatedValue.substituted_because_contains_sensitive_data()
+            elif self.recursive:
+                if isinstance(v, dict):
+                    self.scrub_dict(v)
+                elif isinstance(v, list):
+                    self.scrub_list(v)
 
     def scrub_request(self, event):
         # type: (Event) -> None

{"golden_diff": "diff --git a/sentry_sdk/scrubber.py b/sentry_sdk/scrubber.py\n--- a/sentry_sdk/scrubber.py\n+++ b/sentry_sdk/scrubber.py\n@@ -59,19 +59,36 @@\n \n \n class EventScrubber(object):\n-    def __init__(self, denylist=None):\n-        # type: (Optional[List[str]]) -> None\n+    def __init__(self, denylist=None, recursive=False):\n+        # type: (Optional[List[str]], bool) -> None\n         self.denylist = DEFAULT_DENYLIST if denylist is None else denylist\n         self.denylist = [x.lower() for x in self.denylist]\n+        self.recursive = recursive\n+\n+    def scrub_list(self, lst):\n+        # type: (List[Any]) -> None\n+        if not isinstance(lst, list):\n+            return\n+\n+        for v in lst:\n+            if isinstance(v, dict):\n+                self.scrub_dict(v)\n+            elif isinstance(v, list):\n+                self.scrub_list(v)\n \n     def scrub_dict(self, d):\n         # type: (Dict[str, Any]) -> None\n         if not isinstance(d, dict):\n             return\n \n-        for k in d.keys():\n+        for k, v in d.items():\n             if isinstance(k, string_types) and k.lower() in self.denylist:\n                 d[k] = AnnotatedValue.substituted_because_contains_sensitive_data()\n+            elif self.recursive:\n+                if isinstance(v, dict):\n+                    self.scrub_dict(v)\n+                elif isinstance(v, list):\n+                    self.scrub_list(v)\n \n     def scrub_request(self, event):\n         # type: (Event) -> None\n", "issue": "Make EventScrubber recursive\n### Problem Statement\r\n\r\nWe have a custom `before_send` implementation that scrubs data recursively. I was hopping to replace the custom implementation with the built-in EventScrubber but I found out that it doesn't scrub `vars` recursively.\r\n\r\nAs far as I can tell this was a consistency, perf trade-off thing but it would be nice to have a built-in option to make it recursive.\r\n\r\nThank you!\r\n\r\n### Solution Brainstorm\r\n\r\n`EventScrubber(recursive=True)`\n", "before_files": [{"content": "from sentry_sdk.utils import (\n    capture_internal_exceptions,\n    AnnotatedValue,\n    iter_event_frames,\n)\nfrom sentry_sdk._compat import string_types\nfrom sentry_sdk._types import TYPE_CHECKING\n\nif TYPE_CHECKING:\n    from sentry_sdk._types import Event\n    from typing import Any\n    from typing import Dict\n    from typing import List\n    from typing import Optional\n\n\nDEFAULT_DENYLIST = [\n    # stolen from relay\n    \"password\",\n    \"passwd\",\n    \"secret\",\n    \"api_key\",\n    \"apikey\",\n    \"auth\",\n    \"credentials\",\n    \"mysql_pwd\",\n    \"privatekey\",\n    \"private_key\",\n    \"token\",\n    \"ip_address\",\n    \"session\",\n    # django\n    \"csrftoken\",\n    \"sessionid\",\n    # wsgi\n    \"remote_addr\",\n    \"x_csrftoken\",\n    \"x_forwarded_for\",\n    \"set_cookie\",\n    \"cookie\",\n    \"authorization\",\n    \"x_api_key\",\n    \"x_forwarded_for\",\n    \"x_real_ip\",\n    # other common names used in the wild\n    \"aiohttp_session\",  # aiohttp\n    \"connect.sid\",  # Express\n    \"csrf_token\",  # Pyramid\n    \"csrf\",  # (this is a cookie name used in accepted answers on stack overflow)\n    \"_csrf\",  # Express\n    \"_csrf_token\",  # Bottle\n    \"PHPSESSID\",  # PHP\n    \"_session\",  # Sanic\n    \"symfony\",  # Symfony\n    \"user_session\",  # Vue\n    \"_xsrf\",  # Tornado\n    \"XSRF-TOKEN\",  # Angular, Laravel\n]\n\n\nclass EventScrubber(object):\n    def __init__(self, denylist=None):\n        # type: (Optional[List[str]]) -> None\n        self.denylist = DEFAULT_DENYLIST if denylist is None else denylist\n        self.denylist = [x.lower() for x in self.denylist]\n\n    def scrub_dict(self, d):\n        # type: (Dict[str, Any]) -> None\n        if not isinstance(d, dict):\n            return\n\n        for k in d.keys():\n            if isinstance(k, string_types) and k.lower() in self.denylist:\n                d[k] = AnnotatedValue.substituted_because_contains_sensitive_data()\n\n    def scrub_request(self, event):\n        # type: (Event) -> None\n        with capture_internal_exceptions():\n            if \"request\" in event:\n                if \"headers\" in event[\"request\"]:\n                    self.scrub_dict(event[\"request\"][\"headers\"])\n                if \"cookies\" in event[\"request\"]:\n                    self.scrub_dict(event[\"request\"][\"cookies\"])\n                if \"data\" in event[\"request\"]:\n                    self.scrub_dict(event[\"request\"][\"data\"])\n\n    def scrub_extra(self, event):\n        # type: (Event) -> None\n        with capture_internal_exceptions():\n            if \"extra\" in event:\n                self.scrub_dict(event[\"extra\"])\n\n    def scrub_user(self, event):\n        # type: (Event) -> None\n        with capture_internal_exceptions():\n            if \"user\" in event:\n                self.scrub_dict(event[\"user\"])\n\n    def scrub_breadcrumbs(self, event):\n        # type: (Event) -> None\n        with capture_internal_exceptions():\n            if \"breadcrumbs\" in event:\n                if \"values\" in event[\"breadcrumbs\"]:\n                    for value in event[\"breadcrumbs\"][\"values\"]:\n                        if \"data\" in value:\n                            self.scrub_dict(value[\"data\"])\n\n    def scrub_frames(self, event):\n        # type: (Event) -> None\n        with capture_internal_exceptions():\n            for frame in iter_event_frames(event):\n                if \"vars\" in frame:\n                    self.scrub_dict(frame[\"vars\"])\n\n    def scrub_spans(self, event):\n        # type: (Event) -> None\n        with capture_internal_exceptions():\n            if \"spans\" in event:\n                for span in event[\"spans\"]:\n                    if \"data\" in span:\n                        self.scrub_dict(span[\"data\"])\n\n    def scrub_event(self, event):\n        # type: (Event) -> None\n        self.scrub_request(event)\n        self.scrub_extra(event)\n        self.scrub_user(event)\n        self.scrub_breadcrumbs(event)\n        self.scrub_frames(event)\n        self.scrub_spans(event)\n", "path": "sentry_sdk/scrubber.py"}], "after_files": [{"content": "from sentry_sdk.utils import (\n    capture_internal_exceptions,\n    AnnotatedValue,\n    iter_event_frames,\n)\nfrom sentry_sdk._compat import string_types\nfrom sentry_sdk._types import TYPE_CHECKING\n\nif TYPE_CHECKING:\n    from sentry_sdk._types import Event\n    from typing import Any\n    from typing import Dict\n    from typing import List\n    from typing import Optional\n\n\nDEFAULT_DENYLIST = [\n    # stolen from relay\n    \"password\",\n    \"passwd\",\n    \"secret\",\n    \"api_key\",\n    \"apikey\",\n    \"auth\",\n    \"credentials\",\n    \"mysql_pwd\",\n    \"privatekey\",\n    \"private_key\",\n    \"token\",\n    \"ip_address\",\n    \"session\",\n    # django\n    \"csrftoken\",\n    \"sessionid\",\n    # wsgi\n    \"remote_addr\",\n    \"x_csrftoken\",\n    \"x_forwarded_for\",\n    \"set_cookie\",\n    \"cookie\",\n    \"authorization\",\n    \"x_api_key\",\n    \"x_forwarded_for\",\n    \"x_real_ip\",\n    # other common names used in the wild\n    \"aiohttp_session\",  # aiohttp\n    \"connect.sid\",  # Express\n    \"csrf_token\",  # Pyramid\n    \"csrf\",  # (this is a cookie name used in accepted answers on stack overflow)\n    \"_csrf\",  # Express\n    \"_csrf_token\",  # Bottle\n    \"PHPSESSID\",  # PHP\n    \"_session\",  # Sanic\n    \"symfony\",  # Symfony\n    \"user_session\",  # Vue\n    \"_xsrf\",  # Tornado\n    \"XSRF-TOKEN\",  # Angular, Laravel\n]\n\n\nclass EventScrubber(object):\n    def __init__(self, denylist=None, recursive=False):\n        # type: (Optional[List[str]], bool) -> None\n        self.denylist = DEFAULT_DENYLIST if denylist is None else denylist\n        self.denylist = [x.lower() for x in self.denylist]\n        self.recursive = recursive\n\n    def scrub_list(self, lst):\n        # type: (List[Any]) -> None\n        if not isinstance(lst, list):\n            return\n\n        for v in lst:\n            if isinstance(v, dict):\n                self.scrub_dict(v)\n            elif isinstance(v, list):\n                self.scrub_list(v)\n\n    def scrub_dict(self, d):\n        # type: (Dict[str, Any]) -> None\n        if not isinstance(d, dict):\n            return\n\n        for k, v in d.items():\n            if isinstance(k, string_types) and k.lower() in self.denylist:\n                d[k] = AnnotatedValue.substituted_because_contains_sensitive_data()\n            elif self.recursive:\n                if isinstance(v, dict):\n                    self.scrub_dict(v)\n                elif isinstance(v, list):\n                    self.scrub_list(v)\n\n    def scrub_request(self, event):\n        # type: (Event) -> None\n        with capture_internal_exceptions():\n            if \"request\" in event:\n                if \"headers\" in event[\"request\"]:\n                    self.scrub_dict(event[\"request\"][\"headers\"])\n                if \"cookies\" in event[\"request\"]:\n                    self.scrub_dict(event[\"request\"][\"cookies\"])\n                if \"data\" in event[\"request\"]:\n                    self.scrub_dict(event[\"request\"][\"data\"])\n\n    def scrub_extra(self, event):\n        # type: (Event) -> None\n        with capture_internal_exceptions():\n            if \"extra\" in event:\n                self.scrub_dict(event[\"extra\"])\n\n    def scrub_user(self, event):\n        # type: (Event) -> None\n        with capture_internal_exceptions():\n            if \"user\" in event:\n                self.scrub_dict(event[\"user\"])\n\n    def scrub_breadcrumbs(self, event):\n        # type: (Event) -> None\n        with capture_internal_exceptions():\n            if \"breadcrumbs\" in event:\n                if \"values\" in event[\"breadcrumbs\"]:\n                    for value in event[\"breadcrumbs\"][\"values\"]:\n                        if \"data\" in value:\n                            self.scrub_dict(value[\"data\"])\n\n    def scrub_frames(self, event):\n        # type: (Event) -> None\n        with capture_internal_exceptions():\n            for frame in iter_event_frames(event):\n                if \"vars\" in frame:\n                    self.scrub_dict(frame[\"vars\"])\n\n    def scrub_spans(self, event):\n        # type: (Event) -> None\n        with capture_internal_exceptions():\n            if \"spans\" in event:\n                for span in event[\"spans\"]:\n                    if \"data\" in span:\n                        self.scrub_dict(span[\"data\"])\n\n    def scrub_event(self, event):\n        # type: (Event) -> None\n        self.scrub_request(event)\n        self.scrub_extra(event)\n        self.scrub_user(event)\n        self.scrub_breadcrumbs(event)\n        self.scrub_frames(event)\n        self.scrub_spans(event)\n", "path": "sentry_sdk/scrubber.py"}]}