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
2 changes: 1 addition & 1 deletion .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -1 +1 @@
* @getsentry/team-web-sdk-backend
* @getsentry/owners-python-sdk
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
steps:
- name: Get auth token
id: token
uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1.12.0
uses: actions/create-github-app-token@3ff1caaa28b64c9cc276ce0a02e2ff584f3900c5 # v2.0.2
with:
app-id: ${{ vars.SENTRY_RELEASE_BOT_CLIENT_ID }}
private-key: ${{ secrets.SENTRY_RELEASE_BOT_PRIVATE_KEY }}
Expand Down
72 changes: 51 additions & 21 deletions scripts/populate_tox/populate_tox.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import time
from bisect import bisect_left
from collections import defaultdict
from datetime import datetime, timezone
from datetime import datetime, timedelta, timezone # noqa: F401
from importlib.metadata import metadata
from packaging.specifiers import SpecifierSet
from packaging.version import Version
Expand All @@ -29,13 +29,19 @@
from split_tox_gh_actions.split_tox_gh_actions import GROUPS


# Set CUTOFF this to a datetime to ignore packages older than CUTOFF
CUTOFF = None
# CUTOFF = datetime.now(tz=timezone.utc) - timedelta(days=365 * 5)

TOX_FILE = Path(__file__).resolve().parent.parent.parent / "tox.ini"
ENV = Environment(
loader=FileSystemLoader(Path(__file__).resolve().parent),
trim_blocks=True,
lstrip_blocks=True,
)

PYPI_COOLDOWN = 0.15 # seconds to wait between requests to PyPI

PYPI_PROJECT_URL = "https://pypi.python.org/pypi/{project}/json"
PYPI_VERSION_URL = "https://pypi.python.org/pypi/{project}/{version}/json"
CLASSIFIER_PREFIX = "Programming Language :: Python :: "
Expand Down Expand Up @@ -88,27 +94,34 @@
}


@functools.cache
def fetch_package(package: str) -> dict:
"""Fetch package metadata from PyPI."""
url = PYPI_PROJECT_URL.format(project=package)
pypi_data = requests.get(url)
def fetch_url(url: str) -> Optional[dict]:
for attempt in range(3):
pypi_data = requests.get(url)

if pypi_data.status_code != 200:
print(f"{package} not found")
if pypi_data.status_code == 200:
return pypi_data.json()

return pypi_data.json()
backoff = PYPI_COOLDOWN * 2**attempt
print(
f"{url} returned an error: {pypi_data.status_code}. Attempt {attempt + 1}/3. Waiting {backoff}s"
)
time.sleep(backoff)

return None


@functools.cache
def fetch_release(package: str, version: Version) -> dict:
url = PYPI_VERSION_URL.format(project=package, version=version)
pypi_data = requests.get(url)
def fetch_package(package: str) -> Optional[dict]:
"""Fetch package metadata from PyPI."""
url = PYPI_PROJECT_URL.format(project=package)
return fetch_url(url)

if pypi_data.status_code != 200:
print(f"{package} not found")

return pypi_data.json()
@functools.cache
def fetch_release(package: str, version: Version) -> Optional[dict]:
"""Fetch release metadata from PyPI."""
url = PYPI_VERSION_URL.format(project=package, version=version)
return fetch_url(url)


def _prefilter_releases(
Expand Down Expand Up @@ -153,9 +166,13 @@ def _prefilter_releases(
if meta["yanked"]:
continue

if older_than is not None:
if datetime.fromisoformat(meta["upload_time_iso_8601"]) > older_than:
continue
uploaded = datetime.fromisoformat(meta["upload_time_iso_8601"])

if older_than is not None and uploaded > older_than:
continue

if CUTOFF is not None and uploaded < CUTOFF:
continue

version = Version(release)

Expand Down Expand Up @@ -229,8 +246,14 @@ def get_supported_releases(
expected_python_versions = SpecifierSet(f">={MIN_PYTHON_VERSION}")

def _supports_lowest(release: Version) -> bool:
time.sleep(0.1) # don't DoS PYPI
py_versions = determine_python_versions(fetch_release(package, release))
time.sleep(PYPI_COOLDOWN) # don't DoS PYPI

pypi_data = fetch_release(package, release)
if pypi_data is None:
print("Failed to fetch necessary data from PyPI. Aborting.")
sys.exit(1)

py_versions = determine_python_versions(pypi_data)
target_python_versions = TEST_SUITE_CONFIG[integration].get("python")
if target_python_versions:
target_python_versions = SpecifierSet(target_python_versions)
Expand Down Expand Up @@ -499,7 +522,11 @@ def _add_python_versions_to_release(
integration: str, package: str, release: Version
) -> None:
release_pypi_data = fetch_release(package, release)
time.sleep(0.1) # give PYPI some breathing room
if release_pypi_data is None:
print("Failed to fetch necessary data from PyPI. Aborting.")
sys.exit(1)

time.sleep(PYPI_COOLDOWN) # give PYPI some breathing room

target_python_versions = TEST_SUITE_CONFIG[integration].get("python")
if target_python_versions:
Expand Down Expand Up @@ -592,6 +619,9 @@ def main(fail_on_changes: bool = False) -> None:

# Fetch data for the main package
pypi_data = fetch_package(package)
if pypi_data is None:
print("Failed to fetch necessary data from PyPI. Aborting.")
sys.exit(1)

# Get the list of all supported releases

Expand Down
8 changes: 8 additions & 0 deletions sentry_sdk/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from sentry_sdk.tracing import trace
from sentry_sdk.transport import BaseHttpTransport, make_transport
from sentry_sdk.consts import (
SPANDATA,
DEFAULT_MAX_VALUE_LENGTH,
DEFAULT_OPTIONS,
VERSION,
Expand Down Expand Up @@ -837,6 +838,13 @@ def _capture_experimental_log(self, current_scope, log):
return
isolation_scope = current_scope.get_isolation_scope()

log["attributes"]["sentry.sdk.name"] = SDK_INFO["name"]
log["attributes"]["sentry.sdk.version"] = SDK_INFO["version"]

server_name = self.options.get("server_name")
if server_name is not None and SPANDATA.SERVER_ADDRESS not in log["attributes"]:
log["attributes"][SPANDATA.SERVER_ADDRESS] = server_name

environment = self.options.get("environment")
if environment is not None and "sentry.environment" not in log["attributes"]:
log["attributes"]["sentry.environment"] = environment
Expand Down
4 changes: 3 additions & 1 deletion sentry_sdk/integrations/logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,9 @@ def _capture_log_from_record(client, record):
# type: (BaseClient, LogRecord) -> None
scope = sentry_sdk.get_current_scope()
otel_severity_number, otel_severity_text = _python_level_to_otel(record.levelno)
attrs = {} # type: dict[str, str | bool | float | int]
attrs = {
"sentry.origin": "auto.logger.log",
} # type: dict[str, str | bool | float | int]
if isinstance(record.msg, str):
attrs["sentry.message.template"] = record.msg
if record.args is not None:
Expand Down
13 changes: 13 additions & 0 deletions sentry_sdk/transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,8 @@ def _parse_rate_limits(header, now=None):
class BaseHttpTransport(Transport):
"""The base HTTP transport."""

TIMEOUT = 30 # seconds

def __init__(self, options):
# type: (Self, Dict[str, Any]) -> None
from sentry_sdk.consts import VERSION
Expand Down Expand Up @@ -558,6 +560,7 @@ def _get_pool_options(self):
options = {
"num_pools": 2 if num_pools is None else int(num_pools),
"cert_reqs": "CERT_REQUIRED",
"timeout": urllib3.Timeout(total=self.TIMEOUT),
}

socket_options = None # type: Optional[List[Tuple[int, int, int | bytes]]]
Expand Down Expand Up @@ -673,6 +676,8 @@ def __init__(self, options):
class Http2Transport(BaseHttpTransport): # type: ignore
"""The HTTP2 transport based on httpcore."""

TIMEOUT = 15

if TYPE_CHECKING:
_pool: Union[
httpcore.SOCKSProxy, httpcore.HTTPProxy, httpcore.ConnectionPool
Expand Down Expand Up @@ -702,6 +707,14 @@ def _request(
self._auth.get_api_url(endpoint_type),
content=body,
headers=headers, # type: ignore
extensions={
"timeout": {
"pool": self.TIMEOUT,
"connect": self.TIMEOUT,
"write": self.TIMEOUT,
"read": self.TIMEOUT,
}
},
)
return response

Expand Down
7 changes: 6 additions & 1 deletion tests/test_logs.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from sentry_sdk.envelope import Envelope
from sentry_sdk.integrations.logging import LoggingIntegration
from sentry_sdk.types import Log
from sentry_sdk.consts import SPANDATA, VERSION

minimum_python_37 = pytest.mark.skipif(
sys.version_info < (3, 7), reason="Asyncio tests need Python >= 3.7"
Expand Down Expand Up @@ -161,7 +162,7 @@ def test_logs_attributes(sentry_init, capture_envelopes):
"""
Passing arbitrary attributes to log messages.
"""
sentry_init(_experiments={"enable_logs": True})
sentry_init(_experiments={"enable_logs": True}, server_name="test-server")
envelopes = capture_envelopes()

attrs = {
Expand All @@ -184,6 +185,9 @@ def test_logs_attributes(sentry_init, capture_envelopes):
assert logs[0]["attributes"]["sentry.environment"] == "production"
assert "sentry.release" in logs[0]["attributes"]
assert logs[0]["attributes"]["sentry.message.parameters.my_var"] == "some value"
assert logs[0]["attributes"][SPANDATA.SERVER_ADDRESS] == "test-server"
assert logs[0]["attributes"]["sentry.sdk.name"] == "sentry.python"
assert logs[0]["attributes"]["sentry.sdk.version"] == VERSION


@minimum_python_37
Expand Down Expand Up @@ -283,6 +287,7 @@ def test_logger_integration_warning(sentry_init, capture_envelopes):
assert attrs["sentry.environment"] == "production"
assert attrs["sentry.message.parameters.0"] == "1"
assert attrs["sentry.message.parameters.1"] == "2"
assert attrs["sentry.origin"] == "auto.logger.log"
assert logs[0]["severity_number"] == 13
assert logs[0]["severity_text"] == "warn"

Expand Down
41 changes: 41 additions & 0 deletions tests/test_transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,16 @@
from pytest_localserver.http import WSGIServer
from werkzeug.wrappers import Request, Response

try:
import httpcore
except (ImportError, ModuleNotFoundError):
httpcore = None

try:
import gevent
except ImportError:
gevent = None

import sentry_sdk
from sentry_sdk import (
Client,
Expand Down Expand Up @@ -260,6 +270,37 @@ def test_keep_alive_on_by_default(make_client):
assert "socket_options" not in options


def test_default_timeout(make_client):
client = make_client()

options = client.transport._get_pool_options()
assert "timeout" in options
assert options["timeout"].total == client.transport.TIMEOUT


@pytest.mark.skipif(not PY38, reason="HTTP2 libraries are only available in py3.8+")
def test_default_timeout_http2(make_client):
client = make_client(_experiments={"transport_http2": True})

with mock.patch(
"sentry_sdk.transport.httpcore.ConnectionPool.request",
return_value=httpcore.Response(200),
) as request_mock:
sentry_sdk.get_global_scope().set_client(client)
capture_message("hi")
client.flush()

request_mock.assert_called_once()
assert request_mock.call_args.kwargs["extensions"] == {
"timeout": {
"pool": client.transport.TIMEOUT,
"connect": client.transport.TIMEOUT,
"write": client.transport.TIMEOUT,
"read": client.transport.TIMEOUT,
}
}


@pytest.mark.skipif(not PY38, reason="HTTP2 libraries are only available in py3.8+")
def test_http2_with_https_dsn(make_client):
client = make_client(_experiments={"transport_http2": True})
Expand Down
18 changes: 9 additions & 9 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
# The file (and all resulting CI YAMLs) then need to be regenerated via
# "scripts/generate-test-files.sh".
#
# Last generated: 2025-04-04T13:09:23.106982+00:00
# Last generated: 2025-04-08T10:33:11.499210+00:00

[tox]
requires =
Expand Down Expand Up @@ -200,7 +200,7 @@ envlist =
{py3.8,py3.10,py3.11}-strawberry-v0.209.8
{py3.8,py3.11,py3.12}-strawberry-v0.227.7
{py3.8,py3.11,py3.12}-strawberry-v0.245.0
{py3.9,py3.12,py3.13}-strawberry-v0.263.1
{py3.9,py3.12,py3.13}-strawberry-v0.263.2


# ~~~ Network ~~~
Expand All @@ -211,9 +211,9 @@ envlist =


# ~~~ Tasks ~~~
{py3.8}-celery-v4.4.7
{py3.8}-celery-v5.0.5
{py3.8,py3.12,py3.13}-celery-v5.5.0
{py3.6,py3.7,py3.8}-celery-v4.4.7
{py3.6,py3.7,py3.8}-celery-v5.0.5
{py3.8,py3.12,py3.13}-celery-v5.5.1

{py3.7}-dramatiq-v1.9.0
{py3.7,py3.8,py3.9}-dramatiq-v1.12.3
Expand Down Expand Up @@ -256,7 +256,7 @@ envlist =
{py3.8,py3.10,py3.11}-litestar-v2.0.1
{py3.8,py3.11,py3.12}-litestar-v2.5.5
{py3.8,py3.11,py3.12}-litestar-v2.10.0
{py3.8,py3.12,py3.13}-litestar-v2.15.1
{py3.8,py3.12,py3.13}-litestar-v2.15.2

{py3.7,py3.8,py3.9}-pyramid-v1.10.8
{py3.7,py3.10,py3.11}-pyramid-v2.0.2
Expand Down Expand Up @@ -564,7 +564,7 @@ deps =
strawberry-v0.209.8: strawberry-graphql[fastapi,flask]==0.209.8
strawberry-v0.227.7: strawberry-graphql[fastapi,flask]==0.227.7
strawberry-v0.245.0: strawberry-graphql[fastapi,flask]==0.245.0
strawberry-v0.263.1: strawberry-graphql[fastapi,flask]==0.263.1
strawberry-v0.263.2: strawberry-graphql[fastapi,flask]==0.263.2
strawberry: httpx
strawberry-v0.209.8: pydantic<2.11
strawberry-v0.227.7: pydantic<2.11
Expand All @@ -585,7 +585,7 @@ deps =
# ~~~ Tasks ~~~
celery-v4.4.7: celery==4.4.7
celery-v5.0.5: celery==5.0.5
celery-v5.5.0: celery==5.5.0
celery-v5.5.1: celery==5.5.1
celery: newrelic
celery: redis

Expand Down Expand Up @@ -663,7 +663,7 @@ deps =
litestar-v2.0.1: litestar==2.0.1
litestar-v2.5.5: litestar==2.5.5
litestar-v2.10.0: litestar==2.10.0
litestar-v2.15.1: litestar==2.15.1
litestar-v2.15.2: litestar==2.15.2
litestar: pytest-asyncio
litestar: python-multipart
litestar: requests
Expand Down
Loading