prompt: We are currently solving the following issue within our repository. Here is the issue text:
  --- BEGIN ISSUE ---
  Memory leak with unclosed span in opentelemetry integration
  ### How do you use Sentry?
  
  Sentry Saas (sentry.io)
  
  ### Version
  
  1.40.3
  
  ### Steps to Reproduce
  
  `SentrySpanProcessor` stores all open span in `self.otel_span_map` dict. This leads to a memory leak if otel span is deleted without closing. E.g. due to a bug: https://github.com/open-telemetry/opentelemetry-python-contrib/issues/2149
  
  ### Expected Result
  
  Sentry span should be deleted after otel span is deleted by GC.
  
  ### Actual Result
  
  Sentry spans are stored in self.otel_span_map forever.
  
  --- 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 try:
  2     from typing import cast
  3 except ImportError:
  4     cast = lambda _, obj: obj
  5 
  6 from sentry_sdk.utils import (
  7     capture_internal_exceptions,
  8     AnnotatedValue,
  9     iter_event_frames,
  10 )
  11 from sentry_sdk._types import TYPE_CHECKING
  12 
  13 if TYPE_CHECKING:
  14     from sentry_sdk._types import Event
  15     from typing import List
  16     from typing import Optional
  17 
  18 
  19 DEFAULT_DENYLIST = [
  20     # stolen from relay
  21     "password",
  22     "passwd",
  23     "secret",
  24     "api_key",
  25     "apikey",
  26     "auth",
  27     "credentials",
  28     "mysql_pwd",
  29     "privatekey",
  30     "private_key",
  31     "token",
  32     "ip_address",
  33     "session",
  34     # django
  35     "csrftoken",
  36     "sessionid",
  37     # wsgi
  38     "remote_addr",
  39     "x_csrftoken",
  40     "x_forwarded_for",
  41     "set_cookie",
  42     "cookie",
  43     "authorization",
  44     "x_api_key",
  45     "x_forwarded_for",
  46     "x_real_ip",
  47     # other common names used in the wild
  48     "aiohttp_session",  # aiohttp
  49     "connect.sid",  # Express
  50     "csrf_token",  # Pyramid
  51     "csrf",  # (this is a cookie name used in accepted answers on stack overflow)
  52     "_csrf",  # Express
  53     "_csrf_token",  # Bottle
  54     "PHPSESSID",  # PHP
  55     "_session",  # Sanic
  56     "symfony",  # Symfony
  57     "user_session",  # Vue
  58     "_xsrf",  # Tornado
  59     "XSRF-TOKEN",  # Angular, Laravel
  60 ]
  61 
  62 
  63 class EventScrubber(object):
  64     def __init__(self, denylist=None, recursive=False):
  65         # type: (Optional[List[str]], bool) -> None
  66         self.denylist = DEFAULT_DENYLIST if denylist is None else denylist
  67         self.denylist = [x.lower() for x in self.denylist]
  68         self.recursive = recursive
  69 
  70     def scrub_list(self, lst):
  71         # type: (object) -> None
  72         """
  73         If a list is passed to this method, the method recursively searches the list and any
  74         nested lists for any dictionaries. The method calls scrub_dict on all dictionaries
  75         it finds.
  76         If the parameter passed to this method is not a list, the method does nothing.
  77         """
  78         if not isinstance(lst, list):
  79             return
  80 
  81         for v in lst:
  82             self.scrub_dict(v)  # no-op unless v is a dict
  83             self.scrub_list(v)  # no-op unless v is a list
  84 
  85     def scrub_dict(self, d):
  86         # type: (object) -> None
  87         """
  88         If a dictionary is passed to this method, the method scrubs the dictionary of any
  89         sensitive data. The method calls itself recursively on any nested dictionaries (
  90         including dictionaries nested in lists) if self.recursive is True.
  91         This method does nothing if the parameter passed to it is not a dictionary.
  92         """
  93         if not isinstance(d, dict):
  94             return
  95 
  96         for k, v in d.items():
  97             if isinstance(k, str) and k.lower() in self.denylist:
  98                 d[k] = AnnotatedValue.substituted_because_contains_sensitive_data()
  99             elif self.recursive:
  100                 self.scrub_dict(v)  # no-op unless v is a dict
  101                 self.scrub_list(v)  # no-op unless v is a list
  102 
  103     def scrub_request(self, event):
  104         # type: (Event) -> None
  105         with capture_internal_exceptions():
  106             if "request" in event:
  107                 if "headers" in event["request"]:
  108                     self.scrub_dict(event["request"]["headers"])
  109                 if "cookies" in event["request"]:
  110                     self.scrub_dict(event["request"]["cookies"])
  111                 if "data" in event["request"]:
  112                     self.scrub_dict(event["request"]["data"])
  113 
  114     def scrub_extra(self, event):
  115         # type: (Event) -> None
  116         with capture_internal_exceptions():
  117             if "extra" in event:
  118                 self.scrub_dict(event["extra"])
  119 
  120     def scrub_user(self, event):
  121         # type: (Event) -> None
  122         with capture_internal_exceptions():
  123             if "user" in event:
  124                 self.scrub_dict(event["user"])
  125 
  126     def scrub_breadcrumbs(self, event):
  127         # type: (Event) -> None
  128         with capture_internal_exceptions():
  129             if "breadcrumbs" in event:
  130                 if "values" in event["breadcrumbs"]:
  131                     for value in event["breadcrumbs"]["values"]:
  132                         if "data" in value:
  133                             self.scrub_dict(value["data"])
  134 
  135     def scrub_frames(self, event):
  136         # type: (Event) -> None
  137         with capture_internal_exceptions():
  138             for frame in iter_event_frames(event):
  139                 if "vars" in frame:
  140                     self.scrub_dict(frame["vars"])
  141 
  142     def scrub_spans(self, event):
  143         # type: (Event) -> None
  144         with capture_internal_exceptions():
  145             if "spans" in event:
  146                 for span in event["spans"]:
  147                     if "data" in span:
  148                         self.scrub_dict(span["data"])
  149 
  150     def scrub_event(self, event):
  151         # type: (Event) -> None
  152         self.scrub_request(event)
  153         self.scrub_extra(event)
  154         self.scrub_user(event)
  155         self.scrub_breadcrumbs(event)
  156         self.scrub_frames(event)
  157         self.scrub_spans(event)
  158
  ```
  --- 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()
  
  ```
  
golden_diff: diff --git a/sentry_sdk/scrubber.py b/sentry_sdk/scrubber.py
  --- a/sentry_sdk/scrubber.py
  +++ b/sentry_sdk/scrubber.py
  @@ -1,8 +1,3 @@
  -try:
  -    from typing import cast
  -except ImportError:
  -    cast = lambda _, obj: obj
  -
   from sentry_sdk.utils import (
       capture_internal_exceptions,
       AnnotatedValue,
  @@ -94,6 +89,8 @@
               return
   
           for k, v in d.items():
  +            # The cast is needed because mypy is not smart enough to figure out that k must be a
  +            # string after the isinstance check.
               if isinstance(k, str) and k.lower() in self.denylist:
                   d[k] = AnnotatedValue.substituted_because_contains_sensitive_data()
               elif self.recursive:
  
verification_info: {"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@@ -1,8 +1,3 @@\n-try:\n-    from typing import cast\n-except ImportError:\n-    cast = lambda _, obj: obj\n-\n from sentry_sdk.utils import (\n     capture_internal_exceptions,\n     AnnotatedValue,\n@@ -94,6 +89,8 @@\n             return\n \n         for k, v in d.items():\n+            # The cast is needed because mypy is not smart enough to figure out that k must be a\n+            # string after the isinstance check.\n             if isinstance(k, str) and k.lower() in self.denylist:\n                 d[k] = AnnotatedValue.substituted_because_contains_sensitive_data()\n             elif self.recursive:\n", "issue": "Memory leak with unclosed span in opentelemetry integration\n### How do you use Sentry?\n\nSentry Saas (sentry.io)\n\n### Version\n\n1.40.3\n\n### Steps to Reproduce\n\n`SentrySpanProcessor` stores all open span in `self.otel_span_map` dict. This leads to a memory leak if otel span is deleted without closing. E.g. due to a bug: https://github.com/open-telemetry/opentelemetry-python-contrib/issues/2149\n\n### Expected Result\n\nSentry span should be deleted after otel span is deleted by GC.\n\n### Actual Result\n\nSentry spans are stored in self.otel_span_map forever.\n", "before_files": [{"content": "try:\n    from typing import cast\nexcept ImportError:\n    cast = lambda _, obj: obj\n\nfrom sentry_sdk.utils import (\n    capture_internal_exceptions,\n    AnnotatedValue,\n    iter_event_frames,\n)\nfrom sentry_sdk._types import TYPE_CHECKING\n\nif TYPE_CHECKING:\n    from sentry_sdk._types import Event\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: (object) -> None\n        \"\"\"\n        If a list is passed to this method, the method recursively searches the list and any\n        nested lists for any dictionaries. The method calls scrub_dict on all dictionaries\n        it finds.\n        If the parameter passed to this method is not a list, the method does nothing.\n        \"\"\"\n        if not isinstance(lst, list):\n            return\n\n        for v in lst:\n            self.scrub_dict(v)  # no-op unless v is a dict\n            self.scrub_list(v)  # no-op unless v is a list\n\n    def scrub_dict(self, d):\n        # type: (object) -> None\n        \"\"\"\n        If a dictionary is passed to this method, the method scrubs the dictionary of any\n        sensitive data. The method calls itself recursively on any nested dictionaries (\n        including dictionaries nested in lists) if self.recursive is True.\n        This method does nothing if the parameter passed to it is not a dictionary.\n        \"\"\"\n        if not isinstance(d, dict):\n            return\n\n        for k, v in d.items():\n            if isinstance(k, str) and k.lower() in self.denylist:\n                d[k] = AnnotatedValue.substituted_because_contains_sensitive_data()\n            elif self.recursive:\n                self.scrub_dict(v)  # no-op unless v is a dict\n                self.scrub_list(v)  # no-op unless v is a list\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._types import TYPE_CHECKING\n\nif TYPE_CHECKING:\n    from sentry_sdk._types import Event\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: (object) -> None\n        \"\"\"\n        If a list is passed to this method, the method recursively searches the list and any\n        nested lists for any dictionaries. The method calls scrub_dict on all dictionaries\n        it finds.\n        If the parameter passed to this method is not a list, the method does nothing.\n        \"\"\"\n        if not isinstance(lst, list):\n            return\n\n        for v in lst:\n            self.scrub_dict(v)  # no-op unless v is a dict\n            self.scrub_list(v)  # no-op unless v is a list\n\n    def scrub_dict(self, d):\n        # type: (object) -> None\n        \"\"\"\n        If a dictionary is passed to this method, the method scrubs the dictionary of any\n        sensitive data. The method calls itself recursively on any nested dictionaries (\n        including dictionaries nested in lists) if self.recursive is True.\n        This method does nothing if the parameter passed to it is not a dictionary.\n        \"\"\"\n        if not isinstance(d, dict):\n            return\n\n        for k, v in d.items():\n            # The cast is needed because mypy is not smart enough to figure out that k must be a\n            # string after the isinstance check.\n            if isinstance(k, str) and k.lower() in self.denylist:\n                d[k] = AnnotatedValue.substituted_because_contains_sensitive_data()\n            elif self.recursive:\n                self.scrub_dict(v)  # no-op unless v is a dict\n                self.scrub_list(v)  # no-op unless v is a list\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"}]}