fix: use local timezone in console log timestamps

formatConsoleTimestamp previously used Date.toISOString() which always
returns UTC time (suffixed with Z). This confused users whose local
timezone differs from UTC.

Now uses local time methods (getHours, getMinutes, etc.) and appends the
local UTC offset (e.g. +08:00) instead of Z. The pretty style returns
local HH:MM:SS. The hasTimestampPrefix regex is updated to accept both
Z and +/-HH:MM offset suffixes.

Closes #14699
This commit is contained in:
Elonito
2026-02-13 00:08:43 +08:00
committed by Peter Steinberger
parent af172742a3
commit 468414cac4
3 changed files with 64 additions and 6 deletions

View File

@@ -86,7 +86,10 @@ describe("enableConsoleCapture", () => {
console.warn("[EventQueue] Slow listener detected");
expect(warn).toHaveBeenCalledTimes(1);
const firstArg = String(warn.mock.calls[0]?.[0] ?? "");
expect(firstArg.startsWith("2026-01-17T18:01:02.000Z [EventQueue]")).toBe(true);
// Timestamp uses local time with timezone offset instead of UTC "Z" suffix
expect(firstArg).toMatch(
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}[+-]\d{2}:\d{2} \[EventQueue\]/,
);
vi.useRealTimers();
});

View File

@@ -0,0 +1,39 @@
import { describe, expect, it } from "vitest";
import { formatConsoleTimestamp } from "./console.js";
describe("formatConsoleTimestamp", () => {
it("pretty style returns local HH:MM:SS", () => {
const result = formatConsoleTimestamp("pretty");
expect(result).toMatch(/^\d{2}:\d{2}:\d{2}$/);
// Verify it uses local time, not UTC
const now = new Date();
const expectedHour = String(now.getHours()).padStart(2, "0");
expect(result.slice(0, 2)).toBe(expectedHour);
});
it("compact style returns local ISO-like timestamp with timezone offset", () => {
const result = formatConsoleTimestamp("compact");
// Should match: YYYY-MM-DDTHH:MM:SS.mmm+HH:MM or -HH:MM
expect(result).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}[+-]\d{2}:\d{2}$/);
// Should NOT end with Z (UTC indicator)
expect(result).not.toMatch(/Z$/);
});
it("json style returns local ISO-like timestamp with timezone offset", () => {
const result = formatConsoleTimestamp("json");
expect(result).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}[+-]\d{2}:\d{2}$/);
expect(result).not.toMatch(/Z$/);
});
it("timestamp contains the correct local date components", () => {
const before = new Date();
const result = formatConsoleTimestamp("compact");
const after = new Date();
// The date portion should match the local date
const datePart = result.slice(0, 10);
const beforeDate = `${before.getFullYear()}-${String(before.getMonth() + 1).padStart(2, "0")}-${String(before.getDate()).padStart(2, "0")}`;
const afterDate = `${after.getFullYear()}-${String(after.getMonth() + 1).padStart(2, "0")}-${String(after.getDate()).padStart(2, "0")}`;
// Allow for date boundary crossing during test
expect([beforeDate, afterDate]).toContain(datePart);
});
});

View File

@@ -135,16 +135,32 @@ function isEpipeError(err: unknown): boolean {
return code === "EPIPE" || code === "EIO";
}
function formatConsoleTimestamp(style: ConsoleStyle): string {
const now = new Date().toISOString();
export function formatConsoleTimestamp(style: ConsoleStyle): string {
const now = new Date();
if (style === "pretty") {
return now.slice(11, 19);
const h = String(now.getHours()).padStart(2, "0");
const m = String(now.getMinutes()).padStart(2, "0");
const s = String(now.getSeconds()).padStart(2, "0");
return `${h}:${m}:${s}`;
}
return now;
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, "0");
const day = String(now.getDate()).padStart(2, "0");
const h = String(now.getHours()).padStart(2, "0");
const m = String(now.getMinutes()).padStart(2, "0");
const s = String(now.getSeconds()).padStart(2, "0");
const ms = String(now.getMilliseconds()).padStart(3, "0");
const tzOffset = now.getTimezoneOffset();
const tzSign = tzOffset <= 0 ? "+" : "-";
const tzHours = String(Math.floor(Math.abs(tzOffset) / 60)).padStart(2, "0");
const tzMinutes = String(Math.abs(tzOffset) % 60).padStart(2, "0");
return `${year}-${month}-${day}T${h}:${m}:${s}.${ms}${tzSign}${tzHours}:${tzMinutes}`;
}
function hasTimestampPrefix(value: string): boolean {
return /^(?:\d{2}:\d{2}:\d{2}|\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z?)/.test(value);
return /^(?:\d{2}:\d{2}:\d{2}|\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})?)/.test(
value,
);
}
function isJsonPayload(value: string): boolean {