diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index a643cfa9c5..28fe89febe 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -33,6 +33,7 @@ capture_internal_exception, capture_internal_exceptions, ContextVar, + datetime_from_isoformat, disable_capture_event, event_from_exception, exc_info_from_error, @@ -1264,7 +1265,7 @@ def _apply_breadcrumbs_to_event(self, event, hint, options): try: for crumb in event["breadcrumbs"]["values"]: if isinstance(crumb["timestamp"], str): - crumb["timestamp"] = datetime.fromisoformat(crumb["timestamp"]) + crumb["timestamp"] = datetime_from_isoformat(crumb["timestamp"]) event["breadcrumbs"]["values"].sort(key=lambda crumb: crumb["timestamp"]) except Exception as err: diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index 8b45f555ae..16dfd9c4fa 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -1929,3 +1929,28 @@ def _serialize_span_attribute(value): return str(value) except Exception: return None + + +ISO_TZ_SEPARATORS = frozenset(("+", "-")) + + +def datetime_from_isoformat(value): + # type: (str) -> datetime + try: + result = datetime.fromisoformat(value) + except (AttributeError, ValueError): + # py 3.6 + 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) diff --git a/tests/test_basics.py b/tests/test_basics.py index 749d31d7d3..3c05f9848a 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -32,7 +32,7 @@ from sentry_sdk.integrations.logging import LoggingIntegration 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 +from sentry_sdk.utils import datetime_from_isoformat, get_sdk_name, reraise from sentry_sdk.tracing_utils import has_tracing_enabled @@ -348,7 +348,7 @@ def test_breadcrumb_ordering(sentry_init, capture_events): assert len(event["breadcrumbs"]["values"]) == len(timestamps) timestamps_from_event = [ - datetime.fromisoformat(x["timestamp"]) for x in event["breadcrumbs"]["values"] + datetime_from_isoformat(x["timestamp"]) for x in event["breadcrumbs"]["values"] ] assert timestamps_from_event == sorted(timestamps) @@ -389,7 +389,7 @@ def test_breadcrumb_ordering_different_types(sentry_init, capture_events): assert len(event["breadcrumbs"]["values"]) == len(timestamps) timestamps_from_event = [ - datetime.fromisoformat(x["timestamp"]) 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 5011662f05..2eab252573 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, @@ -933,3 +934,52 @@ def __str__(self): ) def test_serialize_span_attribute(value, result): assert _serialize_span_attribute(value) == result + + +@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 \ No newline at end of file