Skip to content
Closed
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
18 changes: 15 additions & 3 deletions scripts/populate_tox/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@
"celery": {
"package": "celery",
"deps": {
"*": ["newrelic<10.17.0", "redis", "pytest-forked"],
"*": ["newrelic<10.17.0", "redis", "pytest-forked", "pytest<9.2.0"],
"py3.7": ["importlib-metadata<5.0"],
},
},
Expand All @@ -103,6 +103,7 @@
"djangorestframework",
"pytest-django",
"pytest-forked",
"pytest<9.2.0",
"Werkzeug",
"channels[daphne]",
"executing",
Expand Down Expand Up @@ -193,15 +194,20 @@
"types-protobuf",
"pytest-asyncio",
"pytest-forked",
"pytest<9.2.0",
],
},
},
"httpx0": {
"package": "httpx",
"integration_name": "httpx",
"deps": {
"*": ["anyio>=3,<5", "pytest-asyncio"],
# Old pytest-asyncio versions are not compatible with pytest>=9.2.0
"*": ["anyio>=3,<5", "pytest-asyncio", "pytest<9.2.0"],
"<0.24": ["anyio<4"],
# pytest-cov>=7 requires pytest>=7, so installing the latest pytest-cov
# would override the pytest<7 pin these versions resolve to
"<0.21": ["pytest-cov<7"],
">=0.16,<0.17": ["pytest-httpx==0.10.0"],
">=0.17,<0.19": ["pytest-httpx==0.12.0"],
">=0.19,<0.21": ["pytest-httpx==0.14.0"],
Expand Down Expand Up @@ -401,7 +407,13 @@
"quart": {
"package": "quart",
"deps": {
"*": ["quart-auth", "pytest-asyncio", "pytest-forked", "Werkzeug"],
"*": [
"quart-auth",
"pytest-asyncio",
"pytest-forked",
"pytest<9.2.0",
"Werkzeug",
],
">=0.19,<0.23": ["quart-flask-patch"],
"<0.19": [
"blinker<1.6",
Expand Down
1,043 changes: 258 additions & 785 deletions scripts/populate_tox/package_dependencies.jsonl

Large diffs are not rendered by default.

219 changes: 0 additions & 219 deletions scripts/populate_tox/releases.jsonl

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions scripts/populate_tox/tox.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ deps =
py3.7-common: pytest<7.0.0
# Handled by importlib.metadata on Python 3.8+
py3.7-common: setuptools<82
{py3.8,py3.9,py3.10,py3.11,py3.12,py3.13,py3.14,py3.14t,py3.15,py3.15t}-common: pytest
# pytest-forked is not compatible with pytest>=9.2.0
{py3.8,py3.9,py3.10,py3.11,py3.12,py3.13,py3.14,py3.14t,py3.15,py3.15t}-common: pytest<9.2.0
# coverage 7.11.1-7.11.3 makes some of our tests flake
{py3.14,py3.14t,py3.15,py3.15t}-common: coverage==7.11.0

Expand All @@ -94,7 +95,8 @@ deps =
# and https://github.com/pytest-dev/pytest-forked/issues/67
# for justification of the upper bound on pytest
py3.7-gevent: pytest<7.0.0
{py3.8,py3.9,py3.10,py3.11,py3.12}-gevent: pytest
# pytest-forked is not compatible with pytest>=9.2.0
{py3.8,py3.9,py3.10,py3.11,py3.12}-gevent: pytest<9.2.0
gevent: brotli
gevent: executing
gevent: httpcore[http2]
Expand Down
17 changes: 11 additions & 6 deletions sentry_sdk/integrations/falcon.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
)

if TYPE_CHECKING:
from typing import Any, Dict, Optional
from typing import Any, Dict, Optional, Union

from sentry_sdk._types import Event, EventProcessor

Expand Down Expand Up @@ -194,18 +194,23 @@ def sentry_patched_prepare_middleware(


def _exception_leads_to_http_5xx(ex: Exception, response: "falcon.Response") -> bool:
is_server_error = isinstance(ex, falcon.HTTPError) and (ex.status or "").startswith(
"5"
is_server_error = isinstance(ex, falcon.HTTPError) and _is_http_5xx_status(
ex.status or ""
)
is_unhandled_error = not isinstance(
ex, (falcon.HTTPError, falcon.http_status.HTTPStatus)
)

return (is_server_error or is_unhandled_error) and _has_http_5xx_status(response)
return (is_server_error or is_unhandled_error) and _is_http_5xx_status(
response.status
)


def _has_http_5xx_status(response: "falcon.Response") -> bool:
return response.status.startswith("5")
def _is_http_5xx_status(status: "Union[int, str]") -> bool:
if isinstance(status, int):
return 500 <= status < 600
if isinstance(status, str):
return status.startswith("5")


def _make_request_event_processor(
Expand Down
2 changes: 1 addition & 1 deletion sentry_sdk/tracing_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -842,7 +842,7 @@ def serialize(self, include_third_party: bool = False) -> str:
item = Baggage.SENTRY_PREFIX + quote(key) + "=" + quote(str(val))
items.append(item)

if include_third_party:
if include_third_party and self.third_party_items:
items.append(self.third_party_items)

return ",".join(items)
Expand Down
131 changes: 124 additions & 7 deletions tests/integrations/falcon/test_falcon.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import logging
from http import HTTPStatus

import falcon
import falcon.testing
Expand All @@ -8,6 +9,7 @@
from sentry_sdk.consts import SPANDATA
from sentry_sdk.integrations.falcon import FalconIntegration
from sentry_sdk.integrations.logging import LoggingIntegration
from sentry_sdk.utils import parse_version

try:
import falcon.asgi
Expand All @@ -17,6 +19,9 @@
import falcon.inspect # We only need this module for the ASGI test


FALCON_VERSION = parse_version(falcon.__version__)


@pytest.fixture
def make_app(sentry_init):
def inner():
Expand Down Expand Up @@ -178,18 +183,41 @@ def on_get(self, req, resp):
assert " by zero" in event["exception"]["values"][0]["value"]


@pytest.mark.parametrize(
"status",
[
falcon.HTTP_502,
pytest.param(
502,
marks=pytest.mark.skipif(
(FALCON_VERSION or ()) < (4,),
reason="Integer HTTPError statuses require Falcon 4+",
),
),
pytest.param(
HTTPStatus.BAD_GATEWAY,
id="HTTPStatus.BAD_GATEWAY",
marks=pytest.mark.skipif(
(FALCON_VERSION or ()) < (4,),
reason="Integer HTTPError statuses require Falcon 4+",
),
),
],
)
def test_raised_5xx_errors(
sentry_init,
capture_exceptions,
capture_items,
status,
):
"""String, integer, and http.HTTPStatus 5xx HTTPError statuses should be captured."""
sentry_init(
integrations=[FalconIntegration()],
)

class Resource:
def on_get(self, req, resp):
raise falcon.HTTPError(falcon.HTTP_502)
raise falcon.HTTPError(status)

app = falcon.API()
app.add_route("/", Resource())
Expand All @@ -210,18 +238,41 @@ def on_get(self, req, resp):
assert event["exception"]["values"][0]["type"] == "HTTPError"


@pytest.mark.parametrize(
"status",
[
falcon.HTTP_400,
pytest.param(
400,
marks=pytest.mark.skipif(
(FALCON_VERSION or ()) < (4,),
reason="Integer HTTPError statuses require Falcon 4+",
),
),
pytest.param(
HTTPStatus.BAD_REQUEST,
id="HTTPStatus.BAD_REQUEST",
marks=pytest.mark.skipif(
(FALCON_VERSION or ()) < (4,),
reason="Integer HTTPError statuses require Falcon 4+",
),
),
],
)
def test_raised_4xx_errors(
sentry_init,
capture_exceptions,
capture_items,
status,
):
"""String, integer, and http.HTTPStatus 4xx HTTPError statuses should not be captured."""
sentry_init(
integrations=[FalconIntegration()],
)

class Resource:
def on_get(self, req, resp):
raise falcon.HTTPError(falcon.HTTP_400)
raise falcon.HTTPError(status)

app = falcon.API()
app.add_route("/", Resource())
Expand All @@ -236,22 +287,41 @@ def on_get(self, req, resp):
assert len(events) == 0


@pytest.mark.parametrize(
"status",
[
falcon.HTTP_508,
pytest.param(
508,
marks=pytest.mark.skipif(
(FALCON_VERSION or ()) < (4,),
reason="Integer HTTPStatus statuses require Falcon 4+",
),
),
pytest.param(
HTTPStatus.LOOP_DETECTED,
id="HTTPStatus.LOOP_DETECTED",
marks=pytest.mark.skipif(
(FALCON_VERSION or ()) < (4,),
reason="Integer HTTPStatus statuses require Falcon 4+",
),
),
],
)
def test_http_status(
sentry_init,
capture_exceptions,
capture_items,
status,
):
"""
This just demonstrates, that if Falcon raises a HTTPStatus with code 500
(instead of a HTTPError with code 500) Sentry will not capture it.
"""
"""String, integer, and http.HTTPStatus 5xx falcon.HTTPStatus exceptions are not captured."""
sentry_init(
integrations=[FalconIntegration()],
)

class Resource:
def on_get(self, req, resp):
raise falcon.http_status.HTTPStatus(falcon.HTTP_508)
raise falcon.http_status.HTTPStatus(status)

app = falcon.API()
app.add_route("/", Resource())
Expand All @@ -267,6 +337,53 @@ def on_get(self, req, resp):
assert len(events) == 0


@pytest.mark.parametrize(
"status",
[
400,
pytest.param(HTTPStatus.BAD_REQUEST, id="HTTPStatus.BAD_REQUEST"),
499,
500,
pytest.param(
HTTPStatus.INTERNAL_SERVER_ERROR, id="HTTPStatus.INTERNAL_SERVER_ERROR"
),
599,
600,
],
)
def test_integer_response_status(sentry_init, capture_items, status):
"""A handler's integer or HTTPStatus response status should capture only 5xx errors."""
sentry_init(
integrations=[FalconIntegration()],
)

class Resource:
def on_get(self, req, resp):
raise ValueError("handled error")

def error_handler(req, resp, ex, params):
resp.status = status

app = falcon.API()
app.add_route("/", Resource())
app.add_error_handler(ValueError, error_handler)
items = capture_items("event")

response = falcon.testing.TestClient(app).simulate_get("/")

# this exists in falcon >= 3
assert response.status_code == status
if 500 <= status < 600:
(event,) = (item.payload for item in items)
exception = event["exception"]["values"][-1]
assert exception["type"] == "ValueError"
assert exception["value"] == "handled error"
assert exception["mechanism"]["type"] == "falcon"
assert exception["mechanism"]["handled"] is False
else:
assert not items


@pytest.mark.parametrize("max_value_length", [1024, None])
def test_falcon_large_json_request(
sentry_init,
Expand Down
4 changes: 2 additions & 2 deletions tests/test_tracing_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,11 +133,11 @@ def test_strip_sentry_baggage(header, expected):
(Baggage(sentry_items={}, mutable=False), '<Baggage "", mutable=False>'),
(
Baggage(sentry_items={"foo": "bar"}),
'<Baggage "sentry-foo=bar,", mutable=True>',
'<Baggage "sentry-foo=bar", mutable=True>',
),
(
Baggage(sentry_items={"foo": "bar"}, mutable=False),
'<Baggage "sentry-foo=bar,", mutable=False>',
'<Baggage "sentry-foo=bar", mutable=False>',
),
(
Baggage(sentry_items={"foo": "bar"}, third_party_items="asdf=1234,"),
Expand Down
21 changes: 21 additions & 0 deletions tests/tracing/test_baggage.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,27 @@ def test_mixed_baggage():
)


def test_sentry_only_baggage():
header = (
"sentry-trace_id=771a43a4192642f0b136d5159a501700,"
"sentry-public_key=49d0f7386ad645858ae85020e393bef3,"
"sentry-sample_rate=0.01337"
)
baggage = Baggage.from_incoming_header(header)

assert baggage.third_party_items == ""

serialized = baggage.serialize(include_third_party=True)

# with no third-party items, include_third_party should change nothing
assert serialized == baggage.serialize()
assert serialized == (
"sentry-trace_id=771a43a4192642f0b136d5159a501700,"
"sentry-public_key=49d0f7386ad645858ae85020e393bef3,"
"sentry-sample_rate=0.01337"
)


def test_malformed_baggage():
header = ","

Expand Down
Loading
Loading