Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(bottle): Add failed_request_status_codes #3618

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"
Loading