Skip to content

Commit

Permalink
feat(bottle): Add failed_request_status_codes (#3618)
Browse files Browse the repository at this point in the history
  • Loading branch information
szokeasaurusrex authored Oct 8, 2024
1 parent d34c99a commit d0eca65
Show file tree
Hide file tree
Showing 2 changed files with 118 additions and 13 deletions.
50 changes: 38 additions & 12 deletions sentry_sdk/integrations/bottle.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,19 @@
parse_version,
transaction_from_function,
)
from sentry_sdk.integrations import Integration, DidNotEnable
from sentry_sdk.integrations import (
Integration,
DidNotEnable,
_DEFAULT_FAILED_REQUEST_STATUS_CODES,
)
from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware
from sentry_sdk.integrations._wsgi_common import RequestExtractor

from typing import TYPE_CHECKING

if TYPE_CHECKING:
from collections.abc import Set

from sentry_sdk.integrations.wsgi import _ScopedResponse
from typing import Any
from typing import Dict
Expand All @@ -28,6 +34,7 @@
try:
from bottle import (
Bottle,
HTTPResponse,
Route,
request as bottle_request,
__version__ as BOTTLE_VERSION,
Expand All @@ -45,15 +52,21 @@ class BottleIntegration(Integration):

transaction_style = ""

def __init__(self, transaction_style="endpoint"):
# type: (str) -> None
def __init__(
self,
transaction_style="endpoint", # type: str
*,
failed_request_status_codes=_DEFAULT_FAILED_REQUEST_STATUS_CODES, # type: Set[int]
):
# type: (...) -> None

if transaction_style not in TRANSACTION_STYLE_VALUES:
raise ValueError(
"Invalid value for transaction_style: %s (must be in %s)"
% (transaction_style, TRANSACTION_STYLE_VALUES)
)
self.transaction_style = transaction_style
self.failed_request_status_codes = failed_request_status_codes

@staticmethod
def setup_once():
Expand Down Expand Up @@ -102,26 +115,29 @@ def _patched_handle(self, environ):

old_make_callback = Route._make_callback

@ensure_integration_enabled(BottleIntegration, old_make_callback)
@functools.wraps(old_make_callback)
def patched_make_callback(self, *args, **kwargs):
# type: (Route, *object, **object) -> Any
client = sentry_sdk.get_client()
prepared_callback = old_make_callback(self, *args, **kwargs)

integration = sentry_sdk.get_client().get_integration(BottleIntegration)
if integration is None:
return prepared_callback

def wrapped_callback(*args, **kwargs):
# type: (*object, **object) -> Any

try:
res = prepared_callback(*args, **kwargs)
except Exception as exception:
event, hint = event_from_exception(
exception,
client_options=client.options,
mechanism={"type": "bottle", "handled": False},
)
sentry_sdk.capture_event(event, hint=hint)
_capture_exception(exception, handled=False)
raise exception

if (
isinstance(res, HTTPResponse)
and res.status_code in integration.failed_request_status_codes
):
_capture_exception(res, handled=True)

return res

return wrapped_callback
Expand Down Expand Up @@ -191,3 +207,13 @@ def event_processor(event, hint):
return event

return event_processor


def _capture_exception(exception, handled):
# type: (BaseException, bool) -> None
event, hint = event_from_exception(
exception,
client_options=sentry_sdk.get_client().options,
mechanism={"type": "bottle", "handled": handled},
)
sentry_sdk.capture_event(event, hint=hint)
81 changes: 80 additions & 1 deletion tests/integrations/bottle/test_bottle.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
import logging

from io import BytesIO
from bottle import Bottle, debug as set_debug, abort, redirect
from bottle import Bottle, debug as set_debug, abort, redirect, HTTPResponse
from sentry_sdk import capture_message
from sentry_sdk.integrations.bottle import BottleIntegration
from sentry_sdk.serializer import MAX_DATABAG_BREADTH

from sentry_sdk.integrations.logging import LoggingIntegration
from werkzeug.test import Client
from werkzeug.wrappers import Response

import sentry_sdk.integrations.bottle as bottle_sentry

Expand Down Expand Up @@ -445,3 +447,80 @@ def test_span_origin(
(_, event) = events

assert event["contexts"]["trace"]["origin"] == "auto.http.bottle"


@pytest.mark.parametrize("raise_error", [True, False])
@pytest.mark.parametrize(
("integration_kwargs", "status_code", "should_capture"),
(
({}, None, False),
({}, 400, False),
({}, 451, False), # Highest 4xx status code
({}, 500, True),
({}, 511, True), # Highest 5xx status code
({"failed_request_status_codes": set()}, 500, False),
({"failed_request_status_codes": set()}, 511, False),
({"failed_request_status_codes": {404, *range(500, 600)}}, 404, True),
({"failed_request_status_codes": {404, *range(500, 600)}}, 500, True),
({"failed_request_status_codes": {404, *range(500, 600)}}, 400, False),
),
)
def test_failed_request_status_codes(
sentry_init,
capture_events,
integration_kwargs,
status_code,
should_capture,
raise_error,
):
sentry_init(integrations=[BottleIntegration(**integration_kwargs)])
events = capture_events()

app = Bottle()

@app.route("/")
def handle():
if status_code is not None:
response = HTTPResponse(status=status_code)
if raise_error:
raise response
else:
return response
return "OK"

client = Client(app, Response)
response = client.get("/")

expected_status = 200 if status_code is None else status_code
assert response.status_code == expected_status

if should_capture:
(event,) = events
assert event["exception"]["values"][0]["type"] == "HTTPResponse"
else:
assert not events


def test_failed_request_status_codes_non_http_exception(sentry_init, capture_events):
"""
If an exception, which is not an instance of HTTPResponse, is raised, it should be captured, even if
failed_request_status_codes is empty.
"""
sentry_init(integrations=[BottleIntegration(failed_request_status_codes=set())])
events = capture_events()

app = Bottle()

@app.route("/")
def handle():
1 / 0

client = Client(app, Response)

try:
client.get("/")
except ZeroDivisionError:
pass

(event,) = events
assert event["exception"]["values"][0]["type"] == "ZeroDivisionError"

0 comments on commit d0eca65

Please sign in to comment.