Skip to content

Commit 04a3f02

Browse files
Merge branch 'master' into szokeasaurusrex/fork-flaky-continuous-profiler-test
2 parents ed4a62c + 1653c11 commit 04a3f02

File tree

8 files changed

+186
-24
lines changed

8 files changed

+186
-24
lines changed

CHANGELOG.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,67 @@
11
# Changelog
22

3+
## 2.16.0
4+
5+
### Integrations
6+
7+
- Bottle: Add `failed_request_status_codes` (#3618) by @szokeasaurusrex
8+
9+
You can now define a set of integers that will determine which status codes
10+
should be reported to Sentry.
11+
12+
```python
13+
sentry_sdk.init(
14+
integrations=[
15+
BottleIntegration(
16+
failed_request_status_codes={403, *range(500, 600)},
17+
)
18+
]
19+
)
20+
```
21+
22+
Examples of valid `failed_request_status_codes`:
23+
24+
- `{500}` will only send events on HTTP 500.
25+
- `{400, *range(500, 600)}` will send events on HTTP 400 as well as the 5xx range.
26+
- `{500, 503}` will send events on HTTP 500 and 503.
27+
- `set()` (the empty set) will not send events for any HTTP status code.
28+
29+
The default is `{*range(500, 600)}`, meaning that all 5xx status codes are reported to Sentry.
30+
31+
- Bottle: Delete never-reached code (#3605) by @szokeasaurusrex
32+
- Redis: Remove flaky test (#3626) by @sentrivana
33+
- Django: Improve getting `psycopg3` connection info (#3580) by @nijel
34+
- Django: Add `SpotlightMiddleware` when Spotlight is enabled (#3600) by @BYK
35+
- Django: Open relevant error when `SpotlightMiddleware` is on (#3614) by @BYK
36+
- Django: Support `http_methods_to_capture` in ASGI Django (#3607) by @sentrivana
37+
38+
ASGI Django now also supports the `http_methods_to_capture` integration option. This is a configurable tuple of HTTP method verbs that should create a transaction in Sentry. The default is `("CONNECT", "DELETE", "GET", "PATCH", "POST", "PUT", "TRACE",)`. `OPTIONS` and `HEAD` are not included by default.
39+
40+
Here's how to use it:
41+
42+
```python
43+
sentry_sdk.init(
44+
integrations=[
45+
DjangoIntegration(
46+
http_methods_to_capture=("GET", "POST"),
47+
),
48+
],
49+
)
50+
```
51+
52+
### Miscellaneous
53+
54+
- Add 3.13 to setup.py (#3574) by @sentrivana
55+
- Add 3.13 to basepython (#3589) by @sentrivana
56+
- Fix type of `sample_rate` in DSC (and add explanatory tests) (#3603) by @antonpirker
57+
- Add `httpcore` based `HTTP2Transport` (#3588) by @BYK
58+
- Add opportunistic Brotli compression (#3612) by @BYK
59+
- Add `__notes__` support (#3620) by @szokeasaurusrex
60+
- Remove useless makefile targets (#3604) by @antonpirker
61+
- Simplify tox version spec (#3609) by @sentrivana
62+
- Consolidate contributing docs (#3606) by @antonpirker
63+
- Bump `codecov/codecov-action` from `4.5.0` to `4.6.0` (#3617) by @dependabot
64+
365
## 2.15.0
466

567
### Integrations
@@ -18,6 +80,7 @@
1880
),
1981
],
2082
)
83+
```
2184

2285
- Django: Allow ASGI to use `drf_request` in `DjangoRequestExtractor` (#3572) by @PakawiNz
2386
- Django: Don't let `RawPostDataException` bubble up (#3553) by @sentrivana

docs/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
copyright = "2019-{}, Sentry Team and Contributors".format(datetime.now().year)
2929
author = "Sentry Team and Contributors"
3030

31-
release = "2.15.0"
31+
release = "2.16.0"
3232
version = ".".join(release.split(".")[:2]) # The short X.Y version.
3333

3434

sentry_sdk/consts.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -574,4 +574,4 @@ def _get_default_options():
574574
del _get_default_options
575575

576576

577-
VERSION = "2.15.0"
577+
VERSION = "2.16.0"

sentry_sdk/integrations/bottle.py

Lines changed: 38 additions & 12 deletions
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)

setup.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ def get_file_text(file_name):
2121

2222
setup(
2323
name="sentry-sdk",
24-
version="2.15.0",
24+
version="2.16.0",
2525
author="Sentry Team and Contributors",
2626
author_email="[email protected]",
2727
url="https://github.com/getsentry/sentry-python",
@@ -99,6 +99,7 @@ def get_file_text(file_name):
9999
"Programming Language :: Python :: 3.10",
100100
"Programming Language :: Python :: 3.11",
101101
"Programming Language :: Python :: 3.12",
102+
"Programming Language :: Python :: 3.13",
102103
"Topic :: Software Development :: Libraries :: Python Modules",
103104
],
104105
options={"bdist_wheel": {"universal": "1"}},

tests/integrations/bottle/test_bottle.py

Lines changed: 80 additions & 1 deletion
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"

tests/test_basics.py

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@
3434
setup_integrations,
3535
)
3636
from sentry_sdk.integrations.logging import LoggingIntegration
37-
from sentry_sdk.integrations.redis import RedisIntegration
3837
from sentry_sdk.integrations.stdlib import StdlibIntegration
3938
from sentry_sdk.scope import add_global_event_processor
4039
from sentry_sdk.utils import get_sdk_name, reraise
@@ -887,13 +886,6 @@ def test_functions_to_trace_with_class(sentry_init, capture_events):
887886
assert event["spans"][1]["description"] == "tests.test_basics.WorldGreeter.greet"
888887

889888

890-
def test_redis_disabled_when_not_installed(sentry_init):
891-
with ModuleImportErrorSimulator(["redis"], ImportError):
892-
sentry_init()
893-
894-
assert sentry_sdk.get_client().get_integration(RedisIntegration) is None
895-
896-
897889
def test_multiple_setup_integrations_calls():
898890
first_call_return = setup_integrations([NoOpIntegration()], with_defaults=False)
899891
assert first_call_return == {NoOpIntegration.identifier: NoOpIntegration()}

tox.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -766,6 +766,7 @@ basepython =
766766
py3.10: python3.10
767767
py3.11: python3.11
768768
py3.12: python3.12
769+
py3.13: python3.13
769770

770771
# Python version is pinned here because flake8 actually behaves differently
771772
# depending on which version is used. You can patch this out to point to

0 commit comments

Comments
 (0)