diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index adae8dc888..0c0482904e 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -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] """ @@ -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): diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index 38ab7e3618..44cb98bfed 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -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): diff --git a/tests/test_basics.py b/tests/test_basics.py index 6f77353c8a..74dfe1955a 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -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 @@ -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: @@ -417,10 +419,7 @@ 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) @@ -428,11 +427,24 @@ def test_breadcrumb_ordering(sentry_init, capture_events): 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): @@ -440,7 +452,7 @@ def test_breadcrumb_ordering_different_types(sentry_init, capture_events): 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()) @@ -448,10 +460,7 @@ def test_breadcrumb_ordering_different_types(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) diff --git a/tests/test_utils.py b/tests/test_utils.py index 4df343a357..c46cac7f9f 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -12,6 +12,7 @@ from sentry_sdk.utils import ( Components, Dsn, + datetime_from_isoformat, env_to_bool, format_timestamp, get_current_thread_meta, @@ -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", [