Repository: getsentry/sentry-python

Issue:
Slow performance when Sentry integration is enabled
### How do you use Sentry?

Sentry Saas (sentry.io)

### Version

1.11.1

### Steps to Reproduce

Set the environment variable SENTRY_ENABLED to True
Run the command: `python manage.py sqlmigrate app` 

Record the time taken.

Python version: `3.9.15`
Django version: `3.2.17`
Os version: Debian `10.13 Slim`



```python
if SENTRY_DSN and SENTRY_ENABLED and not BUILD_MODE and not TEST_MODE:
    sentry_sdk.init(
        release=f"app@{VERSION}",
        environment=ENVIRONMENT,
        integrations=[DjangoIntegration()],
        ignore_errors=SENTRY_IGNORE_EXCEPTIONS,
        send_default_pii=True,
        sample_rate=min(SENTRY_EVENTS_SAMPLE_RATE, 1.0),
        traces_sample_rate=min(SENTRY_TRACES_SAMPLE_RATE, 1.0),
    )
```

### Expected Result

We have noticed that there is a significant impact on performance when the Sentry integration is enabled.

```
time SENTRY_ENABLED=False ./src/manage.py sqlmigrate app
13min

time SENTRY_ENABLED=True ./src/manage.py sqlmigrate app
~14min
```

### Actual Result

```
time SENTRY_ENABLED=False ./src/manage.py sqlmigrate app
13min

time SENTRY_ENABLED=True ./src/manage.py sqlmigrate app
28min
```

<img width="1496" alt="Screenshot 2023-02-09 at 18 00 58" src="https://user-images.githubusercontent.com/4227868/217885584-69f55769-16c1-43de-8dc6-0fd349e229c5.png">




Discussion:
@antonpirker as discussed, please disable signals instrumentation by default and make it opt-in.
Looks similar to https://github.com/getsentry/sentry-python/issues/1826

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
@@ -90,9 +90,12 @@ class DjangoIntegration(Integration):
 
     transaction_style = ""
     middleware_spans = None
+    signals_spans = None
 
-    def __init__(self, transaction_style="url", middleware_spans=True):
-        # type: (str, bool) -> None
+    def __init__(
+        self, transaction_style="url", middleware_spans=True, signals_spans=True
+    ):
+        # type: (str, bool, bool) -> None
         if transaction_style not in TRANSACTION_STYLE_VALUES:
             raise ValueError(
                 "Invalid value for transaction_style: %s (must be in %s)"
@@ -100,6 +103,7 @@ def __init__(self, transaction_style="url", middleware_spans=True):
             )
         self.transaction_style = transaction_style
         self.middleware_spans = middleware_spans
+        self.signals_spans = signals_spans
 
     @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
@@ -43,6 +43,7 @@ def _get_receiver_name(receiver):
 def patch_signals():
     # type: () -> None
     """Patch django signal receivers to create a span"""
+    from sentry_sdk.integrations.django import DjangoIntegration
 
     old_live_receivers = Signal._live_receivers
 
@@ -66,8 +67,10 @@ def wrapper(*args, **kwargs):
 
             return wrapper
 
-        for idx, receiver in enumerate(receivers):
-            receivers[idx] = sentry_receiver_wrapper(receiver)
+        integration = hub.get_integration(DjangoIntegration)
+        if integration and integration.signals_spans:
+            for idx, receiver in enumerate(receivers):
+                receivers[idx] = sentry_receiver_wrapper(receiver)
 
         return receivers
 


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
@@ -670,7 +670,7 @@ def test_does_not_capture_403(sentry_init, client, capture_events, endpoint):
     sentry_init(integrations=[DjangoIntegration()])
     events = capture_events()
 
-    _content, status, _headers = client.get(reverse(endpoint))
+    _, status, _ = client.get(reverse(endpoint))
     assert status.lower() == "403 forbidden"
 
     assert not events
@@ -697,32 +697,14 @@ def test_render_spans(sentry_init, client, capture_events, render_span_tree):
 
     for url, expected_line in views_tests:
         events = capture_events()
-        _content, status, _headers = client.get(url)
+        client.get(url)
         transaction = events[0]
         assert expected_line in render_span_tree(transaction)
 
 
-def test_middleware_spans(sentry_init, client, capture_events, render_span_tree):
-    sentry_init(
-        integrations=[DjangoIntegration()],
-        traces_sample_rate=1.0,
-        _experiments={"record_sql_params": True},
-    )
-    events = capture_events()
-
-    _content, status, _headers = client.get(reverse("message"))
-
-    message, transaction = events
-
-    assert message["message"] == "hi"
-
-    if DJANGO_VERSION >= (1, 10):
-        assert (
-            render_span_tree(transaction)
-            == """\
+if DJANGO_VERSION >= (1, 10):
+    EXPECTED_MIDDLEWARE_SPANS = """\
 - op="http.server": description=null
-  - op="event.django": description="django.db.reset_queries"
-  - op="event.django": description="django.db.close_old_connections"
   - op="middleware.django": description="django.contrib.sessions.middleware.SessionMiddleware.__call__"
     - op="middleware.django": description="django.contrib.auth.middleware.AuthenticationMiddleware.__call__"
       - op="middleware.django": description="django.middleware.csrf.CsrfViewMiddleware.__call__"
@@ -731,15 +713,9 @@ def test_middleware_spans(sentry_init, client, capture_events, render_span_tree)
             - op="middleware.django": description="django.middleware.csrf.CsrfViewMiddleware.process_view"
             - op="view.render": description="message"\
 """
-        )
-
-    else:
-        assert (
-            render_span_tree(transaction)
-            == """\
+else:
+    EXPECTED_MIDDLEWARE_SPANS = """\
 - op="http.server": description=null
-  - op="event.django": description="django.db.reset_queries"
-  - op="event.django": description="django.db.close_old_connections"
   - op="middleware.django": description="django.contrib.sessions.middleware.SessionMiddleware.process_request"
   - op="middleware.django": description="django.contrib.auth.middleware.AuthenticationMiddleware.process_request"
   - op="middleware.django": description="tests.integrations.django.myapp.settings.TestMiddleware.process_request"
@@ -749,22 +725,71 @@ def test_middleware_spans(sentry_init, client, capture_events, render_span_tree)
   - op="middleware.django": description="django.middleware.csrf.CsrfViewMiddleware.process_response"
   - op="middleware.django": description="django.contrib.sessions.middleware.SessionMiddleware.process_response"\
 """
-        )
+
+
+def test_middleware_spans(sentry_init, client, capture_events, render_span_tree):
+    sentry_init(
+        integrations=[
+            DjangoIntegration(signals_spans=False),
+        ],
+        traces_sample_rate=1.0,
+    )
+    events = capture_events()
+
+    client.get(reverse("message"))
+
+    message, transaction = events
+
+    assert message["message"] == "hi"
+    assert render_span_tree(transaction) == EXPECTED_MIDDLEWARE_SPANS
 
 
 def test_middleware_spans_disabled(sentry_init, client, capture_events):
     sentry_init(
-        integrations=[DjangoIntegration(middleware_spans=False)], traces_sample_rate=1.0
+        integrations=[
+            DjangoIntegration(middleware_spans=False, signals_spans=False),
+        ],
+        traces_sample_rate=1.0,
     )
     events = capture_events()
 
-    _content, status, _headers = client.get(reverse("message"))
+    client.get(reverse("message"))
 
     message, transaction = events
 
     assert message["message"] == "hi"
+    assert not len(transaction["spans"])
+
+
+if DJANGO_VERSION >= (1, 10):
+    EXPECTED_SIGNALS_SPANS = """\
+- op="http.server": description=null
+  - op="event.django": description="django.db.reset_queries"
+  - op="event.django": description="django.db.close_old_connections"\
+"""
+else:
+    EXPECTED_SIGNALS_SPANS = """\
+- op="http.server": description=null
+  - op="event.django": description="django.db.reset_queries"
+  - op="event.django": description="django.db.close_old_connections"\
+"""
+
+
+def test_signals_spans(sentry_init, client, capture_events, render_span_tree):
+    sentry_init(
+        integrations=[
+            DjangoIntegration(middleware_spans=False),
+        ],
+        traces_sample_rate=1.0,
+    )
+    events = capture_events()
 
-    assert len(transaction["spans"]) == 2
+    client.get(reverse("message"))
+
+    message, transaction = events
+
+    assert message["message"] == "hi"
+    assert render_span_tree(transaction) == EXPECTED_SIGNALS_SPANS
 
     assert transaction["spans"][0]["op"] == "event.django"
     assert transaction["spans"][0]["description"] == "django.db.reset_queries"
@@ -773,6 +798,23 @@ def test_middleware_spans_disabled(sentry_init, client, capture_events):
     assert transaction["spans"][1]["description"] == "django.db.close_old_connections"
 
 
+def test_signals_spans_disabled(sentry_init, client, capture_events):
+    sentry_init(
+        integrations=[
+            DjangoIntegration(middleware_spans=False, signals_spans=False),
+        ],
+        traces_sample_rate=1.0,
+    )
+    events = capture_events()
+
+    client.get(reverse("message"))
+
+    message, transaction = events
+
+    assert message["message"] == "hi"
+    assert not transaction["spans"]
+
+
 def test_csrf(sentry_init, client):
     """
     Assert that CSRF view decorator works even with the view wrapped in our own