Skip to content

Commit 46c24ea

Browse files
authored
Set response status code in transaction "response" context. (#2312)
Make sure that the HTTP response status code be set in the transactions "response" context. This works in WSGI (was already calling set_http_status.) Also added this to ASGI projects. Fixes #2289
1 parent 838368c commit 46c24ea

File tree

7 files changed

+217
-39
lines changed

7 files changed

+217
-39
lines changed

sentry_sdk/integrations/asgi.py

+30-9
Original file line numberDiff line numberDiff line change
@@ -132,20 +132,24 @@ def _run_asgi2(self, scope):
132132
# type: (Any) -> Any
133133
async def inner(receive, send):
134134
# type: (Any, Any) -> Any
135-
return await self._run_app(scope, lambda: self.app(scope)(receive, send))
135+
return await self._run_app(scope, receive, send, asgi_version=2)
136136

137137
return inner
138138

139139
async def _run_asgi3(self, scope, receive, send):
140140
# type: (Any, Any, Any) -> Any
141-
return await self._run_app(scope, lambda: self.app(scope, receive, send))
141+
return await self._run_app(scope, receive, send, asgi_version=3)
142142

143-
async def _run_app(self, scope, callback):
144-
# type: (Any, Any) -> Any
143+
async def _run_app(self, scope, receive, send, asgi_version):
144+
# type: (Any, Any, Any, Any, int) -> Any
145145
is_recursive_asgi_middleware = _asgi_middleware_applied.get(False)
146146
if is_recursive_asgi_middleware:
147147
try:
148-
return await callback()
148+
if asgi_version == 2:
149+
return await self.app(scope)(receive, send)
150+
else:
151+
return await self.app(scope, receive, send)
152+
149153
except Exception as exc:
150154
_capture_exception(Hub.current, exc, mechanism_type=self.mechanism_type)
151155
raise exc from None
@@ -178,11 +182,28 @@ async def _run_app(self, scope, callback):
178182
with hub.start_transaction(
179183
transaction, custom_sampling_context={"asgi_scope": scope}
180184
):
181-
# XXX: Would be cool to have correct span status, but we
182-
# would have to wrap send(). That is a bit hard to do with
183-
# the current abstraction over ASGI 2/3.
184185
try:
185-
return await callback()
186+
187+
async def _sentry_wrapped_send(event):
188+
# type: (Dict[str, Any]) -> Any
189+
is_http_response = (
190+
event.get("type") == "http.response.start"
191+
and transaction is not None
192+
and "status" in event
193+
)
194+
if is_http_response:
195+
transaction.set_http_status(event["status"])
196+
197+
return await send(event)
198+
199+
if asgi_version == 2:
200+
return await self.app(scope)(
201+
receive, _sentry_wrapped_send
202+
)
203+
else:
204+
return await self.app(
205+
scope, receive, _sentry_wrapped_send
206+
)
186207
except Exception as exc:
187208
_capture_exception(
188209
hub, exc, mechanism_type=self.mechanism_type

sentry_sdk/tracing.py

+5
Original file line numberDiff line numberDiff line change
@@ -663,6 +663,11 @@ def set_context(self, key, value):
663663
# type: (str, Any) -> None
664664
self._contexts[key] = value
665665

666+
def set_http_status(self, http_status):
667+
# type: (int) -> None
668+
super(Transaction, self).set_http_status(http_status)
669+
self.set_context("response", {"status_code": http_status})
670+
666671
def to_json(self):
667672
# type: () -> Dict[str, Any]
668673
rv = super(Transaction, self).to_json()

tests/integrations/asgi/test_asgi.py

+14-17
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,11 @@ async def app(scope, receive, send):
4848

4949
@pytest.fixture
5050
def asgi3_app_with_error():
51+
async def send_with_error(event):
52+
1 / 0
53+
5154
async def app(scope, receive, send):
52-
await send(
55+
await send_with_error(
5356
{
5457
"type": "http.response.start",
5558
"status": 200,
@@ -58,10 +61,7 @@ async def app(scope, receive, send):
5861
],
5962
}
6063
)
61-
62-
1 / 0
63-
64-
await send(
64+
await send_with_error(
6565
{
6666
"type": "http.response.body",
6767
"body": b"Hello, world!",
@@ -167,9 +167,9 @@ async def test_capture_transaction_with_error(
167167
sentry_init(send_default_pii=True, traces_sample_rate=1.0)
168168
app = SentryAsgiMiddleware(asgi3_app_with_error)
169169

170+
events = capture_events()
170171
with pytest.raises(ZeroDivisionError):
171172
async with TestClient(app) as client:
172-
events = capture_events()
173173
await client.get("/")
174174

175175
(error_event, transaction_event) = events
@@ -395,38 +395,35 @@ async def test_auto_session_tracking_with_aggregates(
395395
(
396396
"/message",
397397
"endpoint",
398-
"tests.integrations.asgi.test_asgi.asgi3_app_with_error.<locals>.app",
398+
"tests.integrations.asgi.test_asgi.asgi3_app.<locals>.app",
399399
"component",
400400
),
401401
],
402402
)
403403
@pytest.mark.asyncio
404404
async def test_transaction_style(
405405
sentry_init,
406-
asgi3_app_with_error,
406+
asgi3_app,
407407
capture_events,
408408
url,
409409
transaction_style,
410410
expected_transaction,
411411
expected_source,
412412
):
413413
sentry_init(send_default_pii=True, traces_sample_rate=1.0)
414-
app = SentryAsgiMiddleware(
415-
asgi3_app_with_error, transaction_style=transaction_style
416-
)
414+
app = SentryAsgiMiddleware(asgi3_app, transaction_style=transaction_style)
417415

418416
scope = {
419-
"endpoint": asgi3_app_with_error,
417+
"endpoint": asgi3_app,
420418
"route": url,
421419
"client": ("127.0.0.1", 60457),
422420
}
423421

424-
with pytest.raises(ZeroDivisionError):
425-
async with TestClient(app, scope=scope) as client:
426-
events = capture_events()
427-
await client.get(url)
422+
async with TestClient(app, scope=scope) as client:
423+
events = capture_events()
424+
await client.get(url)
428425

429-
(_, transaction_event) = events
426+
(transaction_event,) = events
430427

431428
assert transaction_event["transaction"] == expected_transaction
432429
assert transaction_event["transaction_info"] == {"source": expected_source}

tests/integrations/fastapi/test_fastapi.py

+104
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@
2222
def fastapi_app_factory():
2323
app = FastAPI()
2424

25+
@app.get("/error")
26+
async def _error():
27+
capture_message("Hi")
28+
1 / 0
29+
return {"message": "Hi"}
30+
2531
@app.get("/message")
2632
async def _message():
2733
capture_message("Hi")
@@ -218,3 +224,101 @@ async def _error(request: Request):
218224
event = events[0]
219225
assert event["request"]["data"] == {"password": "[Filtered]"}
220226
assert event["request"]["headers"]["authorization"] == "[Filtered]"
227+
228+
229+
@pytest.mark.asyncio
230+
def test_response_status_code_ok_in_transaction_context(sentry_init, capture_envelopes):
231+
"""
232+
Tests that the response status code is added to the transaction "response" context.
233+
"""
234+
sentry_init(
235+
integrations=[StarletteIntegration(), FastApiIntegration()],
236+
traces_sample_rate=1.0,
237+
release="demo-release",
238+
)
239+
240+
envelopes = capture_envelopes()
241+
242+
app = fastapi_app_factory()
243+
244+
client = TestClient(app)
245+
client.get("/message")
246+
247+
(_, transaction_envelope) = envelopes
248+
transaction = transaction_envelope.get_transaction_event()
249+
250+
assert transaction["type"] == "transaction"
251+
assert len(transaction["contexts"]) > 0
252+
assert (
253+
"response" in transaction["contexts"].keys()
254+
), "Response context not found in transaction"
255+
assert transaction["contexts"]["response"]["status_code"] == 200
256+
257+
258+
@pytest.mark.asyncio
259+
def test_response_status_code_error_in_transaction_context(
260+
sentry_init,
261+
capture_envelopes,
262+
):
263+
"""
264+
Tests that the response status code is added to the transaction "response" context.
265+
"""
266+
sentry_init(
267+
integrations=[StarletteIntegration(), FastApiIntegration()],
268+
traces_sample_rate=1.0,
269+
release="demo-release",
270+
)
271+
272+
envelopes = capture_envelopes()
273+
274+
app = fastapi_app_factory()
275+
276+
client = TestClient(app)
277+
with pytest.raises(ZeroDivisionError):
278+
client.get("/error")
279+
280+
(
281+
_,
282+
_,
283+
transaction_envelope,
284+
) = envelopes
285+
transaction = transaction_envelope.get_transaction_event()
286+
287+
assert transaction["type"] == "transaction"
288+
assert len(transaction["contexts"]) > 0
289+
assert (
290+
"response" in transaction["contexts"].keys()
291+
), "Response context not found in transaction"
292+
assert transaction["contexts"]["response"]["status_code"] == 500
293+
294+
295+
@pytest.mark.asyncio
296+
def test_response_status_code_not_found_in_transaction_context(
297+
sentry_init,
298+
capture_envelopes,
299+
):
300+
"""
301+
Tests that the response status code is added to the transaction "response" context.
302+
"""
303+
sentry_init(
304+
integrations=[StarletteIntegration(), FastApiIntegration()],
305+
traces_sample_rate=1.0,
306+
release="demo-release",
307+
)
308+
309+
envelopes = capture_envelopes()
310+
311+
app = fastapi_app_factory()
312+
313+
client = TestClient(app)
314+
client.get("/non-existing-route-123")
315+
316+
(transaction_envelope,) = envelopes
317+
transaction = transaction_envelope.get_transaction_event()
318+
319+
assert transaction["type"] == "transaction"
320+
assert len(transaction["contexts"]) > 0
321+
assert (
322+
"response" in transaction["contexts"].keys()
323+
), "Response context not found in transaction"
324+
assert transaction["contexts"]["response"]["status_code"] == 404

tests/integrations/flask/test_flask.py

+58
Original file line numberDiff line numberDiff line change
@@ -912,3 +912,61 @@ def error():
912912
assert (
913913
event["contexts"]["replay"]["replay_id"] == "12312012123120121231201212312012"
914914
)
915+
916+
917+
def test_response_status_code_ok_in_transaction_context(
918+
sentry_init, capture_envelopes, app
919+
):
920+
"""
921+
Tests that the response status code is added to the transaction context.
922+
This also works for when there is an Exception during the request, but somehow the test flask app doesn't seem to trigger that.
923+
"""
924+
sentry_init(
925+
integrations=[flask_sentry.FlaskIntegration()],
926+
traces_sample_rate=1.0,
927+
release="demo-release",
928+
)
929+
930+
envelopes = capture_envelopes()
931+
932+
client = app.test_client()
933+
client.get("/message")
934+
935+
Hub.current.client.flush()
936+
937+
(_, transaction_envelope, _) = envelopes
938+
transaction = transaction_envelope.get_transaction_event()
939+
940+
assert transaction["type"] == "transaction"
941+
assert len(transaction["contexts"]) > 0
942+
assert (
943+
"response" in transaction["contexts"].keys()
944+
), "Response context not found in transaction"
945+
assert transaction["contexts"]["response"]["status_code"] == 200
946+
947+
948+
def test_response_status_code_not_found_in_transaction_context(
949+
sentry_init, capture_envelopes, app
950+
):
951+
sentry_init(
952+
integrations=[flask_sentry.FlaskIntegration()],
953+
traces_sample_rate=1.0,
954+
release="demo-release",
955+
)
956+
957+
envelopes = capture_envelopes()
958+
959+
client = app.test_client()
960+
client.get("/not-existing-route")
961+
962+
Hub.current.client.flush()
963+
964+
(transaction_envelope, _) = envelopes
965+
transaction = transaction_envelope.get_transaction_event()
966+
967+
assert transaction["type"] == "transaction"
968+
assert len(transaction["contexts"]) > 0
969+
assert (
970+
"response" in transaction["contexts"].keys()
971+
), "Response context not found in transaction"
972+
assert transaction["contexts"]["response"]["status_code"] == 404

tests/integrations/starlette/test_starlette.py

+3-9
Original file line numberDiff line numberDiff line change
@@ -700,9 +700,7 @@ def test_middleware_callback_spans(sentry_init, capture_events):
700700
},
701701
{
702702
"op": "middleware.starlette.send",
703-
"description": "_ASGIAdapter.send.<locals>.send"
704-
if STARLETTE_VERSION < (0, 21)
705-
else "_TestClientTransport.handle_request.<locals>.send",
703+
"description": "SentryAsgiMiddleware._run_app.<locals>._sentry_wrapped_send",
706704
"tags": {"starlette.middleware_name": "ServerErrorMiddleware"},
707705
},
708706
{
@@ -717,9 +715,7 @@ def test_middleware_callback_spans(sentry_init, capture_events):
717715
},
718716
{
719717
"op": "middleware.starlette.send",
720-
"description": "_ASGIAdapter.send.<locals>.send"
721-
if STARLETTE_VERSION < (0, 21)
722-
else "_TestClientTransport.handle_request.<locals>.send",
718+
"description": "SentryAsgiMiddleware._run_app.<locals>._sentry_wrapped_send",
723719
"tags": {"starlette.middleware_name": "ServerErrorMiddleware"},
724720
},
725721
]
@@ -793,9 +789,7 @@ def test_middleware_partial_receive_send(sentry_init, capture_events):
793789
},
794790
{
795791
"op": "middleware.starlette.send",
796-
"description": "_ASGIAdapter.send.<locals>.send"
797-
if STARLETTE_VERSION < (0, 21)
798-
else "_TestClientTransport.handle_request.<locals>.send",
792+
"description": "SentryAsgiMiddleware._run_app.<locals>._sentry_wrapped_send",
799793
"tags": {"starlette.middleware_name": "ServerErrorMiddleware"},
800794
},
801795
{

tests/integrations/starlite/test_starlite.py

+3-4
Original file line numberDiff line numberDiff line change
@@ -221,12 +221,12 @@ def test_middleware_callback_spans(sentry_init, capture_events):
221221
},
222222
{
223223
"op": "middleware.starlite.send",
224-
"description": "TestClientTransport.create_send.<locals>.send",
224+
"description": "SentryAsgiMiddleware._run_app.<locals>._sentry_wrapped_send",
225225
"tags": {"starlite.middleware_name": "SampleMiddleware"},
226226
},
227227
{
228228
"op": "middleware.starlite.send",
229-
"description": "TestClientTransport.create_send.<locals>.send",
229+
"description": "SentryAsgiMiddleware._run_app.<locals>._sentry_wrapped_send",
230230
"tags": {"starlite.middleware_name": "SampleMiddleware"},
231231
},
232232
]
@@ -286,12 +286,11 @@ def test_middleware_partial_receive_send(sentry_init, capture_events):
286286
},
287287
{
288288
"op": "middleware.starlite.send",
289-
"description": "TestClientTransport.create_send.<locals>.send",
289+
"description": "SentryAsgiMiddleware._run_app.<locals>._sentry_wrapped_send",
290290
"tags": {"starlite.middleware_name": "SamplePartialReceiveSendMiddleware"},
291291
},
292292
]
293293

294-
print(transaction_event["spans"])
295294
idx = 0
296295
for span in transaction_event["spans"]:
297296
assert span["op"] == expected[idx]["op"]

0 commit comments

Comments
 (0)