Skip to content

Commit

Permalink
Merge branch 'master' into szokeasaurusrex/get_integration-typing
Browse files Browse the repository at this point in the history
  • Loading branch information
szokeasaurusrex committed Sep 23, 2024
2 parents bad136f + 26b86a5 commit 0196f36
Show file tree
Hide file tree
Showing 8 changed files with 162 additions and 23 deletions.
8 changes: 7 additions & 1 deletion sentry_sdk/integrations/_wsgi_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,13 @@ def json(self):
if not self.is_json():
return None

raw_data = self.raw_data()
try:
raw_data = self.raw_data()
except (RawPostDataException, ValueError):
# The body might have already been read, in which case this will
# fail
raw_data = None

if raw_data is None:
return None

Expand Down
12 changes: 11 additions & 1 deletion sentry_sdk/integrations/aiohttp.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,17 @@ async def sentry_app_handle(self, request, *args, **kwargs):
# have no way to tell. Do not set span status.
reraise(*_capture_exception())

transaction.set_http_status(response.status)
try:
# A valid response handler will return a valid response with a status. But, if the handler
# returns an invalid response (e.g. None), the line below will raise an AttributeError.
# Even though this is likely invalid, we need to handle this case to ensure we don't break
# the application.
response_status = response.status
except AttributeError:
pass
else:
transaction.set_http_status(response_status)

return response

Application._handle = sentry_app_handle
Expand Down
5 changes: 3 additions & 2 deletions sentry_sdk/scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -968,7 +968,7 @@ def start_transaction(
transaction=None,
instrumenter=INSTRUMENTER.SENTRY,
custom_sampling_context=None,
**kwargs
**kwargs,
):
# type: (Optional[Transaction], str, Optional[SamplingContext], Unpack[TransactionKwargs]) -> Union[Transaction, NoOpSpan]
"""
Expand Down Expand Up @@ -1324,7 +1324,8 @@ def _apply_breadcrumbs_to_event(self, event, hint, options):
crumb["timestamp"] = datetime_from_isoformat(crumb["timestamp"])

event["breadcrumbs"]["values"].sort(key=lambda crumb: crumb["timestamp"])
except Exception:
except Exception as err:
logger.debug("Error when sorting breadcrumbs", exc_info=err)
pass

def _apply_user_to_event(self, event, hint, options):
Expand Down
22 changes: 19 additions & 3 deletions sentry_sdk/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,13 +239,29 @@ def format_timestamp(value):
return utctime.strftime("%Y-%m-%dT%H:%M:%S.%fZ")


ISO_TZ_SEPARATORS = frozenset(("+", "-"))


def datetime_from_isoformat(value):
# type: (str) -> datetime
try:
return datetime.fromisoformat(value)
except AttributeError:
result = datetime.fromisoformat(value)
except (AttributeError, ValueError):
# py 3.6
return datetime.strptime(value, "%Y-%m-%dT%H:%M:%S.%f")
timestamp_format = (
"%Y-%m-%dT%H:%M:%S.%f" if "." in value else "%Y-%m-%dT%H:%M:%S"
)
if value.endswith("Z"):
value = value[:-1] + "+0000"

if value[-6] in ISO_TZ_SEPARATORS:
timestamp_format += "%z"
value = value[:-3] + value[-2:]
elif value[-5] in ISO_TZ_SEPARATORS:
timestamp_format += "%z"

result = datetime.strptime(value, timestamp_format)
return result.astimezone(timezone.utc)


def event_hint_with_exc_info(exc_info=None):
Expand Down
21 changes: 21 additions & 0 deletions tests/integrations/aiohttp/test_aiohttp.py
Original file line number Diff line number Diff line change
Expand Up @@ -596,3 +596,24 @@ async def hello(request):
(event,) = events
assert event["contexts"]["trace"]["origin"] == "auto.http.aiohttp"
assert event["spans"][0]["origin"] == "auto.http.aiohttp"


@pytest.mark.asyncio
@pytest.mark.parametrize("invalid_response", (None, "invalid"))
async def test_invalid_response(
sentry_init, aiohttp_client, capture_events, invalid_response
):
sentry_init(integrations=[AioHttpIntegration()])

async def handler(_):
return invalid_response

app = web.Application()
app.router.add_get("/", handler)

client = await aiohttp_client(app)

# Invalid response should result on a ServerDisconnectedError in the client side, not an internal server error.
# Important to note that the ServerDisconnectedError indicates we have no error server-side.
with pytest.raises(ServerDisconnectedError):
await client.get("/")
28 changes: 27 additions & 1 deletion tests/integrations/django/test_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
import re
import pytest
from functools import partial
from unittest.mock import patch

from werkzeug.test import Client

from django import VERSION as DJANGO_VERSION
from django.contrib.auth.models import User
from django.core.management import execute_from_command_line
from django.db.utils import OperationalError, ProgrammingError, DataError
from django.http.request import RawPostDataException

try:
from django.urls import reverse
Expand All @@ -20,7 +22,11 @@
from sentry_sdk._compat import PY310
from sentry_sdk import capture_message, capture_exception
from sentry_sdk.consts import SPANDATA
from sentry_sdk.integrations.django import DjangoIntegration, _set_db_data
from sentry_sdk.integrations.django import (
DjangoIntegration,
DjangoRequestExtractor,
_set_db_data,
)
from sentry_sdk.integrations.django.signals_handlers import _get_receiver_name
from sentry_sdk.integrations.executing import ExecutingIntegration
from sentry_sdk.tracing import Span
Expand Down Expand Up @@ -740,6 +746,26 @@ def test_read_request(sentry_init, client, capture_events):
assert "data" not in event["request"]


def test_request_body_already_read(sentry_init, client, capture_events):
sentry_init(integrations=[DjangoIntegration()])

events = capture_events()

class MockExtractor(DjangoRequestExtractor):
def raw_data(self):
raise RawPostDataException

with patch("sentry_sdk.integrations.django.DjangoRequestExtractor", MockExtractor):
client.post(
reverse("post_echo"), data=b'{"hey": 42}', content_type="application/json"
)

(event,) = events

assert event["message"] == "hi"
assert "data" not in event["request"]


def test_template_tracing_meta(sentry_init, client, capture_events):
sentry_init(integrations=[DjangoIntegration()])
events = capture_events()
Expand Down
39 changes: 24 additions & 15 deletions tests/test_basics.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import pytest
from sentry_sdk.client import Client
from sentry_sdk.utils import datetime_from_isoformat
from tests.conftest import patch_start_tracing_child

import sentry_sdk
Expand Down Expand Up @@ -397,11 +398,12 @@ def test_breadcrumbs(sentry_init, capture_events):
def test_breadcrumb_ordering(sentry_init, capture_events):
sentry_init()
events = capture_events()
now = datetime.datetime.now(datetime.timezone.utc).replace(microsecond=0)

timestamps = [
datetime.datetime.now() - datetime.timedelta(days=10),
datetime.datetime.now() - datetime.timedelta(days=8),
datetime.datetime.now() - datetime.timedelta(days=12),
now - datetime.timedelta(days=10),
now - datetime.timedelta(days=8),
now - datetime.timedelta(days=12),
]

for timestamp in timestamps:
Expand All @@ -417,41 +419,48 @@ def test_breadcrumb_ordering(sentry_init, capture_events):

assert len(event["breadcrumbs"]["values"]) == len(timestamps)
timestamps_from_event = [
datetime.datetime.strptime(
x["timestamp"].replace("Z", ""), "%Y-%m-%dT%H:%M:%S.%f"
)
for x in event["breadcrumbs"]["values"]
datetime_from_isoformat(x["timestamp"]) for x in event["breadcrumbs"]["values"]
]
assert timestamps_from_event == sorted(timestamps)


def test_breadcrumb_ordering_different_types(sentry_init, capture_events):
sentry_init()
events = capture_events()
now = datetime.datetime.now(datetime.timezone.utc)

timestamps = [
datetime.datetime.now() - datetime.timedelta(days=10),
datetime.datetime.now() - datetime.timedelta(days=8),
datetime.datetime.now() - datetime.timedelta(days=12),
now - datetime.timedelta(days=10),
now - datetime.timedelta(days=8),
now.replace(microsecond=0) - datetime.timedelta(days=12),
now - datetime.timedelta(days=9),
now - datetime.timedelta(days=13),
now.replace(microsecond=0) - datetime.timedelta(days=11),
]

breadcrumb_timestamps = [
timestamps[0],
timestamps[1].isoformat(),
datetime.datetime.strftime(timestamps[2], "%Y-%m-%dT%H:%M:%S") + "Z",
datetime.datetime.strftime(timestamps[3], "%Y-%m-%dT%H:%M:%S.%f") + "+00:00",
datetime.datetime.strftime(timestamps[4], "%Y-%m-%dT%H:%M:%S.%f") + "+0000",
datetime.datetime.strftime(timestamps[5], "%Y-%m-%dT%H:%M:%S.%f") + "-0000",
]

for i, timestamp in enumerate(timestamps):
add_breadcrumb(
message="Authenticated at %s" % timestamp,
category="auth",
level="info",
timestamp=timestamp if i % 2 == 0 else timestamp.isoformat(),
timestamp=breadcrumb_timestamps[i],
)

capture_exception(ValueError())
(event,) = events

assert len(event["breadcrumbs"]["values"]) == len(timestamps)
timestamps_from_event = [
datetime.datetime.strptime(
x["timestamp"].replace("Z", ""), "%Y-%m-%dT%H:%M:%S.%f"
)
for x in event["breadcrumbs"]["values"]
datetime_from_isoformat(x["timestamp"]) for x in event["breadcrumbs"]["values"]
]
assert timestamps_from_event == sorted(timestamps)

Expand Down
50 changes: 50 additions & 0 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from sentry_sdk.utils import (
Components,
Dsn,
datetime_from_isoformat,
env_to_bool,
format_timestamp,
get_current_thread_meta,
Expand Down Expand Up @@ -61,6 +62,55 @@ def _normalize_distribution_name(name):
return re.sub(r"[-_.]+", "-", name).lower()


@pytest.mark.parametrize(
("input_str", "expected_output"),
(
(
"2021-01-01T00:00:00.000000Z",
datetime(2021, 1, 1, tzinfo=timezone.utc),
), # UTC time
(
"2021-01-01T00:00:00.000000",
datetime(2021, 1, 1, tzinfo=datetime.now().astimezone().tzinfo),
), # No TZ -- assume UTC
(
"2021-01-01T00:00:00Z",
datetime(2021, 1, 1, tzinfo=timezone.utc),
), # UTC - No milliseconds
(
"2021-01-01T00:00:00.000000+00:00",
datetime(2021, 1, 1, tzinfo=timezone.utc),
),
(
"2021-01-01T00:00:00.000000-00:00",
datetime(2021, 1, 1, tzinfo=timezone.utc),
),
(
"2021-01-01T00:00:00.000000+0000",
datetime(2021, 1, 1, tzinfo=timezone.utc),
),
(
"2021-01-01T00:00:00.000000-0000",
datetime(2021, 1, 1, tzinfo=timezone.utc),
),
(
"2020-12-31T00:00:00.000000+02:00",
datetime(2020, 12, 31, tzinfo=timezone(timedelta(hours=2))),
), # UTC+2 time
(
"2020-12-31T00:00:00.000000-0200",
datetime(2020, 12, 31, tzinfo=timezone(timedelta(hours=-2))),
), # UTC-2 time
(
"2020-12-31T00:00:00-0200",
datetime(2020, 12, 31, tzinfo=timezone(timedelta(hours=-2))),
), # UTC-2 time - no milliseconds
),
)
def test_datetime_from_isoformat(input_str, expected_output):
assert datetime_from_isoformat(input_str) == expected_output, input_str


@pytest.mark.parametrize(
"env_var_value,strict,expected",
[
Expand Down

0 comments on commit 0196f36

Please sign in to comment.