Repository: getsentry/sentry-python

Issue:
Django Signals integration breaks on partial objects for python <3.10
### How do you use Sentry?

Self-hosted/on-premise

### Version

1.9.9

### Steps to Reproduce

1. Use python older than 3.10.
2. Register a partial function as a signal handler.

### Expected Result

Signal is traced correctly.

### Actual Result

Exception is raised from `_get_receiver_name` function as `partial` objects don't have `__module__` before python 3.10 (and even there it's undocumented from what I can see).

It fails in our tests where we don't even register any signals so either Django itself or some kind of integration (Sentry?) registers such signals by default.

The whole signals integration is missing a `capture_internal_exceptions` context too I believe.


Discussion:
Hello @Agalin !

Thanks for writing in!

I have now tried to reproduce this. 

I created a partial signal handler from my existing handler:
![Screenshot 2022-09-27 at 10 19 05](https://user-images.githubusercontent.com/202325/192473043-fe3929f4-c67a-4b01-8bd3-f676e3b21ae7.png)

Connected the signal like usually:
![Screenshot 2022-09-27 at 10 19 44](https://user-images.githubusercontent.com/202325/192473147-b82df8ba-b65e-49b7-9677-6d126de34363.png)

And then refreshed the page a couple of times and the partial signal handler is showing up in performance:
![Screenshot 2022-09-27 at 10 17 57](https://user-images.githubusercontent.com/202325/192473335-8db3b61b-e85d-4d86-9ce2-4916384dc6ce.png)

I tried this using Python 3.9.11, 3.8.13 and 3.7.12 and I never got an exception. 
Could you supply me with a mini project where I could reproduce this problem?
Interesting. As said earlier - we don't use signals ourselves so I need to verify where does it come from to reproduce (possible candidate besides Sentry is pytest-django). Stacktrace seems to suggest Django's test client being at fault:

<details>
<summary>Stacktrace</summary>

```
/home/application/.local/lib/python3.7/site-packages/django/test/client.py:751: in post
    response = super().post(path, data=data, content_type=content_type, secure=secure, **extra)
/home/application/.local/lib/python3.7/site-packages/django/test/client.py:408: in post
    secure=secure, **extra)
/home/application/.local/lib/python3.7/site-packages/django/test/client.py:473: in generic
    return self.request(**r)
/home/application/.local/lib/python3.7/site-packages/django/test/client.py:714: in request
    response = self.handler(environ)
/home/application/.local/lib/python3.7/site-packages/django/test/client.py:145: in __call__
    response = self.get_response(request)
/home/application/.local/lib/python3.7/site-packages/sentry_sdk/integrations/django/__init__.py:409: in sentry_patched_get_response
    rv = old_get_response(self, request)
/home/application/.local/lib/python3.7/site-packages/django/core/handlers/base.py:136: in get_response
    request=request,
/home/application/.local/lib/python3.7/site-packages/django/utils/log.py:230: in log_response
    exc_info=exc_info,
/usr/local/lib/python3.7/logging/__init__.py:1407: in error
    self._log(ERROR, msg, args, **kwargs)
/usr/local/lib/python3.7/logging/__init__.py:1514: in _log
    self.handle(record)
/usr/local/lib/python3.7/logging/__init__.py:1524: in handle
    self.callHandlers(record)
/home/application/.local/lib/python3.7/site-packages/sentry_sdk/integrations/logging.py:96: in sentry_patched_callhandlers
    return old_callhandlers(self, record)
/usr/local/lib/python3.7/logging/__init__.py:1586: in callHandlers
    hdlr.handle(record)
/usr/local/lib/python3.7/logging/__init__.py:894: in handle
    self.emit(record)
/home/application/.local/lib/python3.7/site-packages/django/utils/log.py:120: in emit
    message = "%s\n\n%s" % (self.format(no_exc_record), reporter.get_traceback_text())
/home/application/.local/lib/python3.7/site-packages/django/views/debug.py:353: in get_traceback_text
    return t.render(c)
/home/application/.local/lib/python3.7/site-packages/django/template/base.py:170: in render
    return self._render(context)
/home/application/.local/lib/python3.7/site-packages/django/test/utils.py:99: in instrumented_test_render
    template_rendered.send(sender=self, template=self, context=context)
/home/application/.local/lib/python3.7/site-packages/django/dispatch/dispatcher.py:182: in send
    for receiver in self._live_receivers(sender)
/home/application/.local/lib/python3.7/site-packages/django/dispatch/dispatcher.py:182: in <listcomp>
    for receiver in self._live_receivers(sender)
/home/application/.local/lib/python3.7/site-packages/sentry_sdk/integrations/django/signals_handlers.py:40: in wrapper
    description=_get_receiver_name(receiver),
```
</details>
Oh. Didn't mention that earlier: it's Django 3.2.15 with pytest-django 4.5.2 - it may be relevant.

Resolving patch:
diff --git a/sentry_sdk/integrations/django/signals_handlers.py b/sentry_sdk/integrations/django/signals_handlers.py
--- a/sentry_sdk/integrations/django/signals_handlers.py
+++ b/sentry_sdk/integrations/django/signals_handlers.py
@@ -13,19 +13,32 @@
     from typing import List
 
 
+def _get_receiver_name(receiver):
+    # type: (Callable[..., Any]) -> str
+    name = ""
+
+    if hasattr(receiver, "__qualname__"):
+        name += receiver.__qualname__
+    elif hasattr(receiver, "__name__"):  # Python 2.7 has no __qualname__
+        name += receiver.__name__
+
+    if (
+        name == ""
+    ):  # certain functions (like partials) dont have a name so return the string representation
+        return str(receiver)
+
+    if hasattr(receiver, "__module__"):  # prepend with module, if there is one
+        name = receiver.__module__ + "." + name
+
+    return name
+
+
 def patch_signals():
     # type: () -> None
     """Patch django signal receivers to create a span"""
 
     old_live_receivers = Signal._live_receivers
 
-    def _get_receiver_name(receiver):
-        # type: (Callable[..., Any]) -> str
-        name = receiver.__module__ + "."
-        if hasattr(receiver, "__name__"):
-            return name + receiver.__name__
-        return name + str(receiver)
-
     def _sentry_live_receivers(self, sender):
         # type: (Signal, Any) -> List[Callable[..., Any]]
         hub = Hub.current
@@ -35,11 +48,12 @@ def sentry_receiver_wrapper(receiver):
             # type: (Callable[..., Any]) -> Callable[..., Any]
             def wrapper(*args, **kwargs):
                 # type: (Any, Any) -> Any
+                signal_name = _get_receiver_name(receiver)
                 with hub.start_span(
                     op="django.signals",
-                    description=_get_receiver_name(receiver),
+                    description=signal_name,
                 ) as span:
-                    span.set_data("signal", _get_receiver_name(receiver))
+                    span.set_data("signal", signal_name)
                     return receiver(*args, **kwargs)
 
             return wrapper


Test patch:
diff --git a/tests/integrations/django/test_basic.py b/tests/integrations/django/test_basic.py
--- a/tests/integrations/django/test_basic.py
+++ b/tests/integrations/django/test_basic.py
@@ -1,8 +1,9 @@
 from __future__ import absolute_import
 
+import json
 import pytest
 import pytest_django
-import json
+from functools import partial
 
 from werkzeug.test import Client
 from django import VERSION as DJANGO_VERSION
@@ -10,16 +11,16 @@
 from django.core.management import execute_from_command_line
 from django.db.utils import OperationalError, ProgrammingError, DataError
 
-from sentry_sdk.integrations.executing import ExecutingIntegration
-
 try:
     from django.urls import reverse
 except ImportError:
     from django.core.urlresolvers import reverse
 
+from sentry_sdk._compat import PY2
 from sentry_sdk import capture_message, capture_exception, configure_scope
 from sentry_sdk.integrations.django import DjangoIntegration
-from functools import partial
+from sentry_sdk.integrations.django.signals_handlers import _get_receiver_name
+from sentry_sdk.integrations.executing import ExecutingIntegration
 
 from tests.integrations.django.myapp.wsgi import application
 
@@ -816,3 +817,22 @@ def test_custom_urlconf_middleware(
     assert "custom_urlconf_middleware" in render_span_tree(transaction_event)
 
     settings.MIDDLEWARE.pop(0)
+
+
+def test_get_receiver_name():
+    def dummy(a, b):
+        return a + b
+
+    name = _get_receiver_name(dummy)
+
+    if PY2:
+        assert name == "tests.integrations.django.test_basic.dummy"
+    else:
+        assert (
+            name
+            == "tests.integrations.django.test_basic.test_get_receiver_name.<locals>.dummy"
+        )
+
+    a_partial = partial(dummy)
+    name = _get_receiver_name(a_partial)
+    assert name == str(a_partial)