Skip to content

Commit 6f1aa1f

Browse files
authored
feat(tracing): Add more sampling context for asgi, celery, rq, and wsgi (#906)
1 parent 49657a4 commit 6f1aa1f

File tree

8 files changed

+296
-4
lines changed

8 files changed

+296
-4
lines changed

sentry_sdk/integrations/asgi.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,9 @@ async def _run_app(self, scope, callback):
139139
transaction.name = _DEFAULT_TRANSACTION_NAME
140140
transaction.set_tag("asgi.type", ty)
141141

142-
with hub.start_transaction(transaction):
142+
with hub.start_transaction(
143+
transaction, custom_sampling_context={"asgi_scope": scope}
144+
):
143145
# XXX: Would be cool to have correct span status, but we
144146
# would have to wrap send(). That is a bit hard to do with
145147
# the current abstraction over ASGI 2/3.

sentry_sdk/integrations/celery.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,18 @@ def _inner(*args, **kwargs):
159159
if transaction is None:
160160
return f(*args, **kwargs)
161161

162-
with hub.start_transaction(transaction):
162+
with hub.start_transaction(
163+
transaction,
164+
custom_sampling_context={
165+
"celery_job": {
166+
"task": task.name,
167+
# for some reason, args[1] is a list if non-empty but a
168+
# tuple if empty
169+
"args": list(args[1]),
170+
"kwargs": args[2],
171+
}
172+
},
173+
):
163174
return f(*args, **kwargs)
164175

165176
return _inner # type: ignore

sentry_sdk/integrations/rq.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,9 @@ def sentry_patched_perform_job(self, job, *args, **kwargs):
7070
with capture_internal_exceptions():
7171
transaction.name = job.func_name
7272

73-
with hub.start_transaction(transaction):
73+
with hub.start_transaction(
74+
transaction, custom_sampling_context={"rq_job": job}
75+
):
7476
rv = old_perform_job(self, job, *args, **kwargs)
7577

7678
if self.is_horse:

sentry_sdk/integrations/wsgi.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,9 @@ def __call__(self, environ, start_response):
117117
environ, op="http.server", name="generic WSGI request"
118118
)
119119

120-
with hub.start_transaction(transaction):
120+
with hub.start_transaction(
121+
transaction, custom_sampling_context={"wsgi_environ": environ}
122+
):
121123
try:
122124
rv = self.app(
123125
environ,

tests/integrations/asgi/test_asgi.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@
88
from starlette.testclient import TestClient
99
from starlette.websockets import WebSocket
1010

11+
try:
12+
from unittest import mock # python 3.3 and above
13+
except ImportError:
14+
import mock # python < 3.3
15+
1116

1217
@pytest.fixture
1318
def app():
@@ -202,3 +207,47 @@ def handler(*args, **kwargs):
202207
(exception,) = event["exception"]["values"]
203208
assert exception["type"] == "ValueError"
204209
assert exception["value"] == "oh no"
210+
211+
212+
def test_transaction(app, sentry_init, capture_events):
213+
sentry_init(traces_sample_rate=1.0)
214+
events = capture_events()
215+
216+
@app.route("/tricks/kangaroo")
217+
def kangaroo_handler(request):
218+
return PlainTextResponse("dogs are great")
219+
220+
client = TestClient(app)
221+
client.get("/tricks/kangaroo")
222+
223+
event = events[0]
224+
assert event["type"] == "transaction"
225+
assert (
226+
event["transaction"]
227+
== "tests.integrations.asgi.test_asgi.test_transaction.<locals>.kangaroo_handler"
228+
)
229+
230+
231+
def test_traces_sampler_gets_scope_in_sampling_context(
232+
app, sentry_init, DictionaryContaining # noqa: N803
233+
):
234+
traces_sampler = mock.Mock()
235+
sentry_init(traces_sampler=traces_sampler)
236+
237+
@app.route("/tricks/kangaroo")
238+
def kangaroo_handler(request):
239+
return PlainTextResponse("dogs are great")
240+
241+
client = TestClient(app)
242+
client.get("/tricks/kangaroo")
243+
244+
traces_sampler.assert_any_call(
245+
DictionaryContaining(
246+
{
247+
# starlette just uses a dictionary to hold the scope
248+
"asgi_scope": DictionaryContaining(
249+
{"method": "GET", "path": "/tricks/kangaroo"}
250+
)
251+
}
252+
)
253+
)

tests/integrations/celery/test_celery.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@
1111
from celery import Celery, VERSION
1212
from celery.bin import worker
1313

14+
try:
15+
from unittest import mock # python 3.3 and above
16+
except ImportError:
17+
import mock # python < 3.3
18+
1419

1520
@pytest.fixture
1621
def connect_signal(request):
@@ -379,3 +384,26 @@ def dummy_task(self, x, y):
379384

380385
assert dummy_task.apply(kwargs={"x": 1, "y": 1}).wait() == 1
381386
assert celery_invocation(dummy_task, 1, 1)[0].wait() == 1
387+
388+
389+
def test_traces_sampler_gets_task_info_in_sampling_context(
390+
init_celery, celery_invocation, DictionaryContaining # noqa:N803
391+
):
392+
traces_sampler = mock.Mock()
393+
celery = init_celery(traces_sampler=traces_sampler)
394+
395+
@celery.task(name="dog_walk")
396+
def walk_dogs(x, y):
397+
dogs, route = x
398+
num_loops = y
399+
return dogs, route, num_loops
400+
401+
_, args_kwargs = celery_invocation(
402+
walk_dogs, [["Maisey", "Charlie", "Bodhi", "Cory"], "Dog park round trip"], 1
403+
)
404+
405+
traces_sampler.assert_any_call(
406+
# depending on the iteration of celery_invocation, the data might be
407+
# passed as args or as kwargs, so make this generic
408+
DictionaryContaining({"celery_job": dict(task="dog_walk", **args_kwargs)})
409+
)

tests/integrations/rq/test_rq.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@
55
from fakeredis import FakeStrictRedis
66
import rq
77

8+
try:
9+
from unittest import mock # python 3.3 and above
10+
except ImportError:
11+
import mock # python < 3.3
12+
813

914
@pytest.fixture(autouse=True)
1015
def _patch_rq_get_server_version(monkeypatch):
@@ -28,6 +33,14 @@ def crashing_job(foo):
2833
1 / 0
2934

3035

36+
def chew_up_shoes(dog, human, shoes):
37+
raise Exception("{}!! Why did you eat {}'s {}??".format(dog, human, shoes))
38+
39+
40+
def do_trick(dog, trick):
41+
return "{}, can you {}? Good dog!".format(dog, trick)
42+
43+
3144
def test_basic(sentry_init, capture_events):
3245
sentry_init(integrations=[RqIntegration()])
3346
events = capture_events()
@@ -71,3 +84,96 @@ def test_transport_shutdown(sentry_init, capture_events_forksafe):
7184

7285
(exception,) = event["exception"]["values"]
7386
assert exception["type"] == "ZeroDivisionError"
87+
88+
89+
def test_transaction_with_error(
90+
sentry_init, capture_events, DictionaryContaining # noqa:N803
91+
):
92+
93+
sentry_init(integrations=[RqIntegration()], traces_sample_rate=1.0)
94+
events = capture_events()
95+
96+
queue = rq.Queue(connection=FakeStrictRedis())
97+
worker = rq.SimpleWorker([queue], connection=queue.connection)
98+
99+
queue.enqueue(chew_up_shoes, "Charlie", "Katie", shoes="flip-flops")
100+
worker.work(burst=True)
101+
102+
error_event, envelope = events
103+
104+
assert error_event["transaction"] == "tests.integrations.rq.test_rq.chew_up_shoes"
105+
assert error_event["contexts"]["trace"]["op"] == "rq.task"
106+
assert error_event["exception"]["values"][0]["type"] == "Exception"
107+
assert (
108+
error_event["exception"]["values"][0]["value"]
109+
== "Charlie!! Why did you eat Katie's flip-flops??"
110+
)
111+
112+
assert envelope["type"] == "transaction"
113+
assert envelope["contexts"]["trace"] == error_event["contexts"]["trace"]
114+
assert envelope["transaction"] == error_event["transaction"]
115+
assert envelope["extra"]["rq-job"] == DictionaryContaining(
116+
{
117+
"args": ["Charlie", "Katie"],
118+
"kwargs": {"shoes": "flip-flops"},
119+
"func": "tests.integrations.rq.test_rq.chew_up_shoes",
120+
"description": "tests.integrations.rq.test_rq.chew_up_shoes('Charlie', 'Katie', shoes='flip-flops')",
121+
}
122+
)
123+
124+
125+
def test_transaction_no_error(
126+
sentry_init, capture_events, DictionaryContaining # noqa:N803
127+
):
128+
sentry_init(integrations=[RqIntegration()], traces_sample_rate=1.0)
129+
events = capture_events()
130+
131+
queue = rq.Queue(connection=FakeStrictRedis())
132+
worker = rq.SimpleWorker([queue], connection=queue.connection)
133+
134+
queue.enqueue(do_trick, "Maisey", trick="kangaroo")
135+
worker.work(burst=True)
136+
137+
envelope = events[0]
138+
139+
assert envelope["type"] == "transaction"
140+
assert envelope["contexts"]["trace"]["op"] == "rq.task"
141+
assert envelope["transaction"] == "tests.integrations.rq.test_rq.do_trick"
142+
assert envelope["extra"]["rq-job"] == DictionaryContaining(
143+
{
144+
"args": ["Maisey"],
145+
"kwargs": {"trick": "kangaroo"},
146+
"func": "tests.integrations.rq.test_rq.do_trick",
147+
"description": "tests.integrations.rq.test_rq.do_trick('Maisey', trick='kangaroo')",
148+
}
149+
)
150+
151+
152+
def test_traces_sampler_gets_correct_values_in_sampling_context(
153+
sentry_init, DictionaryContaining, ObjectDescribedBy # noqa:N803
154+
):
155+
traces_sampler = mock.Mock(return_value=True)
156+
sentry_init(integrations=[RqIntegration()], traces_sampler=traces_sampler)
157+
158+
queue = rq.Queue(connection=FakeStrictRedis())
159+
worker = rq.SimpleWorker([queue], connection=queue.connection)
160+
161+
queue.enqueue(do_trick, "Bodhi", trick="roll over")
162+
worker.work(burst=True)
163+
164+
traces_sampler.assert_any_call(
165+
DictionaryContaining(
166+
{
167+
"rq_job": ObjectDescribedBy(
168+
type=rq.job.Job,
169+
attrs={
170+
"description": "tests.integrations.rq.test_rq.do_trick('Bodhi', trick='roll over')",
171+
"result": "Bodhi, can you roll over? Good dog!",
172+
"func_name": "tests.integrations.rq.test_rq.do_trick",
173+
"args": ("Bodhi",),
174+
"kwargs": {"trick": "roll over"},
175+
},
176+
),
177+
}
178+
)
179+
)

tests/integrations/wsgi/test_wsgi.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@
33

44
from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware
55

6+
try:
7+
from unittest import mock # python 3.3 and above
8+
except ImportError:
9+
import mock # python < 3.3
10+
611

712
@pytest.fixture
813
def crashing_app():
@@ -109,3 +114,90 @@ def test_keyboard_interrupt_is_captured(sentry_init, capture_events):
109114
assert exc["type"] == "KeyboardInterrupt"
110115
assert exc["value"] == ""
111116
assert event["level"] == "error"
117+
118+
119+
def test_transaction_with_error(
120+
sentry_init, crashing_app, capture_events, DictionaryContaining # noqa:N803
121+
):
122+
def dogpark(environ, start_response):
123+
raise Exception("Fetch aborted. The ball was not returned.")
124+
125+
sentry_init(send_default_pii=True, traces_sample_rate=1.0)
126+
app = SentryWsgiMiddleware(dogpark)
127+
client = Client(app)
128+
events = capture_events()
129+
130+
with pytest.raises(Exception):
131+
client.get("http://dogs.are.great/sit/stay/rollover/")
132+
133+
error_event, envelope = events
134+
135+
assert error_event["transaction"] == "generic WSGI request"
136+
assert error_event["contexts"]["trace"]["op"] == "http.server"
137+
assert error_event["exception"]["values"][0]["type"] == "Exception"
138+
assert (
139+
error_event["exception"]["values"][0]["value"]
140+
== "Fetch aborted. The ball was not returned."
141+
)
142+
143+
assert envelope["type"] == "transaction"
144+
145+
# event trace context is a subset of envelope trace context
146+
assert envelope["contexts"]["trace"] == DictionaryContaining(
147+
error_event["contexts"]["trace"]
148+
)
149+
assert envelope["contexts"]["trace"]["status"] == "internal_error"
150+
assert envelope["transaction"] == error_event["transaction"]
151+
assert envelope["request"] == error_event["request"]
152+
153+
154+
def test_transaction_no_error(
155+
sentry_init, capture_events, DictionaryContaining # noqa:N803
156+
):
157+
def dogpark(environ, start_response):
158+
start_response("200 OK", [])
159+
return ["Go get the ball! Good dog!"]
160+
161+
sentry_init(send_default_pii=True, traces_sample_rate=1.0)
162+
app = SentryWsgiMiddleware(dogpark)
163+
client = Client(app)
164+
events = capture_events()
165+
166+
client.get("/dogs/are/great/")
167+
168+
envelope = events[0]
169+
170+
assert envelope["type"] == "transaction"
171+
assert envelope["transaction"] == "generic WSGI request"
172+
assert envelope["contexts"]["trace"]["op"] == "http.server"
173+
assert envelope["request"] == DictionaryContaining(
174+
{"method": "GET", "url": "http://localhost/dogs/are/great/"}
175+
)
176+
177+
178+
def test_traces_sampler_gets_correct_values_in_sampling_context(
179+
sentry_init, DictionaryContaining, ObjectDescribedBy # noqa:N803
180+
):
181+
def app(environ, start_response):
182+
start_response("200 OK", [])
183+
return ["Go get the ball! Good dog!"]
184+
185+
traces_sampler = mock.Mock(return_value=True)
186+
sentry_init(send_default_pii=True, traces_sampler=traces_sampler)
187+
app = SentryWsgiMiddleware(app)
188+
client = Client(app)
189+
190+
client.get("/dogs/are/great/")
191+
192+
traces_sampler.assert_any_call(
193+
DictionaryContaining(
194+
{
195+
"wsgi_environ": DictionaryContaining(
196+
{
197+
"PATH_INFO": "/dogs/are/great/",
198+
"REQUEST_METHOD": "GET",
199+
},
200+
),
201+
}
202+
)
203+
)

0 commit comments

Comments
 (0)