fix: normalize SDK timestamps to UTC - #872
Conversation
posthog-python Compliance ReportDate: 2026-08-14 11:21:33 UTC ✅ All Tests Passed!111/111 tests passed Capture_V1 Tests✅ 94/94 tests passed View Details
Feature_Flags Tests✅ 17/17 tests passed View Details
|
|
Reviews (1): Last reviewed commit: "docs: clarify UTC timestamp changeset" | Re-trigger Greptile |
arnohillen
left a comment
There was a problem hiding this comment.
(created by claude code)
[arno's agent] Verified at 015688a: guess_timezone now converts aware datetimes to the UTC instant (never relabels), the 0 <= delta bound stops future naive values being stamped local, every emission path normalizes (v0 enqueue, v1 idempotent re-normalize, sent_at already UTC), and tests assert exact instants; 111/111 compliance green.
|
(created by claude code) [arno's agent] On Python 3.10 fromisoformat rejects non-canonical forms like +0530 offsets that 3.11+ parses, so the same string is normalized to UTC on 3.11+ but passed through unchanged on 3.10; the fallback is safe but the version divergence is worth a comment or test. |
|
(created by claude code) [arno's agent] _normalize_timestamp also parses date-only strings like 2026-06-27 and emits midnight UTC, a new silent coercion that no test covers. |
|
Addressed the Python-version divergence in 46e2adc: compact offsets such as |
|
Addressed the date-only coercion in 46e2adc: canonical extended and basic date-only strings are now preserved unchanged rather than converted to midnight UTC, with regression coverage. |
PR overviewAll previously flagged issues have been addressed. No open security concerns remain on this pull request. Security reviewNo open security issues remain on this pull request. Fixed/addressed: 1 · PR risk: 0/10 |
💡 Motivation and Context
SDK event timestamps could retain a non-UTC offset even when they represented the correct instant. Exception stack timestamps also added a
Zsuffix without first converting aware values to UTC. This made timestamp output inconsistent across capture paths and could describe the wrong instant for exception frames.This change converts canonical SDK timestamps to UTC. Timezone-aware datetimes and parseable ISO timestamp strings preserve their instant and are serialized with a
+00:00offset. Recent naive datetimes still use the existing local-time heuristic before conversion. Older and future naive values continue to be treated as UTC, and unparseable timestamp strings remain unchanged. Datetime values inside event properties are not modified.The v0 queue, v1 event conversion, request timestamps, and aware exception frame timestamps now use UTC consistently. The public
group_identifyandaliasannotations now document their existing support for datetime and string timestamps.💚 How did you test it?
uv run pytest -q posthog/test/test_utils.py posthog/test/test_client.py posthog/test/test_capture_v1.py posthog/test/test_exception_utils.py posthog/test/test_module.py posthog/test/test_request.pyuv run ruff format --check posthog/__init__.py posthog/args.py posthog/capture_v1.py posthog/client.py posthog/exception_utils.py posthog/utils.py posthog/test/test_capture_v1.py posthog/test/test_client.py posthog/test/test_exception_utils.py posthog/test/test_module.py posthog/test/test_request.py posthog/test/test_utils.pyuv run ruff check posthog/__init__.py posthog/args.py posthog/capture_v1.py posthog/client.py posthog/exception_utils.py posthog/utils.py posthog/test/test_capture_v1.py posthog/test/test_client.py posthog/test/test_exception_utils.py posthog/test/test_module.py posthog/test/test_request.py posthog/test/test_utils.pyuv run --with griffe python .github/scripts/check_public_api.pyuv run python -W error -c "import posthog"Regression tests verify that
+05:30and+05:45timestamps preserve their exact instant when converted to UTC, v0 and v1 capture paths emit UTC values, request timestamps have a zero offset, aware exception timestamps are converted before addingZ, future naive ISO strings are not treated as recent local time on a non-UTC host, module-level timestamp annotations accept datetime and string values, and event property datetimes remain untouched.📝 Checklist
If releasing new changes
sampo addto generate a changeset file🤖 Agent context
Autonomy: Human-driven (agent-assisted)
A Pi agent used the PR and autoreview skills to prepare this change. The final committed branch was reviewed against
origin/main. The review found that future naive strings could incorrectly use the local timezone, so the recency check was bounded and regression coverage was added before the final clean review. A public API check also identified the intentional annotation updates and confirmed that the existingposthog.client.guess_timezonealias remains available.