Skip to content

Commit d7669f9

Browse files
fix: Fix non-UTC timestamps
Fixes a bug where all `datetime` timestamps in an event payload were serialized as if they were UTC timestamps, even if they were non-UTC timestamps, completely ignoring the timezone. Now, we respect the timezone by setting the timezone offset if it is present. Fixes #3453.
1 parent 269d96d commit d7669f9

File tree

2 files changed

+39
-3
lines changed

2 files changed

+39
-3
lines changed

sentry_sdk/utils.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import threading
1212
import time
1313
from collections import namedtuple
14-
from datetime import datetime
14+
from datetime import datetime, timezone
1515
from decimal import Decimal
1616
from functools import partial, partialmethod, wraps
1717
from numbers import Real
@@ -226,8 +226,13 @@ def to_timestamp(value):
226226

227227

228228
def format_timestamp(value):
229+
"""Formats a timestamp in RFC 3339 format.
230+
231+
Any datetime objects with a non-UTC timezone are converted to UTC, so that all timestamps are formatted in UTC.
232+
"""
229233
# type: (datetime) -> str
230-
return value.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
234+
utctime = value.astimezone(timezone.utc)
235+
return utctime.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
231236

232237

233238
def event_hint_with_exc_info(exc_info=None):

tests/test_utils.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import threading
22
import re
33
import sys
4-
from datetime import timedelta
4+
from datetime import timedelta, datetime
55
from unittest import mock
66

77
import pytest
@@ -13,6 +13,7 @@
1313
Components,
1414
Dsn,
1515
env_to_bool,
16+
format_timestamp,
1617
get_current_thread_meta,
1718
get_default_release,
1819
get_error_message,
@@ -950,3 +951,33 @@ def target():
950951
thread.start()
951952
thread.join()
952953
assert (main_thread.ident, main_thread.name) == results.get(timeout=1)
954+
955+
956+
@pytest.mark.parametrize(
957+
("input_timestamp", "expected_output"),
958+
(
959+
("2021-01-01T12:00:00Z", "2021-01-01T12:00:00.000000Z"), # UTC time
960+
(
961+
"2021-01-01T12:00:00+02:00",
962+
"2021-01-01T10:00:00.000000Z",
963+
), # Non-UTC time
964+
(
965+
"2021-01-01T12:00:00-07:00",
966+
"2021-01-01T19:00:00.000000Z",
967+
), # Another non-UTC time
968+
),
969+
)
970+
def test_format_timestamp(input_timestamp, expected_output):
971+
datetime_object = datetime.fromisoformat(input_timestamp)
972+
formatted = format_timestamp(datetime_object)
973+
974+
assert formatted == expected_output
975+
976+
977+
def test_format_timestamp_naive():
978+
datetime_object = datetime.fromisoformat("2021-01-01T12:00:00")
979+
timestamp_regex = r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{6}Z"
980+
981+
# Ensure that some timestamp is returned, without error. We currently treat these as local time, but this is an
982+
# implementation detail which we should not assert here.
983+
assert re.fullmatch(timestamp_regex, format_timestamp(datetime_object))

0 commit comments

Comments
 (0)