diff --git a/.github/workflows/test-integrations-ai.yml b/.github/workflows/test-integrations-ai.yml
index 0c66baa03a..14cbce82d6 100644
--- a/.github/workflows/test-integrations-ai.yml
+++ b/.github/workflows/test-integrations-ai.yml
@@ -68,7 +68,7 @@ jobs:
coverage xml
- name: Upload coverage to Codecov
if: ${{ !cancelled() }}
- uses: codecov/codecov-action@v4.5.0
+ uses: codecov/codecov-action@v4.6.0
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: coverage.xml
@@ -130,7 +130,7 @@ jobs:
coverage xml
- name: Upload coverage to Codecov
if: ${{ !cancelled() }}
- uses: codecov/codecov-action@v4.5.0
+ uses: codecov/codecov-action@v4.6.0
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: coverage.xml
diff --git a/.github/workflows/test-integrations-aws-lambda.yml b/.github/workflows/test-integrations-aws-lambda.yml
index 922a39fc16..1a463d57eb 100644
--- a/.github/workflows/test-integrations-aws-lambda.yml
+++ b/.github/workflows/test-integrations-aws-lambda.yml
@@ -87,7 +87,7 @@ jobs:
coverage xml
- name: Upload coverage to Codecov
if: ${{ !cancelled() }}
- uses: codecov/codecov-action@v4.5.0
+ uses: codecov/codecov-action@v4.6.0
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: coverage.xml
diff --git a/.github/workflows/test-integrations-cloud-computing.yml b/.github/workflows/test-integrations-cloud-computing.yml
index 48fdfc9b7d..c36c769b76 100644
--- a/.github/workflows/test-integrations-cloud-computing.yml
+++ b/.github/workflows/test-integrations-cloud-computing.yml
@@ -64,7 +64,7 @@ jobs:
coverage xml
- name: Upload coverage to Codecov
if: ${{ !cancelled() }}
- uses: codecov/codecov-action@v4.5.0
+ uses: codecov/codecov-action@v4.6.0
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: coverage.xml
@@ -122,7 +122,7 @@ jobs:
coverage xml
- name: Upload coverage to Codecov
if: ${{ !cancelled() }}
- uses: codecov/codecov-action@v4.5.0
+ uses: codecov/codecov-action@v4.6.0
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: coverage.xml
diff --git a/.github/workflows/test-integrations-common.yml b/.github/workflows/test-integrations-common.yml
index ec554ebb07..048341b2e4 100644
--- a/.github/workflows/test-integrations-common.yml
+++ b/.github/workflows/test-integrations-common.yml
@@ -52,7 +52,7 @@ jobs:
coverage xml
- name: Upload coverage to Codecov
if: ${{ !cancelled() }}
- uses: codecov/codecov-action@v4.5.0
+ uses: codecov/codecov-action@v4.6.0
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: coverage.xml
diff --git a/.github/workflows/test-integrations-data-processing.yml b/.github/workflows/test-integrations-data-processing.yml
index d809d089f3..91483821f2 100644
--- a/.github/workflows/test-integrations-data-processing.yml
+++ b/.github/workflows/test-integrations-data-processing.yml
@@ -82,7 +82,7 @@ jobs:
coverage xml
- name: Upload coverage to Codecov
if: ${{ !cancelled() }}
- uses: codecov/codecov-action@v4.5.0
+ uses: codecov/codecov-action@v4.6.0
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: coverage.xml
@@ -158,7 +158,7 @@ jobs:
coverage xml
- name: Upload coverage to Codecov
if: ${{ !cancelled() }}
- uses: codecov/codecov-action@v4.5.0
+ uses: codecov/codecov-action@v4.6.0
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: coverage.xml
diff --git a/.github/workflows/test-integrations-databases.yml b/.github/workflows/test-integrations-databases.yml
index 5bca73c83e..08024e766f 100644
--- a/.github/workflows/test-integrations-databases.yml
+++ b/.github/workflows/test-integrations-databases.yml
@@ -91,7 +91,7 @@ jobs:
coverage xml
- name: Upload coverage to Codecov
if: ${{ !cancelled() }}
- uses: codecov/codecov-action@v4.5.0
+ uses: codecov/codecov-action@v4.6.0
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: coverage.xml
@@ -176,7 +176,7 @@ jobs:
coverage xml
- name: Upload coverage to Codecov
if: ${{ !cancelled() }}
- uses: codecov/codecov-action@v4.5.0
+ uses: codecov/codecov-action@v4.6.0
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: coverage.xml
diff --git a/.github/workflows/test-integrations-graphql.yml b/.github/workflows/test-integrations-graphql.yml
index 3b4aa9f1b8..9a6c89eca0 100644
--- a/.github/workflows/test-integrations-graphql.yml
+++ b/.github/workflows/test-integrations-graphql.yml
@@ -64,7 +64,7 @@ jobs:
coverage xml
- name: Upload coverage to Codecov
if: ${{ !cancelled() }}
- uses: codecov/codecov-action@v4.5.0
+ uses: codecov/codecov-action@v4.6.0
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: coverage.xml
@@ -122,7 +122,7 @@ jobs:
coverage xml
- name: Upload coverage to Codecov
if: ${{ !cancelled() }}
- uses: codecov/codecov-action@v4.5.0
+ uses: codecov/codecov-action@v4.6.0
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: coverage.xml
diff --git a/.github/workflows/test-integrations-miscellaneous.yml b/.github/workflows/test-integrations-miscellaneous.yml
index 143648fba3..14af8099f7 100644
--- a/.github/workflows/test-integrations-miscellaneous.yml
+++ b/.github/workflows/test-integrations-miscellaneous.yml
@@ -68,7 +68,7 @@ jobs:
coverage xml
- name: Upload coverage to Codecov
if: ${{ !cancelled() }}
- uses: codecov/codecov-action@v4.5.0
+ uses: codecov/codecov-action@v4.6.0
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: coverage.xml
@@ -130,7 +130,7 @@ jobs:
coverage xml
- name: Upload coverage to Codecov
if: ${{ !cancelled() }}
- uses: codecov/codecov-action@v4.5.0
+ uses: codecov/codecov-action@v4.6.0
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: coverage.xml
diff --git a/.github/workflows/test-integrations-networking.yml b/.github/workflows/test-integrations-networking.yml
index 124bbc065a..166d6e43eb 100644
--- a/.github/workflows/test-integrations-networking.yml
+++ b/.github/workflows/test-integrations-networking.yml
@@ -64,7 +64,7 @@ jobs:
coverage xml
- name: Upload coverage to Codecov
if: ${{ !cancelled() }}
- uses: codecov/codecov-action@v4.5.0
+ uses: codecov/codecov-action@v4.6.0
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: coverage.xml
@@ -122,7 +122,7 @@ jobs:
coverage xml
- name: Upload coverage to Codecov
if: ${{ !cancelled() }}
- uses: codecov/codecov-action@v4.5.0
+ uses: codecov/codecov-action@v4.6.0
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: coverage.xml
diff --git a/.github/workflows/test-integrations-web-frameworks-1.yml b/.github/workflows/test-integrations-web-frameworks-1.yml
index 651118ab24..4092bc9cff 100644
--- a/.github/workflows/test-integrations-web-frameworks-1.yml
+++ b/.github/workflows/test-integrations-web-frameworks-1.yml
@@ -82,7 +82,7 @@ jobs:
coverage xml
- name: Upload coverage to Codecov
if: ${{ !cancelled() }}
- uses: codecov/codecov-action@v4.5.0
+ uses: codecov/codecov-action@v4.6.0
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: coverage.xml
@@ -158,7 +158,7 @@ jobs:
coverage xml
- name: Upload coverage to Codecov
if: ${{ !cancelled() }}
- uses: codecov/codecov-action@v4.5.0
+ uses: codecov/codecov-action@v4.6.0
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: coverage.xml
diff --git a/.github/workflows/test-integrations-web-frameworks-2.yml b/.github/workflows/test-integrations-web-frameworks-2.yml
index b5c44d7abe..a222e523b4 100644
--- a/.github/workflows/test-integrations-web-frameworks-2.yml
+++ b/.github/workflows/test-integrations-web-frameworks-2.yml
@@ -88,7 +88,7 @@ jobs:
coverage xml
- name: Upload coverage to Codecov
if: ${{ !cancelled() }}
- uses: codecov/codecov-action@v4.5.0
+ uses: codecov/codecov-action@v4.6.0
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: coverage.xml
@@ -170,7 +170,7 @@ jobs:
coverage xml
- name: Upload coverage to Codecov
if: ${{ !cancelled() }}
- uses: codecov/codecov-action@v4.5.0
+ uses: codecov/codecov-action@v4.6.0
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: coverage.xml
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7db062694d..78aad7d292 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,67 @@
# Changelog
+## 2.16.0
+
+### Integrations
+
+- Bottle: Add `failed_request_status_codes` (#3618) by @szokeasaurusrex
+
+ You can now define a set of integers that will determine which status codes
+ should be reported to Sentry.
+
+ ```python
+ sentry_sdk.init(
+ integrations=[
+ BottleIntegration(
+ failed_request_status_codes={403, *range(500, 600)},
+ )
+ ]
+ )
+ ```
+
+ Examples of valid `failed_request_status_codes`:
+
+ - `{500}` will only send events on HTTP 500.
+ - `{400, *range(500, 600)}` will send events on HTTP 400 as well as the 5xx range.
+ - `{500, 503}` will send events on HTTP 500 and 503.
+ - `set()` (the empty set) will not send events for any HTTP status code.
+
+ The default is `{*range(500, 600)}`, meaning that all 5xx status codes are reported to Sentry.
+
+- Bottle: Delete never-reached code (#3605) by @szokeasaurusrex
+- Redis: Remove flaky test (#3626) by @sentrivana
+- Django: Improve getting `psycopg3` connection info (#3580) by @nijel
+- Django: Add `SpotlightMiddleware` when Spotlight is enabled (#3600) by @BYK
+- Django: Open relevant error when `SpotlightMiddleware` is on (#3614) by @BYK
+- Django: Support `http_methods_to_capture` in ASGI Django (#3607) by @sentrivana
+
+ 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.
+
+ Here's how to use it:
+
+ ```python
+ sentry_sdk.init(
+ integrations=[
+ DjangoIntegration(
+ http_methods_to_capture=("GET", "POST"),
+ ),
+ ],
+ )
+ ```
+
+### Miscellaneous
+
+- Add 3.13 to setup.py (#3574) by @sentrivana
+- Add 3.13 to basepython (#3589) by @sentrivana
+- Fix type of `sample_rate` in DSC (and add explanatory tests) (#3603) by @antonpirker
+- Add `httpcore` based `HTTP2Transport` (#3588) by @BYK
+- Add opportunistic Brotli compression (#3612) by @BYK
+- Add `__notes__` support (#3620) by @szokeasaurusrex
+- Remove useless makefile targets (#3604) by @antonpirker
+- Simplify tox version spec (#3609) by @sentrivana
+- Consolidate contributing docs (#3606) by @antonpirker
+- Bump `codecov/codecov-action` from `4.5.0` to `4.6.0` (#3617) by @dependabot
+
## 2.15.0
### Integrations
@@ -18,6 +80,7 @@
),
],
)
+ ```
- Django: Allow ASGI to use `drf_request` in `DjangoRequestExtractor` (#3572) by @PakawiNz
- Django: Don't let `RawPostDataException` bubble up (#3553) by @sentrivana
diff --git a/docs/conf.py b/docs/conf.py
index c1a219e278..390f576219 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -28,7 +28,7 @@
copyright = "2019-{}, Sentry Team and Contributors".format(datetime.now().year)
author = "Sentry Team and Contributors"
-release = "2.15.0"
+release = "2.16.0"
version = ".".join(release.split(".")[:2]) # The short X.Y version.
diff --git a/requirements-testing.txt b/requirements-testing.txt
index ce7bfa7962..851620c1d3 100644
--- a/requirements-testing.txt
+++ b/requirements-testing.txt
@@ -14,3 +14,4 @@ socksio
httpcore[http2]
setuptools
freezegun
+Brotli
diff --git a/scripts/split-tox-gh-actions/templates/test_group.jinja b/scripts/split-tox-gh-actions/templates/test_group.jinja
index f54ba1e413..38ab182b10 100644
--- a/scripts/split-tox-gh-actions/templates/test_group.jinja
+++ b/scripts/split-tox-gh-actions/templates/test_group.jinja
@@ -81,7 +81,7 @@
- name: Upload coverage to Codecov
if: {% raw %}${{ !cancelled() }}{% endraw %}
- uses: codecov/codecov-action@v4.5.0
+ uses: codecov/codecov-action@v4.6.0
with:
token: {% raw %}${{ secrets.CODECOV_TOKEN }}{% endraw %}
files: coverage.xml
diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py
index eed60828b0..d66eb3653e 100644
--- a/sentry_sdk/client.py
+++ b/sentry_sdk/client.py
@@ -704,18 +704,16 @@ def capture_event(
:returns: An event ID. May be `None` if there is no DSN set or of if the SDK decided to discard the event for other reasons. In such situations setting `debug=True` on `init()` may help.
"""
- if hint is None:
- hint = {}
- event_id = event.get("event_id")
hint = dict(hint or ()) # type: Hint
- if event_id is None:
- event["event_id"] = event_id = uuid.uuid4().hex
if not self._should_capture(event, hint, scope):
return None
profile = event.pop("profile", None)
+ event_id = event.get("event_id")
+ if event_id is None:
+ event["event_id"] = event_id = uuid.uuid4().hex
event_opt = self._prepare_event(event, hint, scope)
if event_opt is None:
return None
@@ -763,15 +761,16 @@ def capture_event(
for attachment in attachments or ():
envelope.add_item(attachment.to_envelope_item())
+ return_value = None
if self.spotlight:
self.spotlight.capture_envelope(envelope)
+ return_value = event_id
- if self.transport is None:
- return None
-
- self.transport.capture_envelope(envelope)
+ if self.transport is not None:
+ self.transport.capture_envelope(envelope)
+ return_value = event_id
- return event_id
+ return return_value
def capture_session(
self, session # type: Session
diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py
index 121c3f74da..2bd606f472 100644
--- a/sentry_sdk/consts.py
+++ b/sentry_sdk/consts.py
@@ -18,6 +18,11 @@ class EndpointType(Enum):
ENVELOPE = "envelope"
+class CompressionAlgo(Enum):
+ GZIP = "gzip"
+ BROTLI = "br"
+
+
if TYPE_CHECKING:
import sentry_sdk
@@ -56,6 +61,8 @@ class EndpointType(Enum):
"continuous_profiling_mode": Optional[ContinuousProfilerMode],
"otel_powered_performance": Optional[bool],
"transport_zlib_compression_level": Optional[int],
+ "transport_compression_level": Optional[int],
+ "transport_compression_algo": Optional[CompressionAlgo],
"transport_num_pools": Optional[int],
"transport_http2": Optional[bool],
},
@@ -553,4 +560,4 @@ def _get_default_options():
del _get_default_options
-VERSION = "2.15.0"
+VERSION = "2.16.0"
diff --git a/sentry_sdk/integrations/bottle.py b/sentry_sdk/integrations/bottle.py
index 6dae8d9188..a2d6b51033 100644
--- a/sentry_sdk/integrations/bottle.py
+++ b/sentry_sdk/integrations/bottle.py
@@ -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
@@ -28,6 +34,7 @@
try:
from bottle import (
Bottle,
+ HTTPResponse,
Route,
request as bottle_request,
__version__ as BOTTLE_VERSION,
@@ -45,8 +52,13 @@ 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(
@@ -54,6 +66,7 @@ def __init__(self, transaction_style="endpoint"):
% (transaction_style, TRANSACTION_STYLE_VALUES)
)
self.transaction_style = transaction_style
+ self.failed_request_status_codes = failed_request_status_codes
@staticmethod
def setup_once():
@@ -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
@@ -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)
diff --git a/sentry_sdk/integrations/django/__init__.py b/sentry_sdk/integrations/django/__init__.py
index c9f20dd49b..e68f0cacef 100644
--- a/sentry_sdk/integrations/django/__init__.py
+++ b/sentry_sdk/integrations/django/__init__.py
@@ -717,8 +717,18 @@ def _set_db_data(span, cursor_or_db):
connection_params = cursor_or_db.connection.get_dsn_parameters()
else:
try:
- # psycopg3
- connection_params = cursor_or_db.connection.info.get_parameters()
+ # psycopg3, only extract needed params as get_parameters
+ # can be slow because of the additional logic to filter out default
+ # values
+ connection_params = {
+ "dbname": cursor_or_db.connection.info.dbname,
+ "port": cursor_or_db.connection.info.port,
+ }
+ # PGhost returns host or base dir of UNIX socket as an absolute path
+ # starting with /, use it only when it contains host
+ pg_host = cursor_or_db.connection.info.host
+ if pg_host and not pg_host.startswith("/"):
+ connection_params["host"] = pg_host
except Exception:
connection_params = db.get_connection_params()
diff --git a/sentry_sdk/spotlight.py b/sentry_sdk/spotlight.py
index 3e8072b5d8..e21bf56545 100644
--- a/sentry_sdk/spotlight.py
+++ b/sentry_sdk/spotlight.py
@@ -79,14 +79,22 @@ def process_exception(self, _request, exception):
spotlight_url = spotlight_client.url.rsplit("/", 1)[0]
try:
- spotlight = (
- urllib.request.urlopen(spotlight_url).read().decode("utf-8")
- ).replace("", f'')
+ spotlight = urllib.request.urlopen(spotlight_url).read().decode("utf-8")
except urllib.error.URLError:
return None
else:
- sentry_sdk.api.capture_exception(exception)
- return HttpResponseServerError(spotlight)
+ event_id = sentry_sdk.api.capture_exception(exception)
+ return HttpResponseServerError(
+ spotlight.replace(
+ "",
+ (
+ f''
+ ''.format(
+ event_id=event_id
+ )
+ ),
+ )
+ )
except ImportError:
settings = None
diff --git a/sentry_sdk/transport.py b/sentry_sdk/transport.py
index 5d71a4575a..d8e8d80810 100644
--- a/sentry_sdk/transport.py
+++ b/sentry_sdk/transport.py
@@ -10,6 +10,11 @@
from collections import defaultdict
from urllib.request import getproxies
+try:
+ import brotli # type: ignore
+except ImportError:
+ brotli = None
+
import urllib3
import certifi
@@ -29,6 +34,7 @@
from typing import List
from typing import Mapping
from typing import Optional
+ from typing import Self
from typing import Tuple
from typing import Type
from typing import Union
@@ -61,20 +67,16 @@ class Transport(ABC):
parsed_dsn = None # type: Optional[Dsn]
- def __init__(
- self, options=None # type: Optional[Dict[str, Any]]
- ):
- # type: (...) -> None
+ def __init__(self, options=None):
+ # type: (Self, Optional[Dict[str, Any]]) -> None
self.options = options
if options and options["dsn"] is not None and options["dsn"]:
self.parsed_dsn = Dsn(options["dsn"])
else:
self.parsed_dsn = None
- def capture_event(
- self, event # type: Event
- ):
- # type: (...) -> None
+ def capture_event(self, event):
+ # type: (Self, Event) -> None
"""
DEPRECATED: Please use capture_envelope instead.
@@ -93,25 +95,23 @@ def capture_event(
self.capture_envelope(envelope)
@abstractmethod
- def capture_envelope(
- self, envelope # type: Envelope
- ):
- # type: (...) -> None
+ def capture_envelope(self, envelope):
+ # type: (Self, Envelope) -> None
"""
Send an envelope to Sentry.
Envelopes are a data container format that can hold any type of data
submitted to Sentry. We use it to send all event data (including errors,
- transactions, crons checkins, etc.) to Sentry.
+ transactions, crons check-ins, etc.) to Sentry.
"""
pass
def flush(
self,
- timeout, # type: float
- callback=None, # type: Optional[Any]
+ timeout,
+ callback=None,
):
- # type: (...) -> None
+ # type: (Self, float, Optional[Any]) -> None
"""
Wait `timeout` seconds for the current events to be sent out.
@@ -121,7 +121,7 @@ def flush(
return None
def kill(self):
- # type: () -> None
+ # type: (Self) -> None
"""
Forcefully kills the transport.
@@ -156,11 +156,11 @@ def record_lost_event(
return None
def is_healthy(self):
- # type: () -> bool
+ # type: (Self) -> bool
return True
def __del__(self):
- # type: () -> None
+ # type: (Self) -> None
try:
self.kill()
except Exception:
@@ -168,16 +168,16 @@ def __del__(self):
def _parse_rate_limits(header, now=None):
- # type: (Any, Optional[datetime]) -> Iterable[Tuple[Optional[EventDataCategory], datetime]]
+ # type: (str, Optional[datetime]) -> Iterable[Tuple[Optional[EventDataCategory], datetime]]
if now is None:
now = datetime.now(timezone.utc)
for limit in header.split(","):
try:
parameters = limit.strip().split(":")
- retry_after, categories = parameters[:2]
+ retry_after_val, categories = parameters[:2]
- retry_after = now + timedelta(seconds=int(retry_after))
+ retry_after = now + timedelta(seconds=int(retry_after_val))
for category in categories and categories.split(";") or (None,):
yield category, retry_after
except (LookupError, ValueError):
@@ -187,10 +187,8 @@ def _parse_rate_limits(header, now=None):
class BaseHttpTransport(Transport):
"""The base HTTP transport."""
- def __init__(
- self, options # type: Dict[str, Any]
- ):
- # type: (...) -> None
+ def __init__(self, options):
+ # type: (Self, Dict[str, Any]) -> None
from sentry_sdk.consts import VERSION
Transport.__init__(self, options)
@@ -206,13 +204,6 @@ def __init__(
) # type: DefaultDict[Tuple[EventDataCategory, str], int]
self._last_client_report_sent = time.time()
- compression_level = options.get("_experiments", {}).get(
- "transport_zlib_compression_level"
- )
- self._compression_level = (
- 9 if compression_level is None else int(compression_level)
- )
-
self._pool = self._make_pool(
self.parsed_dsn,
http_proxy=options["http_proxy"],
@@ -223,6 +214,45 @@ def __init__(
proxy_headers=options["proxy_headers"],
)
+ experiments = options.get("_experiments", {})
+ compression_level = experiments.get(
+ "transport_compression_level",
+ experiments.get("transport_zlib_compression_level"),
+ )
+ compression_algo = experiments.get(
+ "transport_compression_algo",
+ (
+ "gzip"
+ # if only compression level is set, assume gzip for backwards compatibility
+ # if we don't have brotli available, fallback to gzip
+ if compression_level is not None or brotli is None
+ else "br"
+ ),
+ )
+
+ if compression_algo == "br" and brotli is None:
+ logger.warning(
+ "You asked for brotli compression without the Brotli module, falling back to gzip -9"
+ )
+ compression_algo = "gzip"
+ compression_level = None
+
+ if compression_algo not in ("br", "gzip"):
+ logger.warning(
+ "Unknown compression algo %s, disabling compression", compression_algo
+ )
+ self._compression_level = 0
+ self._compression_algo = None
+ else:
+ self._compression_algo = compression_algo
+
+ if compression_level is not None:
+ self._compression_level = compression_level
+ elif self._compression_algo == "gzip":
+ self._compression_level = 9
+ elif self._compression_algo == "br":
+ self._compression_level = 4
+
def record_lost_event(
self,
reason, # type: str
@@ -258,11 +288,11 @@ def record_lost_event(
self._discarded_events[data_category, reason] += quantity
def _get_header_value(self, response, header):
- # type: (Any, str) -> Optional[str]
+ # type: (Self, Any, str) -> Optional[str]
return response.headers.get(header)
def _update_rate_limits(self, response):
- # type: (Union[urllib3.BaseHTTPResponse, httpcore.Response]) -> None
+ # type: (Self, Union[urllib3.BaseHTTPResponse, httpcore.Response]) -> None
# new sentries with more rate limit insights. We honor this header
# no matter of the status code to update our internal rate limits.
@@ -288,12 +318,12 @@ def _update_rate_limits(self, response):
def _send_request(
self,
- body, # type: bytes
- headers, # type: Dict[str, str]
- endpoint_type=EndpointType.ENVELOPE, # type: EndpointType
- envelope=None, # type: Optional[Envelope]
+ body,
+ headers,
+ endpoint_type=EndpointType.ENVELOPE,
+ envelope=None,
):
- # type: (...) -> None
+ # type: (Self, bytes, Dict[str, str], EndpointType, Optional[Envelope]) -> None
def record_loss(reason):
# type: (str) -> None
@@ -343,12 +373,12 @@ def record_loss(reason):
finally:
response.close()
- def on_dropped_event(self, reason):
- # type: (str) -> None
+ def on_dropped_event(self, _reason):
+ # type: (Self, str) -> None
return None
def _fetch_pending_client_report(self, force=False, interval=60):
- # type: (bool, int) -> Optional[Item]
+ # type: (Self, bool, int) -> Optional[Item]
if not self.options["send_client_reports"]:
return None
@@ -379,7 +409,7 @@ def _fetch_pending_client_report(self, force=False, interval=60):
)
def _flush_client_reports(self, force=False):
- # type: (bool) -> None
+ # type: (Self, bool) -> None
client_report = self._fetch_pending_client_report(force=force, interval=60)
if client_report is not None:
self.capture_envelope(Envelope(items=[client_report]))
@@ -394,23 +424,21 @@ def _disabled(bucket):
return _disabled(category) or _disabled(None)
def _is_rate_limited(self):
- # type: () -> bool
+ # type: (Self) -> bool
return any(
ts > datetime.now(timezone.utc) for ts in self._disabled_until.values()
)
def _is_worker_full(self):
- # type: () -> bool
+ # type: (Self) -> bool
return self._worker.full()
def is_healthy(self):
- # type: () -> bool
+ # type: (Self) -> bool
return not (self._is_worker_full() or self._is_rate_limited())
- def _send_envelope(
- self, envelope # type: Envelope
- ):
- # type: (...) -> None
+ def _send_envelope(self, envelope):
+ # type: (Self, Envelope) -> None
# remove all items from the envelope which are over quota
new_items = []
@@ -438,14 +466,7 @@ def _send_envelope(
if client_report_item is not None:
envelope.items.append(client_report_item)
- body = io.BytesIO()
- if self._compression_level == 0:
- envelope.serialize_into(body)
- else:
- with gzip.GzipFile(
- fileobj=body, mode="w", compresslevel=self._compression_level
- ) as f:
- envelope.serialize_into(f)
+ content_encoding, body = self._serialize_envelope(envelope)
assert self.parsed_dsn is not None
logger.debug(
@@ -458,8 +479,8 @@ def _send_envelope(
headers = {
"Content-Type": "application/x-sentry-envelope",
}
- if self._compression_level > 0:
- headers["Content-Encoding"] = "gzip"
+ if content_encoding:
+ headers["Content-Encoding"] = content_encoding
self._send_request(
body.getvalue(),
@@ -469,12 +490,34 @@ def _send_envelope(
)
return None
+ def _serialize_envelope(self, envelope):
+ # type: (Self, Envelope) -> tuple[Optional[str], io.BytesIO]
+ content_encoding = None
+ body = io.BytesIO()
+ if self._compression_level == 0 or self._compression_algo is None:
+ envelope.serialize_into(body)
+ else:
+ content_encoding = self._compression_algo
+ if self._compression_algo == "br" and brotli is not None:
+ body.write(
+ brotli.compress(
+ envelope.serialize(), quality=self._compression_level
+ )
+ )
+ else: # assume gzip as we sanitize the algo value in init
+ with gzip.GzipFile(
+ fileobj=body, mode="w", compresslevel=self._compression_level
+ ) as f:
+ envelope.serialize_into(f)
+
+ return content_encoding, body
+
def _get_pool_options(self, ca_certs, cert_file=None, key_file=None):
- # type: (Optional[Any], Optional[Any], Optional[Any]) -> Dict[str, Any]
+ # type: (Self, Optional[Any], Optional[Any], Optional[Any]) -> Dict[str, Any]
raise NotImplementedError()
def _in_no_proxy(self, parsed_dsn):
- # type: (Dsn) -> bool
+ # type: (Self, Dsn) -> bool
no_proxy = getproxies().get("no")
if not no_proxy:
return False
@@ -504,7 +547,7 @@ def _request(
body,
headers,
):
- # type: (str, EndpointType, Any, Mapping[str, str]) -> Union[urllib3.BaseHTTPResponse, httpcore.Response]
+ # type: (Self, str, EndpointType, Any, Mapping[str, str]) -> Union[urllib3.BaseHTTPResponse, httpcore.Response]
raise NotImplementedError()
def capture_envelope(
@@ -524,10 +567,10 @@ def send_envelope_wrapper():
def flush(
self,
- timeout, # type: float
- callback=None, # type: Optional[Any]
+ timeout,
+ callback=None,
):
- # type: (...) -> None
+ # type: (Self, float, Optional[Callable[[int, float], None]]) -> None
logger.debug("Flushing HTTP transport")
if timeout > 0:
@@ -535,7 +578,7 @@ def flush(
self._worker.flush(timeout, callback)
def kill(self):
- # type: () -> None
+ # type: (Self) -> None
logger.debug("Killing HTTP transport")
self._worker.kill()
@@ -545,7 +588,7 @@ class HttpTransport(BaseHttpTransport):
_pool: Union[PoolManager, ProxyManager]
def _get_pool_options(self, ca_certs, cert_file=None, key_file=None):
- # type: (Optional[Any], Optional[Any], Optional[Any]) -> Dict[str, Any]
+ # type: (Self, Any, Any, Any) -> Dict[str, Any]
num_pools = self.options.get("_experiments", {}).get("transport_num_pools")
options = {
@@ -587,9 +630,9 @@ def _make_pool(
parsed_dsn, # type: Dsn
http_proxy, # type: Optional[str]
https_proxy, # type: Optional[str]
- ca_certs, # type: Optional[Any]
- cert_file, # type: Optional[Any]
- key_file, # type: Optional[Any]
+ ca_certs, # type: Any
+ cert_file, # type: Any
+ key_file, # type: Any
proxy_headers, # type: Optional[Dict[str, str]]
):
# type: (...) -> Union[PoolManager, ProxyManager]
@@ -638,7 +681,7 @@ def _request(
body,
headers,
):
- # type: (str, EndpointType, Any, Mapping[str, str]) -> urllib3.BaseHTTPResponse
+ # type: (Self, str, EndpointType, Any, Mapping[str, str]) -> urllib3.BaseHTTPResponse
return self._pool.request(
method,
self._auth.get_api_url(endpoint_type),
@@ -652,10 +695,8 @@ def _request(
except ImportError:
# Sorry, no Http2Transport for you
class Http2Transport(HttpTransport):
- def __init__(
- self, options # type: Dict[str, Any]
- ):
- # type: (...) -> None
+ def __init__(self, options):
+ # type: (Self, Dict[str, Any]) -> None
super().__init__(options)
logger.warning(
"You tried to use HTTP2Transport but don't have httpcore[http2] installed. Falling back to HTTPTransport."
@@ -672,7 +713,7 @@ class Http2Transport(BaseHttpTransport): # type: ignore
]
def _get_header_value(self, response, header):
- # type: (httpcore.Response, str) -> Optional[str]
+ # type: (Self, httpcore.Response, str) -> Optional[str]
return next(
(
val.decode("ascii")
@@ -689,7 +730,7 @@ def _request(
body,
headers,
):
- # type: (str, EndpointType, Any, Mapping[str, str]) -> httpcore.Response
+ # type: (Self, str, EndpointType, Any, Mapping[str, str]) -> httpcore.Response
response = self._pool.request(
method,
self._auth.get_api_url(endpoint_type),
@@ -699,7 +740,7 @@ def _request(
return response
def _get_pool_options(self, ca_certs, cert_file=None, key_file=None):
- # type: (Optional[Any], Optional[Any], Optional[Any]) -> Dict[str, Any]
+ # type: (Any, Any, Any) -> Dict[str, Any]
options = {
"http2": True,
"retries": 3,
@@ -739,9 +780,9 @@ def _make_pool(
parsed_dsn, # type: Dsn
http_proxy, # type: Optional[str]
https_proxy, # type: Optional[str]
- ca_certs, # type: Optional[Any]
- cert_file, # type: Optional[Any]
- key_file, # type: Optional[Any]
+ ca_certs, # type: Any
+ cert_file, # type: Any
+ key_file, # type: Any
proxy_headers, # type: Optional[Dict[str, str]]
):
# type: (...) -> Union[httpcore.SOCKSProxy, httpcore.HTTPProxy, httpcore.ConnectionPool]
diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py
index 5fb3cf604c..80a5df9700 100644
--- a/sentry_sdk/utils.py
+++ b/sentry_sdk/utils.py
@@ -30,8 +30,6 @@
from typing import TYPE_CHECKING
if TYPE_CHECKING:
- from collections.abc import Awaitable
-
from types import FrameType, TracebackType
from typing import (
Any,
@@ -687,11 +685,21 @@ def get_errno(exc_value):
def get_error_message(exc_value):
# type: (Optional[BaseException]) -> str
- return (
+ message = (
getattr(exc_value, "message", "")
or getattr(exc_value, "detail", "")
or safe_str(exc_value)
- )
+ ) # type: str
+
+ # __notes__ should be a list of strings when notes are added
+ # via add_note, but can be anything else if __notes__ is set
+ # directly. We only support strings in __notes__, since that
+ # is the correct use.
+ notes = getattr(exc_value, "__notes__", None) # type: object
+ if isinstance(notes, list) and len(notes) > 0:
+ message += "\n" + "\n".join(note for note in notes if isinstance(note, str))
+
+ return message
def single_exception_from_error_tuple(
@@ -1681,12 +1689,6 @@ def _no_op(*_a, **_k):
pass
-async def _no_op_async(*_a, **_k):
- # type: (*Any, **Any) -> None
- """No-op function for ensure_integration_enabled_async."""
- pass
-
-
if TYPE_CHECKING:
@overload
@@ -1753,59 +1755,6 @@ def runner(*args: "P.args", **kwargs: "P.kwargs"):
return patcher
-if TYPE_CHECKING:
-
- # mypy has some trouble with the overloads, hence the ignore[no-overload-impl]
- @overload # type: ignore[no-overload-impl]
- def ensure_integration_enabled_async(
- integration, # type: type[sentry_sdk.integrations.Integration]
- original_function, # type: Callable[P, Awaitable[R]]
- ):
- # type: (...) -> Callable[[Callable[P, Awaitable[R]]], Callable[P, Awaitable[R]]]
- ...
-
- @overload
- def ensure_integration_enabled_async(
- integration, # type: type[sentry_sdk.integrations.Integration]
- ):
- # type: (...) -> Callable[[Callable[P, Awaitable[None]]], Callable[P, Awaitable[None]]]
- ...
-
-
-# The ignore[no-redef] also needed because mypy is struggling with these overloads.
-def ensure_integration_enabled_async( # type: ignore[no-redef]
- integration, # type: type[sentry_sdk.integrations.Integration]
- original_function=_no_op_async, # type: Union[Callable[P, Awaitable[R]], Callable[P, Awaitable[None]]]
-):
- # type: (...) -> Callable[[Callable[P, Awaitable[R]]], Callable[P, Awaitable[R]]]
- """
- Version of `ensure_integration_enabled` for decorating async functions.
-
- Please refer to the `ensure_integration_enabled` documentation for more information.
- """
-
- if TYPE_CHECKING:
- # Type hint to ensure the default function has the right typing. The overloads
- # ensure the default _no_op function is only used when R is None.
- original_function = cast(Callable[P, Awaitable[R]], original_function)
-
- def patcher(sentry_patched_function):
- # type: (Callable[P, Awaitable[R]]) -> Callable[P, Awaitable[R]]
- async def runner(*args: "P.args", **kwargs: "P.kwargs"):
- # type: (...) -> R
- if sentry_sdk.get_client().get_integration(integration) is None:
- return await original_function(*args, **kwargs)
-
- return await sentry_patched_function(*args, **kwargs)
-
- if original_function is _no_op_async:
- return wraps(sentry_patched_function)(runner)
-
- return wraps(original_function)(runner)
-
- return patcher
-
-
def now():
# type: () -> float
return time.perf_counter()
diff --git a/setup.py b/setup.py
index 24f1203406..bb72fe2b31 100644
--- a/setup.py
+++ b/setup.py
@@ -21,7 +21,7 @@ def get_file_text(file_name):
setup(
name="sentry-sdk",
- version="2.15.0",
+ version="2.16.0",
author="Sentry Team and Contributors",
author_email="hello@sentry.io",
url="https://github.com/getsentry/sentry-python",
@@ -99,6 +99,7 @@ def get_file_text(file_name):
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
+ "Programming Language :: Python :: 3.13",
"Topic :: Software Development :: Libraries :: Python Modules",
],
options={"bdist_wheel": {"universal": "1"}},
diff --git a/tests/integrations/aiohttp/test_aiohttp.py b/tests/integrations/aiohttp/test_aiohttp.py
index 5b25629a83..cd65e7cdd5 100644
--- a/tests/integrations/aiohttp/test_aiohttp.py
+++ b/tests/integrations/aiohttp/test_aiohttp.py
@@ -55,7 +55,7 @@ async def hello(request):
assert request["url"] == "http://{host}/".format(host=host)
assert request["headers"] == {
"Accept": "*/*",
- "Accept-Encoding": "gzip, deflate",
+ "Accept-Encoding": mock.ANY,
"Host": host,
"User-Agent": request["headers"]["User-Agent"],
"baggage": mock.ANY,
diff --git a/tests/integrations/bottle/test_bottle.py b/tests/integrations/bottle/test_bottle.py
index 9dd23cf45a..9cc436a229 100644
--- a/tests/integrations/bottle/test_bottle.py
+++ b/tests/integrations/bottle/test_bottle.py
@@ -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
@@ -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"
diff --git a/tests/test_basics.py b/tests/test_basics.py
index 62122b3fff..749d31d7d3 100644
--- a/tests/test_basics.py
+++ b/tests/test_basics.py
@@ -30,7 +30,6 @@
setup_integrations,
)
from sentry_sdk.integrations.logging import LoggingIntegration
-from sentry_sdk.integrations.redis import RedisIntegration
from sentry_sdk.integrations.stdlib import StdlibIntegration
from sentry_sdk.scope import add_global_event_processor
from sentry_sdk.utils import get_sdk_name, reraise
@@ -748,13 +747,6 @@ def test_functions_to_trace_with_class(sentry_init, capture_events):
assert event["spans"][1]["description"] == "tests.test_basics.WorldGreeter.greet"
-def test_redis_disabled_when_not_installed(sentry_init):
- with ModuleImportErrorSimulator(["redis"], ImportError):
- sentry_init()
-
- assert sentry_sdk.get_client().get_integration(RedisIntegration) is None
-
-
def test_multiple_setup_integrations_calls():
first_call_return = setup_integrations([NoOpIntegration()], with_defaults=False)
assert first_call_return == {NoOpIntegration.identifier: NoOpIntegration()}
@@ -842,3 +834,46 @@ def test_last_event_id_scope(sentry_init):
# Should not crash
with isolation_scope() as scope:
assert scope.last_event_id() is None
+
+
+@pytest.mark.skipif(sys.version_info < (3, 11), reason="add_note() not supported")
+def test_notes(sentry_init, capture_events):
+ sentry_init()
+ events = capture_events()
+ try:
+ e = ValueError("aha!")
+ e.add_note("Test 123")
+ e.add_note("another note")
+ raise e
+ except Exception:
+ capture_exception()
+
+ (event,) = events
+
+ assert event["exception"]["values"][0]["value"] == "aha!\nTest 123\nanother note"
+
+
+@pytest.mark.skipif(sys.version_info < (3, 11), reason="add_note() not supported")
+def test_notes_safe_str(sentry_init, capture_events):
+ class Note2:
+ def __repr__(self):
+ raise TypeError
+
+ def __str__(self):
+ raise TypeError
+
+ sentry_init()
+ events = capture_events()
+ try:
+ e = ValueError("aha!")
+ e.add_note("note 1")
+ e.__notes__.append(Note2()) # type: ignore
+ e.add_note("note 3")
+ e.__notes__.append(2) # type: ignore
+ raise e
+ except Exception:
+ capture_exception()
+
+ (event,) = events
+
+ assert event["exception"]["values"][0]["value"] == "aha!\nnote 1\nnote 3"
diff --git a/tests/test_transport.py b/tests/test_transport.py
index fdb360826f..2a326fbdfb 100644
--- a/tests/test_transport.py
+++ b/tests/test_transport.py
@@ -9,6 +9,7 @@
from datetime import datetime, timedelta, timezone
from unittest import mock
+import brotli
import pytest
from pytest_localserver.http import WSGIServer
from werkzeug.wrappers import Request, Response
@@ -52,9 +53,13 @@ def __call__(self, environ, start_response):
"""
request = Request(environ)
event = envelope = None
- if request.headers.get("content-encoding") == "gzip":
+ content_encoding = request.headers.get("content-encoding")
+ if content_encoding == "gzip":
rdr = gzip.GzipFile(fileobj=io.BytesIO(request.data))
compressed = True
+ elif content_encoding == "br":
+ rdr = io.BytesIO(brotli.decompress(request.data))
+ compressed = True
else:
rdr = io.BytesIO(request.data)
compressed = False
@@ -115,7 +120,8 @@ def mock_transaction_envelope(span_count):
@pytest.mark.parametrize("debug", (True, False))
@pytest.mark.parametrize("client_flush_method", ["close", "flush"])
@pytest.mark.parametrize("use_pickle", (True, False))
-@pytest.mark.parametrize("compression_level", (0, 9))
+@pytest.mark.parametrize("compression_level", (0, 9, None))
+@pytest.mark.parametrize("compression_algo", ("gzip", "br", "", None))
@pytest.mark.parametrize(
"http2", [True, False] if sys.version_info >= (3, 8) else [False]
)
@@ -129,14 +135,18 @@ def test_transport_works(
client_flush_method,
use_pickle,
compression_level,
+ compression_algo,
http2,
maybe_monkeypatched_threading,
):
caplog.set_level(logging.DEBUG)
- experiments = {
- "transport_zlib_compression_level": compression_level,
- }
+ experiments = {}
+ if compression_level is not None:
+ experiments["transport_compression_level"] = compression_level
+
+ if compression_algo is not None:
+ experiments["transport_compression_algo"] = compression_algo
if http2:
experiments["transport_http2"] = True
@@ -162,7 +172,21 @@ def test_transport_works(
out, err = capsys.readouterr()
assert not err and not out
assert capturing_server.captured
- assert capturing_server.captured[0].compressed == (compression_level > 0)
+ should_compress = (
+ # default is to compress with brotli if available, gzip otherwise
+ (compression_level is None)
+ or (
+ # setting compression level to 0 means don't compress
+ compression_level
+ > 0
+ )
+ ) and (
+ # if we couldn't resolve to a known algo, we don't compress
+ compression_algo
+ != ""
+ )
+
+ assert capturing_server.captured[0].compressed == should_compress
assert any("Sending envelope" in record.msg for record in caplog.records) == debug
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 4df343a357..6745e2a966 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -30,14 +30,12 @@
_get_installed_modules,
_generate_installed_modules,
ensure_integration_enabled,
- ensure_integration_enabled_async,
)
class TestIntegration(Integration):
"""
- Test integration for testing ensure_integration_enabled and
- ensure_integration_enabled_async decorators.
+ Test integration for testing ensure_integration_enabled decorator.
"""
identifier = "test"
@@ -733,90 +731,6 @@ def function_to_patch():
assert patched_function.__name__ == "function_to_patch"
-@pytest.mark.asyncio
-async def test_ensure_integration_enabled_async_integration_enabled(sentry_init):
- # Setup variables and functions for the test
- async def original_function():
- return "original"
-
- async def function_to_patch():
- return "patched"
-
- sentry_init(integrations=[TestIntegration()])
-
- # Test the decorator by applying to function_to_patch
- patched_function = ensure_integration_enabled_async(
- TestIntegration, original_function
- )(function_to_patch)
-
- assert await patched_function() == "patched"
- assert patched_function.__name__ == "original_function"
-
-
-@pytest.mark.asyncio
-async def test_ensure_integration_enabled_async_integration_disabled(sentry_init):
- # Setup variables and functions for the test
- async def original_function():
- return "original"
-
- async def function_to_patch():
- return "patched"
-
- sentry_init(integrations=[]) # TestIntegration is disabled
-
- # Test the decorator by applying to function_to_patch
- patched_function = ensure_integration_enabled_async(
- TestIntegration, original_function
- )(function_to_patch)
-
- assert await patched_function() == "original"
- assert patched_function.__name__ == "original_function"
-
-
-@pytest.mark.asyncio
-async def test_ensure_integration_enabled_async_no_original_function_enabled(
- sentry_init,
-):
- shared_variable = "original"
-
- async def function_to_patch():
- nonlocal shared_variable
- shared_variable = "patched"
-
- sentry_init(integrations=[TestIntegration])
-
- # Test the decorator by applying to function_to_patch
- patched_function = ensure_integration_enabled_async(TestIntegration)(
- function_to_patch
- )
- await patched_function()
-
- assert shared_variable == "patched"
- assert patched_function.__name__ == "function_to_patch"
-
-
-@pytest.mark.asyncio
-async def test_ensure_integration_enabled_async_no_original_function_disabled(
- sentry_init,
-):
- shared_variable = "original"
-
- async def function_to_patch():
- nonlocal shared_variable
- shared_variable = "patched"
-
- sentry_init(integrations=[])
-
- # Test the decorator by applying to function_to_patch
- patched_function = ensure_integration_enabled_async(TestIntegration)(
- function_to_patch
- )
- await patched_function()
-
- assert shared_variable == "original"
- assert patched_function.__name__ == "function_to_patch"
-
-
@pytest.mark.parametrize(
"delta,expected_milliseconds",
[
diff --git a/tox.ini b/tox.ini
index 6dd3a367e5..ebe8d06506 100644
--- a/tox.ini
+++ b/tox.ini
@@ -756,6 +756,7 @@ basepython =
py3.10: python3.10
py3.11: python3.11
py3.12: python3.12
+ py3.13: python3.13
# Python version is pinned here because flake8 actually behaves differently
# depending on which version is used. You can patch this out to point to