Skip to content

Commit d0eca65

Browse files
feat(bottle): Add failed_request_status_codes (#3618)
1 parent d34c99a commit d0eca65

File tree

2 files changed

+118
-13
lines changed

2 files changed

+118
-13
lines changed

sentry_sdk/integrations/bottle.py

+38-12
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,19 @@
99
parse_version,
1010
transaction_from_function,
1111
)
12-
from sentry_sdk.integrations import Integration, DidNotEnable
12+
from sentry_sdk.integrations import (
13+
Integration,
14+
DidNotEnable,
15+
_DEFAULT_FAILED_REQUEST_STATUS_CODES,
16+
)
1317
from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware
1418
from sentry_sdk.integrations._wsgi_common import RequestExtractor
1519

1620
from typing import TYPE_CHECKING
1721

1822
if TYPE_CHECKING:
23+
from collections.abc import Set
24+
1925
from sentry_sdk.integrations.wsgi import _ScopedResponse
2026
from typing import Any
2127
from typing import Dict
@@ -28,6 +34,7 @@
2834
try:
2935
from bottle import (
3036
Bottle,
37+
HTTPResponse,
3138
Route,
3239
request as bottle_request,
3340
__version__ as BOTTLE_VERSION,
@@ -45,15 +52,21 @@ class BottleIntegration(Integration):
4552

4653
transaction_style = ""
4754

48-
def __init__(self, transaction_style="endpoint"):
49-
# type: (str) -> None
55+
def __init__(
56+
self,
57+
transaction_style="endpoint", # type: str
58+
*,
59+
failed_request_status_codes=_DEFAULT_FAILED_REQUEST_STATUS_CODES, # type: Set[int]
60+
):
61+
# type: (...) -> None
5062

5163
if transaction_style not in TRANSACTION_STYLE_VALUES:
5264
raise ValueError(
5365
"Invalid value for transaction_style: %s (must be in %s)"
5466
% (transaction_style, TRANSACTION_STYLE_VALUES)
5567
)
5668
self.transaction_style = transaction_style
69+
self.failed_request_status_codes = failed_request_status_codes
5770

5871
@staticmethod
5972
def setup_once():
@@ -102,26 +115,29 @@ def _patched_handle(self, environ):
102115

103116
old_make_callback = Route._make_callback
104117

105-
@ensure_integration_enabled(BottleIntegration, old_make_callback)
118+
@functools.wraps(old_make_callback)
106119
def patched_make_callback(self, *args, **kwargs):
107120
# type: (Route, *object, **object) -> Any
108-
client = sentry_sdk.get_client()
109121
prepared_callback = old_make_callback(self, *args, **kwargs)
110122

123+
integration = sentry_sdk.get_client().get_integration(BottleIntegration)
124+
if integration is None:
125+
return prepared_callback
126+
111127
def wrapped_callback(*args, **kwargs):
112128
# type: (*object, **object) -> Any
113-
114129
try:
115130
res = prepared_callback(*args, **kwargs)
116131
except Exception as exception:
117-
event, hint = event_from_exception(
118-
exception,
119-
client_options=client.options,
120-
mechanism={"type": "bottle", "handled": False},
121-
)
122-
sentry_sdk.capture_event(event, hint=hint)
132+
_capture_exception(exception, handled=False)
123133
raise exception
124134

135+
if (
136+
isinstance(res, HTTPResponse)
137+
and res.status_code in integration.failed_request_status_codes
138+
):
139+
_capture_exception(res, handled=True)
140+
125141
return res
126142

127143
return wrapped_callback
@@ -191,3 +207,13 @@ def event_processor(event, hint):
191207
return event
192208

193209
return event_processor
210+
211+
212+
def _capture_exception(exception, handled):
213+
# type: (BaseException, bool) -> None
214+
event, hint = event_from_exception(
215+
exception,
216+
client_options=sentry_sdk.get_client().options,
217+
mechanism={"type": "bottle", "handled": handled},
218+
)
219+
sentry_sdk.capture_event(event, hint=hint)

tests/integrations/bottle/test_bottle.py

+80-1
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@
33
import logging
44

55
from io import BytesIO
6-
from bottle import Bottle, debug as set_debug, abort, redirect
6+
from bottle import Bottle, debug as set_debug, abort, redirect, HTTPResponse
77
from sentry_sdk import capture_message
8+
from sentry_sdk.integrations.bottle import BottleIntegration
89
from sentry_sdk.serializer import MAX_DATABAG_BREADTH
910

1011
from sentry_sdk.integrations.logging import LoggingIntegration
1112
from werkzeug.test import Client
13+
from werkzeug.wrappers import Response
1214

1315
import sentry_sdk.integrations.bottle as bottle_sentry
1416

@@ -445,3 +447,80 @@ def test_span_origin(
445447
(_, event) = events
446448

447449
assert event["contexts"]["trace"]["origin"] == "auto.http.bottle"
450+
451+
452+
@pytest.mark.parametrize("raise_error", [True, False])
453+
@pytest.mark.parametrize(
454+
("integration_kwargs", "status_code", "should_capture"),
455+
(
456+
({}, None, False),
457+
({}, 400, False),
458+
({}, 451, False), # Highest 4xx status code
459+
({}, 500, True),
460+
({}, 511, True), # Highest 5xx status code
461+
({"failed_request_status_codes": set()}, 500, False),
462+
({"failed_request_status_codes": set()}, 511, False),
463+
({"failed_request_status_codes": {404, *range(500, 600)}}, 404, True),
464+
({"failed_request_status_codes": {404, *range(500, 600)}}, 500, True),
465+
({"failed_request_status_codes": {404, *range(500, 600)}}, 400, False),
466+
),
467+
)
468+
def test_failed_request_status_codes(
469+
sentry_init,
470+
capture_events,
471+
integration_kwargs,
472+
status_code,
473+
should_capture,
474+
raise_error,
475+
):
476+
sentry_init(integrations=[BottleIntegration(**integration_kwargs)])
477+
events = capture_events()
478+
479+
app = Bottle()
480+
481+
@app.route("/")
482+
def handle():
483+
if status_code is not None:
484+
response = HTTPResponse(status=status_code)
485+
if raise_error:
486+
raise response
487+
else:
488+
return response
489+
return "OK"
490+
491+
client = Client(app, Response)
492+
response = client.get("/")
493+
494+
expected_status = 200 if status_code is None else status_code
495+
assert response.status_code == expected_status
496+
497+
if should_capture:
498+
(event,) = events
499+
assert event["exception"]["values"][0]["type"] == "HTTPResponse"
500+
else:
501+
assert not events
502+
503+
504+
def test_failed_request_status_codes_non_http_exception(sentry_init, capture_events):
505+
"""
506+
If an exception, which is not an instance of HTTPResponse, is raised, it should be captured, even if
507+
failed_request_status_codes is empty.
508+
"""
509+
sentry_init(integrations=[BottleIntegration(failed_request_status_codes=set())])
510+
events = capture_events()
511+
512+
app = Bottle()
513+
514+
@app.route("/")
515+
def handle():
516+
1 / 0
517+
518+
client = Client(app, Response)
519+
520+
try:
521+
client.get("/")
522+
except ZeroDivisionError:
523+
pass
524+
525+
(event,) = events
526+
assert event["exception"]["values"][0]["type"] == "ZeroDivisionError"

0 commit comments

Comments
 (0)