Skip to content

Commit 11e1f9a

Browse files
authored
feat(integrations): Add django signals_denylist to filter signals that are attached to by signals_span (#2758)
1 parent f5ec34c commit 11e1f9a

File tree

6 files changed

+83
-2
lines changed

6 files changed

+83
-2
lines changed

sentry_sdk/integrations/django/__init__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,15 +114,17 @@ class DjangoIntegration(Integration):
114114
middleware_spans = None
115115
signals_spans = None
116116
cache_spans = None
117+
signals_denylist = [] # type: list[signals.Signal]
117118

118119
def __init__(
119120
self,
120121
transaction_style="url",
121122
middleware_spans=True,
122123
signals_spans=True,
123124
cache_spans=False,
125+
signals_denylist=None,
124126
):
125-
# type: (str, bool, bool, bool) -> None
127+
# type: (str, bool, bool, bool, Optional[list[signals.Signal]]) -> None
126128
if transaction_style not in TRANSACTION_STYLE_VALUES:
127129
raise ValueError(
128130
"Invalid value for transaction_style: %s (must be in %s)"
@@ -132,6 +134,7 @@ def __init__(
132134
self.middleware_spans = middleware_spans
133135
self.signals_spans = signals_spans
134136
self.cache_spans = cache_spans
137+
self.signals_denylist = signals_denylist or []
135138

136139
@staticmethod
137140
def setup_once():

sentry_sdk/integrations/django/signals_handlers.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,11 @@ def wrapper(*args, **kwargs):
7878
return wrapper
7979

8080
integration = hub.get_integration(DjangoIntegration)
81-
if integration and integration.signals_spans:
81+
if (
82+
integration
83+
and integration.signals_spans
84+
and self not in integration.signals_denylist
85+
):
8286
for idx, receiver in enumerate(sync_receivers):
8387
sync_receivers[idx] = sentry_sync_receiver_wrapper(receiver)
8488

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from django.core import signals
2+
from django.dispatch import receiver
3+
4+
myapp_custom_signal = signals.Signal()
5+
myapp_custom_signal_silenced = signals.Signal()
6+
7+
8+
@receiver(myapp_custom_signal)
9+
def signal_handler(sender, **kwargs):
10+
assert sender == "hello"
11+
12+
13+
@receiver(myapp_custom_signal_silenced)
14+
def signal_handler_silenced(sender, **kwargs):
15+
assert sender == "hello"

tests/integrations/django/myapp/urls.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,11 @@ def path(path, *args, **kwargs):
7676
name="csrf_hello_not_exempt",
7777
),
7878
path("sync/thread_ids", views.thread_ids_sync, name="thread_ids_sync"),
79+
path(
80+
"send-myapp-custom-signal",
81+
views.send_myapp_custom_signal,
82+
name="send_myapp_custom_signal",
83+
),
7984
]
8085

8186
# async views

tests/integrations/django/myapp/views.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@
1414
from django.views.decorators.csrf import csrf_exempt
1515
from django.views.generic import ListView
1616

17+
from tests.integrations.django.myapp.signals import (
18+
myapp_custom_signal,
19+
myapp_custom_signal_silenced,
20+
)
21+
1722
try:
1823
from rest_framework.decorators import api_view
1924
from rest_framework.response import Response
@@ -253,3 +258,10 @@ def thread_ids_sync(*args, **kwargs):
253258
my_async_view = None
254259
thread_ids_async = None
255260
post_echo_async = None
261+
262+
263+
@csrf_exempt
264+
def send_myapp_custom_signal(request):
265+
myapp_custom_signal.send(sender="hello")
266+
myapp_custom_signal_silenced.send(sender="hello")
267+
return HttpResponse("ok")

tests/integrations/django/test_basic.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
from sentry_sdk.tracing import Span
3030
from tests.conftest import ApproxDict, unpack_werkzeug_response
3131
from tests.integrations.django.myapp.wsgi import application
32+
from tests.integrations.django.myapp.signals import myapp_custom_signal_silenced
3233
from tests.integrations.django.utils import pytest_mark_django_db_decorator
3334

3435
DJANGO_VERSION = DJANGO_VERSION[:2]
@@ -1035,6 +1036,47 @@ def test_signals_spans_disabled(sentry_init, client, capture_events):
10351036
assert not transaction["spans"]
10361037

10371038

1039+
EXPECTED_SIGNALS_SPANS_FILTERED = """\
1040+
- op="http.server": description=null
1041+
- op="event.django": description="django.db.reset_queries"
1042+
- op="event.django": description="django.db.close_old_connections"
1043+
- op="event.django": description="tests.integrations.django.myapp.signals.signal_handler"\
1044+
"""
1045+
1046+
1047+
def test_signals_spans_filtering(sentry_init, client, capture_events, render_span_tree):
1048+
sentry_init(
1049+
integrations=[
1050+
DjangoIntegration(
1051+
middleware_spans=False,
1052+
signals_denylist=[
1053+
myapp_custom_signal_silenced,
1054+
],
1055+
),
1056+
],
1057+
traces_sample_rate=1.0,
1058+
)
1059+
events = capture_events()
1060+
1061+
client.get(reverse("send_myapp_custom_signal"))
1062+
1063+
(transaction,) = events
1064+
1065+
assert render_span_tree(transaction) == EXPECTED_SIGNALS_SPANS_FILTERED
1066+
1067+
assert transaction["spans"][0]["op"] == "event.django"
1068+
assert transaction["spans"][0]["description"] == "django.db.reset_queries"
1069+
1070+
assert transaction["spans"][1]["op"] == "event.django"
1071+
assert transaction["spans"][1]["description"] == "django.db.close_old_connections"
1072+
1073+
assert transaction["spans"][2]["op"] == "event.django"
1074+
assert (
1075+
transaction["spans"][2]["description"]
1076+
== "tests.integrations.django.myapp.signals.signal_handler"
1077+
)
1078+
1079+
10381080
def test_csrf(sentry_init, client):
10391081
"""
10401082
Assert that CSRF view decorator works even with the view wrapped in our own

0 commit comments

Comments
 (0)