From c9f68754df64ec536be58359b902ccee168ef808 Mon Sep 17 00:00:00 2001 From: Shrinath Rao Date: Thu, 6 Aug 2026 09:56:06 +0530 Subject: [PATCH] fix(logstream): show local timezone in stream detail time range (#13652) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem The **Stream Detail** header shows the stream's document time range with a timezone badge that is **hardcoded to \`UTC\`** (i18n key \`logStream.utc\`), while the timestamps next to it are rendered by date-fns \`format()\` in the **browser's local timezone**. So the badge and the value disagree for any non-UTC user — the range reads as local time but is mislabeled "UTC". ## Fix - Add \`formatTimestampInTimezone(us, format, timezone)\` in \`web/src/utils/date.ts\` — same as \`formatTimestamp\` but renders in a given IANA zone (via \`date-fns-tz\` \`formatInTimeZone\`, already a dependency), matching the pattern already used across dashboards. - In \`schema.vue\`, add a \`displayTimezone\` computed = \`store.state.timezone || \` (never a hardcoded "UTC"), render it in the badge, and format the \`doc_time_min\`/\`doc_time_max\` range in that same timezone. Now the badge label and the displayed range always use the same timezone — the user's selected timezone, falling back to their browser zone. ## Scope note The separate **Extended Retention** date picker further down still uses UTC intentionally (retention day-boundaries are computed in UTC on the backend), so its \`(UTC Timezone)\` label is left unchanged. ## Validation Verified \`formatInTimeZone\` behavior against the installed \`date-fns-tz@3.2.0\`: | timezone | rendered value | |---|---| | UTC | \`2025-08-03T20:03:22:99\` | | Asia/Kolkata | \`2025-08-04T01:33:22:99\` | | America/New_York | \`2025-08-03T16:03:22:99\` | Label and value now agree in every case. --- web/src/components/logstream/schema.vue | 19 +++++++++++++++---- web/src/utils/date.ts | 10 ++++++++++ 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/web/src/components/logstream/schema.vue b/web/src/components/logstream/schema.vue index 2e6bf706b3..830a598025 100644 --- a/web/src/components/logstream/schema.vue +++ b/web/src/components/logstream/schema.vue @@ -41,7 +41,7 @@ along with this program. If not, see . - {{ t("logStream.utc") }} + {{ displayTimezone }}
{{ indexData.stats.doc_time_min }} @@ -792,7 +792,10 @@ import { computed, defineComponent, onBeforeMount, ref, watch } from "vue"; import { raw, useI18nTyped, type I18nText } from "@/types/i18n"; import { useStore } from "vuex"; import useTheme from "@/composables/useTheme"; -import { convertUnixToDateFormat as convertUnixToFormat, formatTimestamp } from "@/utils/date"; +import { + convertUnixToDateFormat as convertUnixToFormat, + formatTimestampInTimezone, +} from "@/utils/date"; import streamService from "../../services/stream"; import segment from "../../services/segment_analytics"; import { @@ -917,6 +920,11 @@ export default defineComponent({ const { t } = useI18nTyped(); const store = useStore(); const { isDark } = useTheme(); + // Timezone used for the stream-stats time range: the user's selected + // timezone, falling back to the browser's zone (never a hardcoded "UTC"). + const displayTimezone = computed( + () => store.state.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone, + ); const indexData: any = ref(defaultValue()); const updateSettingsForm: any = ref(null); const isCloud = config.isCloud; @@ -1355,13 +1363,15 @@ export default defineComponent({ indexData.value.stats.original_doc_time_max = streamResponse.stats.doc_time_max; indexData.value.stats.original_doc_time_min = streamResponse.stats.doc_time_min; - indexData.value.stats.doc_time_max = formatTimestamp( + indexData.value.stats.doc_time_max = formatTimestampInTimezone( parseInt(streamResponse.stats.doc_time_max), "YYYY-MM-DDTHH:mm:ss:SS", + displayTimezone.value, ); - indexData.value.stats.doc_time_min = formatTimestamp( + indexData.value.stats.doc_time_min = formatTimestampInTimezone( parseInt(streamResponse.stats.doc_time_min), "YYYY-MM-DDTHH:mm:ss:SS", + displayTimezone.value, ); indexData.value.defined_schema_fields = streamResponse.settings.defined_schema_fields || []; @@ -2439,6 +2449,7 @@ export default defineComponent({ t, raw, store, + displayTimezone, config, dateChangeValue, isCloud, diff --git a/web/src/utils/date.ts b/web/src/utils/date.ts index 99c7d5d423..42c18b3461 100644 --- a/web/src/utils/date.ts +++ b/web/src/utils/date.ts @@ -14,6 +14,7 @@ // along with this program. If not, see . import { format as dfFormat, sub } from "date-fns"; +import { formatInTimeZone } from "date-fns-tz"; import { DateTime as _DateTime } from "luxon"; // --------------------------------------------------------------------------- @@ -267,6 +268,15 @@ export function formatTimestamp(us: number, format: string): string { return formatDate(us / 1000, format); } +/** + * Format a microsecond timestamp in a specific IANA timezone. + * Mirrors formatTimestamp but renders in the given timezone instead of the + * browser's local zone, so the displayed value matches a timezone label. + */ +export function formatTimestampInTimezone(us: number, format: string, timezone: string): string { + return formatInTimeZone(us / 1000, timezone, normalizeFormat(format)); +} + /** * Format a nanosecond timestamp to a string. * Converts nanoseconds to milliseconds internally.