Describe the bug
AlertModel.__init__ converts detected_at with detected_at.astimezone(...). The detected_at values loaded from the alerts table are naive datetimes stored in UTC, and astimezone() interprets a naive datetime as local time. As a result, when edr monitor runs on a machine whose timezone is not UTC, the "Time:" field of every alert (and formatted_detected_at in the source freshness result description) shows the UTC value labeled with the local timezone name.
In the same source freshness message, max_loaded_at goes through convert_datetime_utc_str_to_timezone_str(), which correctly treats the value as UTC. So a single message mixes two different timezones.
Real-world example (container with TZ=Asia/Tokyo, no timezone config; the check actually ran at 18:08:22 JST = 09:08:22 UTC):
Time: 2026-07-22 09:08:22 JST
When the test ran at 2026-07-22 09:08:22 JST, the most recent record found in
the table was 1 day 0h 11m 41s earlier (2026-07-21 17:55:10 JST).
Note the internal inconsistency: 09:08:22 − (17:55:10 the previous day) is 15h13m, not "1 day 0h 11m 41s". The elapsed time (computed in UTC) and max_loaded_at (correctly converted to JST) are right; only the "test ran at" timestamp is mislabeled. This misled our team into believing the alert had been delayed by 9 hours.
To Reproduce
Run the following on a machine whose timezone is not UTC (or keep the two TZ lines to force it):
import os, time
os.environ["TZ"] = "Asia/Tokyo"; time.tzset()
from datetime import datetime
from elementary.monitor.alerts.source_freshness_alert import SourceFreshnessAlertModel
alert = SourceFreshnessAlertModel(
id="x", source_name="s", identifier="t", original_status="fail",
path="p", error=None, alert_class_id="c", source_freshness_execution_id="e",
detected_at=datetime(2026, 7, 22, 9, 8, 22), # naive UTC, as loaded from the alerts table
max_loaded_at=datetime(2026, 7, 21, 8, 55, 10), # naive UTC
max_loaded_at_time_ago_in_s=87101.8,
)
print(alert.detected_at_str) # 2026-07-22 09:08:22 JST <- UTC value, JST label
print(alert.result_description) # mixes 09:08:22 "JST" with a correctly converted 17:55:10 JST
Setting the timezone config does not help while the host timezone is non-UTC: the naive value is assumed to be local, so converting local→local is a no-op.
| Host TZ |
timezone config |
"Time:" shown |
correct? |
| Asia/Tokyo |
(none) |
09:08:22 JST |
✗ |
| UTC |
(none) |
09:08:22 UTC |
✓ |
| UTC |
Asia/Tokyo |
18:08:22 JST |
✓ |
| Asia/Tokyo |
Asia/Tokyo |
09:08:22 JST |
✗ |
Expected behavior
Naive detected_at should be interpreted as UTC (matching how it is stored and how max_loaded_at is handled in the same message), so the example above renders Time: 2026-07-22 18:08:22 JST.
Screenshots
Environment (please complete the following information):
- Elementary CLI (edr) version: 0.23.4 (the relevant code is unchanged on v0.25.1 and current master)
- Elementary dbt package version: 0.25.1
- dbt version you're using: 1.11.12
- Data warehouse: snowflake
- Infrastructure details: Linux Docker container with
ENV TZ Asia/Tokyo, running edr monitor on AWS ECS Fargate (prod)
Additional context
Affects the "Time:" field of all alert types via AlertModel.detected_at_str, and additionally formatted_detected_at inside the source freshness result description. Deployments whose host timezone is UTC never see the bug, which is probably why it hasn't been reported.
Would you be willing to contribute a fix for this issue?
Yes — I have a fix ready (a few lines in elementary/monitor/alerts/alert.py interpreting naive detected_at as UTC before astimezone, plus unit tests) and will open a PR.
Describe the bug
AlertModel.__init__convertsdetected_atwithdetected_at.astimezone(...). Thedetected_atvalues loaded from the alerts table are naive datetimes stored in UTC, andastimezone()interprets a naive datetime as local time. As a result, whenedr monitorruns on a machine whose timezone is not UTC, the "Time:" field of every alert (andformatted_detected_atin the source freshness result description) shows the UTC value labeled with the local timezone name.In the same source freshness message,
max_loaded_atgoes throughconvert_datetime_utc_str_to_timezone_str(), which correctly treats the value as UTC. So a single message mixes two different timezones.Real-world example (container with
TZ=Asia/Tokyo, notimezoneconfig; the check actually ran at 18:08:22 JST = 09:08:22 UTC):Note the internal inconsistency:
09:08:22 − (17:55:10 the previous day)is 15h13m, not "1 day 0h 11m 41s". The elapsed time (computed in UTC) andmax_loaded_at(correctly converted to JST) are right; only the "test ran at" timestamp is mislabeled. This misled our team into believing the alert had been delayed by 9 hours.To Reproduce
Run the following on a machine whose timezone is not UTC (or keep the two
TZlines to force it):Setting the
timezoneconfig does not help while the host timezone is non-UTC: the naive value is assumed to be local, so converting local→local is a no-op.timezoneconfigExpected behavior
Naive
detected_atshould be interpreted as UTC (matching how it is stored and howmax_loaded_atis handled in the same message), so the example above rendersTime: 2026-07-22 18:08:22 JST.Screenshots
Environment (please complete the following information):
ENV TZ Asia/Tokyo, runningedr monitoron AWS ECS Fargate (prod)Additional context
Affects the "Time:" field of all alert types via
AlertModel.detected_at_str, and additionallyformatted_detected_atinside the source freshness result description. Deployments whose host timezone is UTC never see the bug, which is probably why it hasn't been reported.Would you be willing to contribute a fix for this issue?
Yes — I have a fix ready (a few lines in
elementary/monitor/alerts/alert.pyinterpreting naivedetected_atas UTC beforeastimezone, plus unit tests) and will open a PR.