Repository: getsentry/sentry-python

Issue:
Threading integration breaks OpenCensus' threading integration
### How do you use Sentry?

Sentry Saas (sentry.io)

### Version

1.21.1

### Steps to Reproduce

1. Enable threading Sentry integration (it seems to be enabled by default)
2. Enable [threading OpenCensus integration](https://pypi.org/project/opencensus-ext-threading/)
3. Start a thread

```python
import threading

import sentry_sdk.integrations.threading
import opencensus.trace.config_integration

sentry_sdk.init(integrations=[
    sentry_sdk.integrations.threading.ThreadingIntegration(),
])

opencensus.trace.config_integration.trace_integrations(
    integrations=["threading"],
)

thread = threading.Thread(target=print, args=("foo",))
thread.start()
thread.join()
```

### Expected Result

No error, output: `foo`

### Actual Result

Exceptions raised on every thread's startup

```python
Exception in thread Thread-1 (print):
Traceback (most recent call last):
  File "/usr/lib/python3.10/threading.py", line 1016, in _bootstrap_inner
    self.run()
  File ".venv/lib/python3.10/site-packages/sentry_sdk/integrations/threading.py", line 69, in run
    reraise(*_capture_exception())
  File ".venv/lib/python3.10/site-packages/sentry_sdk/_compat.py", line 60, in reraise
    raise value
  File ".venv/lib/python3.10/site-packages/sentry_sdk/integrations/threading.py", line 67, in run
    return old_run_func(self, *a, **kw)
  File ".venv/lib/python3.10/site-packages/opencensus/ext/threading/trace.py", line 80, in call
    *self._opencensus_context
AttributeError: 'Thread' object has no attribute '_opencensus_context'
```

### Additional details

* Python: 3.10
* OpenCensus threading extension: 0.1.2

My suggestion is to use [`functools.update_wrapper`](https://docs.python.org/3/library/functools.html#functools.update_wrapper) (or the convenience decorator [`functools.wraps`](https://docs.python.org/3/library/functools.html#functools.wraps)) to set the name of [`sentry_start`](https://github.com/getsentry/sentry-python/blob/f763061ed9d9e99d85b3e95adc3ed63b623fc4a0/sentry_sdk/integrations/threading.py#L35-54) to `"start"` (and maybe the same for the result of `_wrap_run`, to `"run"`), so it "holds up to basic introspection".

PS: please include 'Additional details' block in issue template


Discussion:

Resolving patch:
diff --git a/sentry_sdk/integrations/threading.py b/sentry_sdk/integrations/threading.py
--- a/sentry_sdk/integrations/threading.py
+++ b/sentry_sdk/integrations/threading.py
@@ -1,6 +1,7 @@
 from __future__ import absolute_import
 
 import sys
+from functools import wraps
 from threading import Thread, current_thread
 
 from sentry_sdk import Hub
@@ -32,6 +33,7 @@ def setup_once():
         # type: () -> None
         old_start = Thread.start
 
+        @wraps(old_start)
         def sentry_start(self, *a, **kw):
             # type: (Thread, *Any, **Any) -> Any
             hub = Hub.current
@@ -58,6 +60,7 @@ def sentry_start(self, *a, **kw):
 
 def _wrap_run(parent_hub, old_run_func):
     # type: (Optional[Hub], F) -> F
+    @wraps(old_run_func)
     def run(*a, **kw):
         # type: (*Any, **Any) -> Any
         hub = parent_hub or Hub.current


Test patch:
diff --git a/tests/integrations/threading/test_threading.py b/tests/integrations/threading/test_threading.py
--- a/tests/integrations/threading/test_threading.py
+++ b/tests/integrations/threading/test_threading.py
@@ -7,6 +7,9 @@
 from sentry_sdk import configure_scope, capture_message
 from sentry_sdk.integrations.threading import ThreadingIntegration
 
+original_start = Thread.start
+original_run = Thread.run
+
 
 @pytest.mark.forked
 @pytest.mark.parametrize("integrations", [[ThreadingIntegration()], []])
@@ -114,3 +117,25 @@ def run(self):
     for event in events:
         (exception,) = event["exception"]["values"]
         assert exception["type"] == "ZeroDivisionError"
+
+
+def test_wrapper_attributes(sentry_init):
+    sentry_init(default_integrations=False, integrations=[ThreadingIntegration()])
+
+    def target():
+        assert t.run.__name__ == "run"
+        assert t.run.__qualname__ == original_run.__qualname__
+
+    t = Thread(target=target)
+    t.start()
+    t.join()
+
+    assert Thread.start.__name__ == "start"
+    assert Thread.start.__qualname__ == original_start.__qualname__
+    assert t.start.__name__ == "start"
+    assert t.start.__qualname__ == original_start.__qualname__
+
+    assert Thread.run.__name__ == "run"
+    assert Thread.run.__qualname__ == original_run.__qualname__
+    assert t.run.__name__ == "run"
+    assert t.run.__qualname__ == original_run.__qualname__