Found while fixing #14852 (service-automation's resolveToken() offset branch). ⛔ Deliberately NOT fixed there — different package, different domain, outside that card's fence. Refs #14852.
The shape
packages/drivers/driver-memory/src/memory-analytics.ts:1415 parseDateRangeString(), reached from the analytics query path at line 742 (: this.parseDateRangeString(timeDim.dateRange)):
const now = new Date();
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
if (range === 'today') {
return [today.toISOString(), new Date(today.getTime() + 86400000).toISOString()];
} else if (range.startsWith('last ')) {
...
const start = new Date(today);
if (unit.startsWith('day')) start.setDate(start.getDate() - num);
else if (unit.startsWith('week')) start.setDate(start.getDate() - num * 7);
...
return [start.toISOString(), now.toISOString()];
}
Two defects, and the first is the larger one
1. The window boundary is LOCAL midnight, rendered as UTC — wrong all year, not only across DST. new Date(y, m, d) constructs local midnight; toISOString() then renders that instant in UTC. So in any process not sitting at UTC, the 'today' window is the local day expressed as a UTC range — shifted by the zone's offset. At Asia/Shanghai the "today" bucket runs from the previous 16:00Z; at America/Los_Angeles it starts at 08:00Z. This disagrees with the rest of the platform, where a bare date resolves to the UTC day (@objectstack/core's {today} macro falls back to UTC parts when the context carries no timezone, and {TODAY()} in flow templates resolves to the UTC day — see #14852). ⇒ The same analytics question answered through this path and through a flow token can select different rows.
2. The last N days / last N weeks legs additionally mix two calendars, exactly as #14852 did: setDate(getDate() - n) is local arithmetic and toISOString() is a UTC rendering, so across a DST transition the window start slips an hour. (The month / year legs use setMonth / setFullYear, same class.)
⚠️ Defect 1 subsumes defect 2 in magnitude, but they are independent: fixing only the setDate spelling leaves the local-midnight boundary in place.
Provenance / status caveat
The function carries its own disclaimer — // Simple parser for common date range strings / // In production, this would use a proper date range parser — so this may be known scaffolding rather than a surprise. It is nevertheless reachable, not dead: the call at line 742 is on the live analytics query path. Whether the right repair is a spelling fix or replacing the parser is a call for triage; the reachability is the part worth recording.
Guard note (carried over from #14852)
⛔ A regression test for the DST half cannot be written to run only at TZ=UTC — the two spellings are indistinguishable there. Defect 1, by contrast, is visible at any non-UTC zone without needing a transition instant, so it is the cheaper of the two to pin. packages/services/service-automation/src/builtin/template-date-offset-dst.test.ts (added for #14852) is a working model for the DST half.
Unlabelled beyond pm:queue on purpose: routing, domain:*, type and priority belong to triage.
Found while fixing #14852 (
service-automation'sresolveToken()offset branch). ⛔ Deliberately NOT fixed there — different package, different domain, outside that card's fence. Refs #14852.The shape
packages/drivers/driver-memory/src/memory-analytics.ts:1415parseDateRangeString(), reached from the analytics query path at line 742 (: this.parseDateRangeString(timeDim.dateRange)):Two defects, and the first is the larger one
1. The window boundary is LOCAL midnight, rendered as UTC — wrong all year, not only across DST.
new Date(y, m, d)constructs local midnight;toISOString()then renders that instant in UTC. So in any process not sitting at UTC, the'today'window is the local day expressed as a UTC range — shifted by the zone's offset. AtAsia/Shanghaithe "today" bucket runs from the previous 16:00Z; atAmerica/Los_Angelesit starts at 08:00Z. This disagrees with the rest of the platform, where a bare date resolves to the UTC day (@objectstack/core's{today}macro falls back to UTC parts when the context carries no timezone, and{TODAY()}in flow templates resolves to the UTC day — see #14852). ⇒ The same analytics question answered through this path and through a flow token can select different rows.2. The
last N days/last N weekslegs additionally mix two calendars, exactly as #14852 did:setDate(getDate() - n)is local arithmetic andtoISOString()is a UTC rendering, so across a DST transition the window start slips an hour. (Themonth/yearlegs usesetMonth/setFullYear, same class.)setDatespelling leaves the local-midnight boundary in place.Provenance / status caveat
The function carries its own disclaimer —
// Simple parser for common date range strings/// In production, this would use a proper date range parser— so this may be known scaffolding rather than a surprise. It is nevertheless reachable, not dead: the call at line 742 is on the live analytics query path. Whether the right repair is a spelling fix or replacing the parser is a call for triage; the reachability is the part worth recording.Guard note (carried over from #14852)
⛔ A regression test for the DST half cannot be written to run only at
TZ=UTC— the two spellings are indistinguishable there. Defect 1, by contrast, is visible at any non-UTC zone without needing a transition instant, so it is the cheaper of the two to pin.packages/services/service-automation/src/builtin/template-date-offset-dst.test.ts(added for #14852) is a working model for the DST half.Unlabelled beyond
pm:queueon purpose: routing,domain:*, type and priority belong to triage.