Skip to content

Commit a545ec0

Browse files
authored
Properly sort breadcrumbs (#3864)
1 parent fdb5cdc commit a545ec0

File tree

4 files changed

+80
-4
lines changed

4 files changed

+80
-4
lines changed

sentry_sdk/scope.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
capture_internal_exception,
3434
capture_internal_exceptions,
3535
ContextVar,
36+
datetime_from_isoformat,
3637
disable_capture_event,
3738
event_from_exception,
3839
exc_info_from_error,
@@ -1264,7 +1265,7 @@ def _apply_breadcrumbs_to_event(self, event, hint, options):
12641265
try:
12651266
for crumb in event["breadcrumbs"]["values"]:
12661267
if isinstance(crumb["timestamp"], str):
1267-
crumb["timestamp"] = datetime.fromisoformat(crumb["timestamp"])
1268+
crumb["timestamp"] = datetime_from_isoformat(crumb["timestamp"])
12681269

12691270
event["breadcrumbs"]["values"].sort(key=lambda crumb: crumb["timestamp"])
12701271
except Exception as err:

sentry_sdk/utils.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1929,3 +1929,28 @@ def _serialize_span_attribute(value):
19291929
return str(value)
19301930
except Exception:
19311931
return None
1932+
1933+
1934+
ISO_TZ_SEPARATORS = frozenset(("+", "-"))
1935+
1936+
1937+
def datetime_from_isoformat(value):
1938+
# type: (str) -> datetime
1939+
try:
1940+
result = datetime.fromisoformat(value)
1941+
except (AttributeError, ValueError):
1942+
# py 3.6
1943+
timestamp_format = (
1944+
"%Y-%m-%dT%H:%M:%S.%f" if "." in value else "%Y-%m-%dT%H:%M:%S"
1945+
)
1946+
if value.endswith("Z"):
1947+
value = value[:-1] + "+0000"
1948+
1949+
if value[-6] in ISO_TZ_SEPARATORS:
1950+
timestamp_format += "%z"
1951+
value = value[:-3] + value[-2:]
1952+
elif value[-5] in ISO_TZ_SEPARATORS:
1953+
timestamp_format += "%z"
1954+
1955+
result = datetime.strptime(value, timestamp_format)
1956+
return result.astimezone(timezone.utc)

tests/test_basics.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
from sentry_sdk.integrations.logging import LoggingIntegration
3333
from sentry_sdk.integrations.stdlib import StdlibIntegration
3434
from sentry_sdk.scope import add_global_event_processor
35-
from sentry_sdk.utils import get_sdk_name, reraise
35+
from sentry_sdk.utils import datetime_from_isoformat, get_sdk_name, reraise
3636
from sentry_sdk.tracing_utils import has_tracing_enabled
3737

3838

@@ -348,7 +348,7 @@ def test_breadcrumb_ordering(sentry_init, capture_events):
348348

349349
assert len(event["breadcrumbs"]["values"]) == len(timestamps)
350350
timestamps_from_event = [
351-
datetime.fromisoformat(x["timestamp"]) for x in event["breadcrumbs"]["values"]
351+
datetime_from_isoformat(x["timestamp"]) for x in event["breadcrumbs"]["values"]
352352
]
353353
assert timestamps_from_event == sorted(timestamps)
354354

@@ -389,7 +389,7 @@ def test_breadcrumb_ordering_different_types(sentry_init, capture_events):
389389

390390
assert len(event["breadcrumbs"]["values"]) == len(timestamps)
391391
timestamps_from_event = [
392-
datetime.fromisoformat(x["timestamp"]) for x in event["breadcrumbs"]["values"]
392+
datetime_from_isoformat(x["timestamp"]) for x in event["breadcrumbs"]["values"]
393393
]
394394
assert timestamps_from_event == sorted(timestamps)
395395

tests/test_utils.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from sentry_sdk.utils import (
1313
Components,
1414
Dsn,
15+
datetime_from_isoformat,
1516
env_to_bool,
1617
format_timestamp,
1718
get_current_thread_meta,
@@ -933,3 +934,52 @@ def __str__(self):
933934
)
934935
def test_serialize_span_attribute(value, result):
935936
assert _serialize_span_attribute(value) == result
937+
938+
939+
@pytest.mark.parametrize(
940+
("input_str", "expected_output"),
941+
(
942+
(
943+
"2021-01-01T00:00:00.000000Z",
944+
datetime(2021, 1, 1, tzinfo=timezone.utc),
945+
), # UTC time
946+
(
947+
"2021-01-01T00:00:00.000000",
948+
datetime(2021, 1, 1, tzinfo=datetime.now().astimezone().tzinfo),
949+
), # No TZ -- assume UTC
950+
(
951+
"2021-01-01T00:00:00Z",
952+
datetime(2021, 1, 1, tzinfo=timezone.utc),
953+
), # UTC - No milliseconds
954+
(
955+
"2021-01-01T00:00:00.000000+00:00",
956+
datetime(2021, 1, 1, tzinfo=timezone.utc),
957+
),
958+
(
959+
"2021-01-01T00:00:00.000000-00:00",
960+
datetime(2021, 1, 1, tzinfo=timezone.utc),
961+
),
962+
(
963+
"2021-01-01T00:00:00.000000+0000",
964+
datetime(2021, 1, 1, tzinfo=timezone.utc),
965+
),
966+
(
967+
"2021-01-01T00:00:00.000000-0000",
968+
datetime(2021, 1, 1, tzinfo=timezone.utc),
969+
),
970+
(
971+
"2020-12-31T00:00:00.000000+02:00",
972+
datetime(2020, 12, 31, tzinfo=timezone(timedelta(hours=2))),
973+
), # UTC+2 time
974+
(
975+
"2020-12-31T00:00:00.000000-0200",
976+
datetime(2020, 12, 31, tzinfo=timezone(timedelta(hours=-2))),
977+
), # UTC-2 time
978+
(
979+
"2020-12-31T00:00:00-0200",
980+
datetime(2020, 12, 31, tzinfo=timezone(timedelta(hours=-2))),
981+
), # UTC-2 time - no milliseconds
982+
),
983+
)
984+
def test_datetime_from_isoformat(input_str, expected_output):
985+
assert datetime_from_isoformat(input_str) == expected_output, input_str

0 commit comments

Comments
 (0)