Skip to content

driver-memory analytics parseDateRangeString() builds its window on the LOCAL calendar and renders it as UTC — 'today' and 'last N days' are offset by the process timezone in every non-UTC zone #15825

Description

@os-warren

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions