Repository: getsentry/sentry-python

Issue:
Possible performance regression related to django signals instrumentation
### How do you use Sentry?

Self-hosted/on-premise

### Version

1.12.1

### Steps to Reproduce

Updated `sentry-sdk` from `1.9.8` to `1.12.1`.

### Expected Result

Overall project performance not changed

### Actual Result

Performance decreased for about 25%.

Looks like instrumentation of some django signals (https://github.com/getsentry/sentry-python/pull/1526/) brought some overhead.
In my specific example, I've used https://github.com/matthewwithanm/django-imagekit for thumbnails generation. And this [lib uses `post_init` signal](https://github.com/matthewwithanm/django-imagekit/blob/762f9990e9c20824db005798626ece030da89cd4/imagekit/specs/sourcegroups.py#L60) to track changes of `ImageField` values.
For cases when there is about 1000 instances in response of DRF API method, `post_init` handler is called for a lot of times (even for models that actually does not have any `ImageField`). `imagekit` itself implements a fast check if handler should be called. However, looks like instrumentation of this slow down overall time of response.

See screenshots with example request. The first one is with commented out [`patch_signals`](https://github.com/getsentry/sentry-python/blob/2f67f12e405f8a6f89418d96071158367fcf516f/sentry_sdk/integrations/django/__init__.py#L217) call:

![image](https://user-images.githubusercontent.com/7027722/211202879-9a65c320-9271-4ad8-9345-e07693563eb0.png)

The second is with enabled signals instrumentation:
![image](https://user-images.githubusercontent.com/7027722/211202958-981bed57-0c10-4549-a83f-60e6d101ba55.png)

I think that sentry option that allow to disable instrumentation of some specific signals/receivers will allow to solve this problem. Or maybe option that will allow to disable specific instrumentation.

P.S. I used following monkey-patch to disable signals instrumentation and restore previous performance:
```python
from sentry_sdk.integrations import django
django.patch_signals = lambda: None

sentry_sdk.init(
	"integrations": [django.DjangoIntegration()],
	"dsn": "dsn",
)
```


Discussion:
I would also love to have this configurable.  On signal or sender level.
~What I additionally noticed was that I am not able to easily see the connected receivers of a signal by calling
e.g. `post_save._live_receivers(AnyModel)` as they are wrapped now. But this should be solvable by using `functools.wraps` to update the wrapper function~ : signature was fixed in #1700


Hey @kanu nice to see you here! 

We have an issue for adding a configuration to disable signal instrumentation: https://github.com/getsentry/sentry-python/issues/1739
@antonpirker I've checked performance with changes from https://github.com/getsentry/sentry-python/pull/1929 (`sentry-sdk==1.17.0`) and it solved the problem. Thanks for fixing this!

Resolving patch:
diff --git a/sentry_sdk/integrations/django/__init__.py b/sentry_sdk/integrations/django/__init__.py
--- a/sentry_sdk/integrations/django/__init__.py
+++ b/sentry_sdk/integrations/django/__init__.py
@@ -114,6 +114,7 @@ class DjangoIntegration(Integration):
     middleware_spans = None
     signals_spans = None
     cache_spans = None
+    signals_denylist = []  # type: list[signals.Signal]
 
     def __init__(
         self,
@@ -121,8 +122,9 @@ def __init__(
         middleware_spans=True,
         signals_spans=True,
         cache_spans=False,
+        signals_denylist=None,
     ):
-        # type: (str, bool, bool, bool) -> None
+        # type: (str, bool, bool, bool, Optional[list[signals.Signal]]) -> None
         if transaction_style not in TRANSACTION_STYLE_VALUES:
             raise ValueError(
                 "Invalid value for transaction_style: %s (must be in %s)"
@@ -132,6 +134,7 @@ def __init__(
         self.middleware_spans = middleware_spans
         self.signals_spans = signals_spans
         self.cache_spans = cache_spans
+        self.signals_denylist = signals_denylist or []
 
     @staticmethod
     def setup_once():
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
@@ -78,7 +78,11 @@ def wrapper(*args, **kwargs):
             return wrapper
 
         integration = hub.get_integration(DjangoIntegration)
-        if integration and integration.signals_spans:
+        if (
+            integration
+            and integration.signals_spans
+            and self not in integration.signals_denylist
+        ):
             for idx, receiver in enumerate(sync_receivers):
                 sync_receivers[idx] = sentry_sync_receiver_wrapper(receiver)
 


Test patch:
diff --git a/tests/integrations/django/myapp/signals.py b/tests/integrations/django/myapp/signals.py
new file mode 100644
--- /dev/null
+++ b/tests/integrations/django/myapp/signals.py
@@ -0,0 +1,15 @@
+from django.core import signals
+from django.dispatch import receiver
+
+myapp_custom_signal = signals.Signal()
+myapp_custom_signal_silenced = signals.Signal()
+
+
+@receiver(myapp_custom_signal)
+def signal_handler(sender, **kwargs):
+    assert sender == "hello"
+
+
+@receiver(myapp_custom_signal_silenced)
+def signal_handler_silenced(sender, **kwargs):
+    assert sender == "hello"
diff --git a/tests/integrations/django/myapp/urls.py b/tests/integrations/django/myapp/urls.py
--- a/tests/integrations/django/myapp/urls.py
+++ b/tests/integrations/django/myapp/urls.py
@@ -76,6 +76,11 @@ def path(path, *args, **kwargs):
         name="csrf_hello_not_exempt",
     ),
     path("sync/thread_ids", views.thread_ids_sync, name="thread_ids_sync"),
+    path(
+        "send-myapp-custom-signal",
+        views.send_myapp_custom_signal,
+        name="send_myapp_custom_signal",
+    ),
 ]
 
 # async views
diff --git a/tests/integrations/django/myapp/views.py b/tests/integrations/django/myapp/views.py
--- a/tests/integrations/django/myapp/views.py
+++ b/tests/integrations/django/myapp/views.py
@@ -14,6 +14,11 @@
 from django.views.decorators.csrf import csrf_exempt
 from django.views.generic import ListView
 
+from tests.integrations.django.myapp.signals import (
+    myapp_custom_signal,
+    myapp_custom_signal_silenced,
+)
+
 try:
     from rest_framework.decorators import api_view
     from rest_framework.response import Response
@@ -253,3 +258,10 @@ def thread_ids_sync(*args, **kwargs):
     my_async_view = None
     thread_ids_async = None
     post_echo_async = None
+
+
+@csrf_exempt
+def send_myapp_custom_signal(request):
+    myapp_custom_signal.send(sender="hello")
+    myapp_custom_signal_silenced.send(sender="hello")
+    return HttpResponse("ok")
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
@@ -29,6 +29,7 @@
 from sentry_sdk.tracing import Span
 from tests.conftest import ApproxDict, unpack_werkzeug_response
 from tests.integrations.django.myapp.wsgi import application
+from tests.integrations.django.myapp.signals import myapp_custom_signal_silenced
 from tests.integrations.django.utils import pytest_mark_django_db_decorator
 
 DJANGO_VERSION = DJANGO_VERSION[:2]
@@ -1035,6 +1036,47 @@ def test_signals_spans_disabled(sentry_init, client, capture_events):
     assert not transaction["spans"]
 
 
+EXPECTED_SIGNALS_SPANS_FILTERED = """\
+- op="http.server": description=null
+  - op="event.django": description="django.db.reset_queries"
+  - op="event.django": description="django.db.close_old_connections"
+  - op="event.django": description="tests.integrations.django.myapp.signals.signal_handler"\
+"""
+
+
+def test_signals_spans_filtering(sentry_init, client, capture_events, render_span_tree):
+    sentry_init(
+        integrations=[
+            DjangoIntegration(
+                middleware_spans=False,
+                signals_denylist=[
+                    myapp_custom_signal_silenced,
+                ],
+            ),
+        ],
+        traces_sample_rate=1.0,
+    )
+    events = capture_events()
+
+    client.get(reverse("send_myapp_custom_signal"))
+
+    (transaction,) = events
+
+    assert render_span_tree(transaction) == EXPECTED_SIGNALS_SPANS_FILTERED
+
+    assert transaction["spans"][0]["op"] == "event.django"
+    assert transaction["spans"][0]["description"] == "django.db.reset_queries"
+
+    assert transaction["spans"][1]["op"] == "event.django"
+    assert transaction["spans"][1]["description"] == "django.db.close_old_connections"
+
+    assert transaction["spans"][2]["op"] == "event.django"
+    assert (
+        transaction["spans"][2]["description"]
+        == "tests.integrations.django.myapp.signals.signal_handler"
+    )
+
+
 def test_csrf(sentry_init, client):
     """
     Assert that CSRF view decorator works even with the view wrapped in our own