Skip to content

Commit 26b86a5

Browse files
BYKantonpirkersentrivana
authored
fix: Fix breadcrumb timestamp casting and its tests (getsentry#3546)
These tests were failing for me locally as the timestamps were without tzinfo and all were assumed UTC whereas my local timezone is BST at the moment. This patch fixes the tests along with faulty/incomplete breadcrumb timestamp parsing logic on py3.7 and py3.8. --------- Co-authored-by: Anton Pirker <anton.pirker@sentry.io> Co-authored-by: Ivana Kellyer <ivana.kellyer@sentry.io>
1 parent 25ab10c commit 26b86a5

File tree

4 files changed

+96
-20
lines changed

4 files changed

+96
-20
lines changed

sentry_sdk/scope.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -968,7 +968,7 @@ def start_transaction(
968968
transaction=None,
969969
instrumenter=INSTRUMENTER.SENTRY,
970970
custom_sampling_context=None,
971-
**kwargs
971+
**kwargs,
972972
):
973973
# type: (Optional[Transaction], str, Optional[SamplingContext], Unpack[TransactionKwargs]) -> Union[Transaction, NoOpSpan]
974974
"""
@@ -1324,7 +1324,8 @@ def _apply_breadcrumbs_to_event(self, event, hint, options):
13241324
crumb["timestamp"] = datetime_from_isoformat(crumb["timestamp"])
13251325

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

13301331
def _apply_user_to_event(self, event, hint, options):

sentry_sdk/utils.py

+19-3
Original file line numberDiff line numberDiff line change
@@ -239,13 +239,29 @@ def format_timestamp(value):
239239
return utctime.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
240240

241241

242+
ISO_TZ_SEPARATORS = frozenset(("+", "-"))
243+
244+
242245
def datetime_from_isoformat(value):
243246
# type: (str) -> datetime
244247
try:
245-
return datetime.fromisoformat(value)
246-
except AttributeError:
248+
result = datetime.fromisoformat(value)
249+
except (AttributeError, ValueError):
247250
# py 3.6
248-
return datetime.strptime(value, "%Y-%m-%dT%H:%M:%S.%f")
251+
timestamp_format = (
252+
"%Y-%m-%dT%H:%M:%S.%f" if "." in value else "%Y-%m-%dT%H:%M:%S"
253+
)
254+
if value.endswith("Z"):
255+
value = value[:-1] + "+0000"
256+
257+
if value[-6] in ISO_TZ_SEPARATORS:
258+
timestamp_format += "%z"
259+
value = value[:-3] + value[-2:]
260+
elif value[-5] in ISO_TZ_SEPARATORS:
261+
timestamp_format += "%z"
262+
263+
result = datetime.strptime(value, timestamp_format)
264+
return result.astimezone(timezone.utc)
249265

250266

251267
def event_hint_with_exc_info(exc_info=None):

tests/test_basics.py

+24-15
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import pytest
1010
from sentry_sdk.client import Client
11+
from sentry_sdk.utils import datetime_from_isoformat
1112
from tests.conftest import patch_start_tracing_child
1213

1314
import sentry_sdk
@@ -397,11 +398,12 @@ def test_breadcrumbs(sentry_init, capture_events):
397398
def test_breadcrumb_ordering(sentry_init, capture_events):
398399
sentry_init()
399400
events = capture_events()
401+
now = datetime.datetime.now(datetime.timezone.utc).replace(microsecond=0)
400402

401403
timestamps = [
402-
datetime.datetime.now() - datetime.timedelta(days=10),
403-
datetime.datetime.now() - datetime.timedelta(days=8),
404-
datetime.datetime.now() - datetime.timedelta(days=12),
404+
now - datetime.timedelta(days=10),
405+
now - datetime.timedelta(days=8),
406+
now - datetime.timedelta(days=12),
405407
]
406408

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

418420
assert len(event["breadcrumbs"]["values"]) == len(timestamps)
419421
timestamps_from_event = [
420-
datetime.datetime.strptime(
421-
x["timestamp"].replace("Z", ""), "%Y-%m-%dT%H:%M:%S.%f"
422-
)
423-
for x in event["breadcrumbs"]["values"]
422+
datetime_from_isoformat(x["timestamp"]) for x in event["breadcrumbs"]["values"]
424423
]
425424
assert timestamps_from_event == sorted(timestamps)
426425

427426

428427
def test_breadcrumb_ordering_different_types(sentry_init, capture_events):
429428
sentry_init()
430429
events = capture_events()
430+
now = datetime.datetime.now(datetime.timezone.utc)
431431

432432
timestamps = [
433-
datetime.datetime.now() - datetime.timedelta(days=10),
434-
datetime.datetime.now() - datetime.timedelta(days=8),
435-
datetime.datetime.now() - datetime.timedelta(days=12),
433+
now - datetime.timedelta(days=10),
434+
now - datetime.timedelta(days=8),
435+
now.replace(microsecond=0) - datetime.timedelta(days=12),
436+
now - datetime.timedelta(days=9),
437+
now - datetime.timedelta(days=13),
438+
now.replace(microsecond=0) - datetime.timedelta(days=11),
439+
]
440+
441+
breadcrumb_timestamps = [
442+
timestamps[0],
443+
timestamps[1].isoformat(),
444+
datetime.datetime.strftime(timestamps[2], "%Y-%m-%dT%H:%M:%S") + "Z",
445+
datetime.datetime.strftime(timestamps[3], "%Y-%m-%dT%H:%M:%S.%f") + "+00:00",
446+
datetime.datetime.strftime(timestamps[4], "%Y-%m-%dT%H:%M:%S.%f") + "+0000",
447+
datetime.datetime.strftime(timestamps[5], "%Y-%m-%dT%H:%M:%S.%f") + "-0000",
436448
]
437449

438450
for i, timestamp in enumerate(timestamps):
439451
add_breadcrumb(
440452
message="Authenticated at %s" % timestamp,
441453
category="auth",
442454
level="info",
443-
timestamp=timestamp if i % 2 == 0 else timestamp.isoformat(),
455+
timestamp=breadcrumb_timestamps[i],
444456
)
445457

446458
capture_exception(ValueError())
447459
(event,) = events
448460

449461
assert len(event["breadcrumbs"]["values"]) == len(timestamps)
450462
timestamps_from_event = [
451-
datetime.datetime.strptime(
452-
x["timestamp"].replace("Z", ""), "%Y-%m-%dT%H:%M:%S.%f"
453-
)
454-
for x in event["breadcrumbs"]["values"]
463+
datetime_from_isoformat(x["timestamp"]) for x in event["breadcrumbs"]["values"]
455464
]
456465
assert timestamps_from_event == sorted(timestamps)
457466

tests/test_utils.py

+50
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,
@@ -61,6 +62,55 @@ def _normalize_distribution_name(name):
6162
return re.sub(r"[-_.]+", "-", name).lower()
6263

6364

65+
@pytest.mark.parametrize(
66+
("input_str", "expected_output"),
67+
(
68+
(
69+
"2021-01-01T00:00:00.000000Z",
70+
datetime(2021, 1, 1, tzinfo=timezone.utc),
71+
), # UTC time
72+
(
73+
"2021-01-01T00:00:00.000000",
74+
datetime(2021, 1, 1, tzinfo=datetime.now().astimezone().tzinfo),
75+
), # No TZ -- assume UTC
76+
(
77+
"2021-01-01T00:00:00Z",
78+
datetime(2021, 1, 1, tzinfo=timezone.utc),
79+
), # UTC - No milliseconds
80+
(
81+
"2021-01-01T00:00:00.000000+00:00",
82+
datetime(2021, 1, 1, tzinfo=timezone.utc),
83+
),
84+
(
85+
"2021-01-01T00:00:00.000000-00:00",
86+
datetime(2021, 1, 1, tzinfo=timezone.utc),
87+
),
88+
(
89+
"2021-01-01T00:00:00.000000+0000",
90+
datetime(2021, 1, 1, tzinfo=timezone.utc),
91+
),
92+
(
93+
"2021-01-01T00:00:00.000000-0000",
94+
datetime(2021, 1, 1, tzinfo=timezone.utc),
95+
),
96+
(
97+
"2020-12-31T00:00:00.000000+02:00",
98+
datetime(2020, 12, 31, tzinfo=timezone(timedelta(hours=2))),
99+
), # UTC+2 time
100+
(
101+
"2020-12-31T00:00:00.000000-0200",
102+
datetime(2020, 12, 31, tzinfo=timezone(timedelta(hours=-2))),
103+
), # UTC-2 time
104+
(
105+
"2020-12-31T00:00:00-0200",
106+
datetime(2020, 12, 31, tzinfo=timezone(timedelta(hours=-2))),
107+
), # UTC-2 time - no milliseconds
108+
),
109+
)
110+
def test_datetime_from_isoformat(input_str, expected_output):
111+
assert datetime_from_isoformat(input_str) == expected_output, input_str
112+
113+
64114
@pytest.mark.parametrize(
65115
"env_var_value,strict,expected",
66116
[

0 commit comments

Comments
 (0)