Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions src/configs/mock-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1082,18 +1082,18 @@ export function calculateTeamMetricsStep(
/**
* Generate mock team metrics for monitoring charts
* Supports small, medium, and large teams with realistic patterns
* Can generate data for the past 30 days from now
* Can generate data for the past 90 days from now
*/
export function generateMockTeamMetrics(
startMs: number,
endMs: number
): { metrics: ClientTeamMetrics; step: number } {
const now = Date.now()
const thirtyDaysAgo = now - 30 * 24 * 60 * 60 * 1000
const ninetyDaysAgo = now - 90 * 24 * 60 * 60 * 1000

// Clamp start time to no earlier than 30 days ago
if (startMs < thirtyDaysAgo) {
startMs = thirtyDaysAgo
// Clamp start time to no earlier than 90 days ago
if (startMs < ninetyDaysAgo) {
startMs = ninetyDaysAgo
}

// Don't generate data beyond current time
Expand Down
2 changes: 1 addition & 1 deletion src/features/dashboard/sandboxes/monitoring/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export default function SandboxesMonitoringHeader({
<BaseSubtitle>
Peak Concurrent Sandboxes
<br className="max-md:hidden" />
<span className="max-md:hidden">(30-day max)</span>
<span className="max-md:hidden">(90-day max)</span>
</BaseSubtitle>
</BaseCard>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
import { TIME_RANGES, type TimeRangeKey } from '@/lib/utils/timeframe'
import { calculateIsLive } from '../utils'

const MAX_DAYS_AGO = 31 * 24 * 60 * 60 * 1000
const MAX_DAYS_AGO = 90 * 24 * 60 * 60 * 1000
const MIN_RANGE_MS = 1.5 * 60 * 1000

const getStableNow = () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,17 @@ export const TIME_OPTIONS: TimeOption[] = [
shortcut: '30D',
rangeMs: 30 * 24 * 60 * 60 * 1000,
},
{
label: `Last 90 days`,
value: '90d',
shortcut: '90D',
rangeMs: 90 * 24 * 60 * 60 * 1000,
},
]

// constraints
export const MAX_DAYS_AGO = 31 * 24 * 60 * 60 * 1000 // 31 days in ms
export const MAX_DAYS_AGO = 90 * 24 * 60 * 60 * 1000 // 90 days in ms
export const MAX_DAYS_AGO_BUFFER = MAX_DAYS_AGO + 60 * 1000 // validation buffer to prevent preset race conditions
export const MIN_RANGE_MS = 1.5 * 60 * 1000 // 1.5 minutes minimum
export const CLOCK_SKEW_TOLERANCE = 60 * 1000 // 60 seconds
export const DEFAULT_RANGE_MS = 60 * 60 * 1000 // 1 hour default
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ export const TimePanel = forwardRef<TimePanelRef, TimePanelProps>(
const { minDate, maxDate } = useMemo(() => {
const now = new Date()

// create new Date object for minDate (31 days ago)
// create new Date object for minDate (MAX_DAYS_AGO ago)
const minDate = new Date(now.getTime() - MAX_DAYS_AGO)
minDate.setHours(0, 0, 0, 0)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@

import { z } from 'zod'
import { combineDateTimeStrings } from '@/lib/utils/formatting'
import { CLOCK_SKEW_TOLERANCE, MAX_DAYS_AGO, MIN_RANGE_MS } from './constants'
import {
CLOCK_SKEW_TOLERANCE,
MAX_DAYS_AGO,
MAX_DAYS_AGO_BUFFER,
MIN_RANGE_MS,
} from './constants'

export const customTimeFormSchema = z
.object({
Expand Down Expand Up @@ -39,11 +44,11 @@ export const customTimeFormSchema = z
const now = Date.now()
const startTimestamp = startDateTime.getTime()

// validate start date is not more than 31 days ago
if (startTimestamp < now - MAX_DAYS_AGO) {
// validate start date is not more than MAX_DAYS_AGO
if (startTimestamp < now - MAX_DAYS_AGO_BUFFER) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Start date cannot be more than 31 days ago',
message: `Start date cannot be more than ${MAX_DAYS_AGO / (1000 * 60 * 60 * 24)} days ago`,
path: ['startDate'],
})
return
Comment on lines 44 to 54
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 The validation error messages now reference "91 days" (Start date cannot be more than 91 days ago / Date range cannot exceed 91 days) but the UI advertises 90 days everywhere — the new "Last 90 days" preset in TIME_OPTIONS/TIME_RANGES and the header card's "(90-day max)" label. A user who selects "Last 90 days" and nudges the start a touch earlier in the custom picker will see an error referencing a 91-day limit the UI never promised. The same drift propagates server-side because get-team-metrics.ts:29, get-team-metrics-max.ts:32, schemas.ts:18,36, and app/api/teams/[teamSlug]/metrics/types.ts:18,35 interpolate MAX_DAYS_AGO / 86_400_000 (= 91) into their error text. Either hardcode "90 days" in user-facing messages and keep MAX_DAYS_AGO as the internal clock-skew buffer, or tighten MAX_DAYS_AGO to 90 days and add a separate small buffer constant (the existing CLOCK_SKEW_TOLERANCE = 60_000 already covers clock skew).

Extended reasoning...

What the bug is

The PR introduces a "Last 90 days" preset and updates the header card to read "(90-day max)" — the user-visible product surface promises 90 days. Internally, however, MAX_DAYS_AGO is set to 91 days (apparently a 1-day safety buffer over the advertised 90), and the validation error strings in validation.ts were rewritten to literally say 91 days:

// src/features/dashboard/sandboxes/monitoring/time-picker/validation.ts:46
message: 'Start date cannot be more than 91 days ago',
// :111
message: 'Date range cannot exceed 91 days',

The same 91 leaks into server-side error toasts because several schemas build their messages by dividing MAX_DAYS_AGO by 86_400_000:

  • src/core/server/functions/sandboxes/get-team-metrics.ts:29 and :46
  • src/core/server/functions/sandboxes/get-team-metrics-max.ts:32 and :50
  • src/core/modules/sandboxes/schemas.ts:18 and :36
  • src/app/api/teams/[teamSlug]/metrics/types.ts:18 and :35

All of those now interpolate 91 into user-facing error messages.

How it manifests — step-by-step proof

  1. User opens the monitoring page and sees the new "(90-day max)" caption on the Peak Concurrent Sandboxes card and "Last 90 days" as the largest preset.
  2. User clicks "Last 90 days" — setTimeRange('90d') is called, which computes now - 90 * 24 * 60 * 60 * 1000 and writes the resulting start/end to the URL.
  3. User opens the custom picker to refine the start (say, scrolls it back by an hour). The form's superRefine checks startTimestamp < now - MAX_DAYS_AGO. MAX_DAYS_AGO is now 91 days, so the picker happily accepts up to 91 days back — but if the user moves the start more than 1 day before the original 90-day preset (still well within what "90-day max" suggests should be allowed/rejected boundary), they'll first see no error.
  4. As soon as the user drags slightly past 91 days (or types a date that ends up >91 days ago after combining), they get the toast: "Start date cannot be more than 91 days ago." That number was never advertised. The same happens with the "Date range cannot exceed 91 days" message if the end is also adjusted.
  5. The server-side validators have the same 91 baked into their template-literal error text, so any path that hits the API directly (or any case where client-side validation is bypassed/raced) surfaces "91 days" to the user as well.

Why existing code doesn't prevent it

The PR deliberately left MAX_DAYS_AGO = 91 * 24 * 60 * 60 * 1000 as a 1-day safety buffer (mirrored by the 60_000 ms margin in header.tsx:162 for the same reason). That buffer is sensible internally, but it leaks because (a) the new copy in validation.ts hard-codes the buffered number into the human-readable string, and (b) the server-side schemas compute the user-facing copy directly from MAX_DAYS_AGO. There's no separate "advertised max" constant to drive UX copy independently.

Impact

UX polish, not a functional bug — the picker still works and the data still loads. But a user who reads "(90-day max)" and then sees "91 days" in a validation toast is left wondering which is correct, and the inconsistency surfaces an implementation detail (the clock-skew buffer) as if it were a product limit. It's more visually jarring at 90/91 than at the previous 30/31 because the larger numbers are read more carefully.

How to fix

Two clean options:

  1. Keep the 91-day internal buffer, fix the copy. Change validation.ts:46 and :111 to say '90 days' instead of '91 days', and change the server-side templates in get-team-metrics.ts, get-team-metrics-max.ts, schemas.ts, and metrics/types.ts to interpolate a separate ADVERTISED_MAX_DAYS = 90 constant (or just hardcode 90) instead of MAX_DAYS_AGO / 86_400_000.
  2. Tighten MAX_DAYS_AGO to 90 days and rely on the existing CLOCK_SKEW_TOLERANCE = 60 * 1000 (already imported into validation.ts) for the buffer. The header.tsx query already subtracts a separate 60_000 margin, so it would continue to work.

Option 1 is the smaller change and keeps the existing buffer semantics. The pre-existing 30/31 split shows the pattern was already in the codebase, but the larger number makes it more noticeable now.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 8de383e — all user-facing validation error messages now hardcode "90 days" instead of interpolating MAX_DAYS_AGO / 86_400_000 (which yielded 91). This applies to validation.ts, get-team-metrics.ts, get-team-metrics-max.ts, schemas.ts, and types.ts. MAX_DAYS_AGO stays at 91 days as an internal buffer so the "Last 90 days" preset never hits the validation boundary.

Expand Down Expand Up @@ -105,10 +110,10 @@ export const customTimeFormSchema = z
}

// ensure range doesn't exceed maximum
if (endTimestamp - startTimestamp > MAX_DAYS_AGO) {
if (endTimestamp - startTimestamp > MAX_DAYS_AGO_BUFFER) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Date range cannot exceed 31 days',
message: `Date range cannot exceed ${MAX_DAYS_AGO / (1000 * 60 * 60 * 24)} days`,
path: ['endDate'],
})
return
Expand Down
1 change: 1 addition & 0 deletions src/lib/utils/timeframe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export const TIME_RANGES = {
'6h': 1000 * 60 * 60 * 6,
'24h': 1000 * 60 * 60 * 24,
'30d': 1000 * 60 * 60 * 24 * 30,
'90d': 1000 * 60 * 60 * 24 * 90,
} as const

export type TimeRangeKey = keyof typeof TIME_RANGES
Expand Down
Loading