From dadda7b64839f1c9431c87b83d25f9f78dc04fd4 Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Thu, 13 Mar 2025 15:44:10 +0100 Subject: [PATCH] fix: Data usage graphs don't work in UTC-n time zones (#9530) Fixes a number of issues that would surface in UTC-n (where n > 1) timezones. I've not found a way to check this with tests (and it looks like [we weren't able to last time either](https://github.com/Unleash/unleash/pull/9110/files#r1919746328)), so all the testing's been done manually by adjusting my system time and zone. (From what I understand, you can't generate a Date with a specific TZ offset in JS: it's only utc or local time) Resolved: - [x] Selecting "Jan" in the dropdown results in the selection being "December" (off by one in the selector) - [x] Selecting a month view only gives you one data point (and it's probably empty). Wrong date parsing on the way out resulted in sending `{ from: "2025-02-28", to: "2025-02-28"}` instead of `{ from: "2025-03-01", to: "2025-03-31"}` - [x] The dates we create when making "daysRec" need to be adjusted. They showed the wrong month, so the dates were off. - [x] Make sure the labels are correct when hovering over. Again: we used the wrong month for generating these. - [x] The available months are wrong. Incorrect month parsing again. - [x] The request summary month is wrong. You guessed it: incorrect month parsing --- .../network/NetworkTrafficUsage/PeriodSelector.tsx | 14 +++++++++----- .../network/NetworkTrafficUsage/RequestSummary.tsx | 3 ++- .../NetworkTrafficUsage/chart-data-selection.ts | 3 ++- .../network/NetworkTrafficUsage/chart-functions.ts | 10 +++++----- .../admin/network/NetworkTrafficUsage/dates.ts | 12 +++++++++++- .../hooks/useChartDataSelection.ts | 7 ++++--- .../network/NetworkTrafficUsage/hooks/useStats.ts | 4 ++-- .../NetworkTrafficUsage/selectable-periods.ts | 13 +++++-------- frontend/src/utils/traffic-calculations.ts | 3 ++- 9 files changed, 42 insertions(+), 27 deletions(-) diff --git a/frontend/src/component/admin/network/NetworkTrafficUsage/PeriodSelector.tsx b/frontend/src/component/admin/network/NetworkTrafficUsage/PeriodSelector.tsx index 764a20a5e8..7090e93e42 100644 --- a/frontend/src/component/admin/network/NetworkTrafficUsage/PeriodSelector.tsx +++ b/frontend/src/component/admin/network/NetworkTrafficUsage/PeriodSelector.tsx @@ -5,6 +5,7 @@ import { useRef, useState, type FC } from 'react'; import { format } from 'date-fns'; import type { ChartDataSelection } from './chart-data-selection'; import { selectablePeriods } from './selectable-periods'; +import { parseMonthString } from './dates'; const dropdownWidth = '15rem'; const dropdownInlinePadding = (theme: Theme) => theme.spacing(3); @@ -148,10 +149,13 @@ export const PeriodSelector: FC = ({ selectedPeriod, setPeriod }) => { selectedPeriod.grouping === 'daily' ? selectedPeriod.month === format(new Date(), 'yyyy-MM') ? 'Current month' - : new Date(selectedPeriod.month).toLocaleDateString('en-US', { - month: 'long', - year: 'numeric', - }) + : parseMonthString(selectedPeriod.month).toLocaleDateString( + 'en-US', + { + month: 'long', + year: 'numeric', + }, + ) : `Last ${selectedPeriod.monthsBack} months`; return ( @@ -184,7 +188,7 @@ export const PeriodSelector: FC = ({ selectedPeriod, setPeriod }) => {

Last 12 months

- {selectablePeriods.map((period, index) => ( + {selectablePeriods.map((period) => (
  • { return `Average requests from ${formatMonth(fromMonth)} to ${formatMonth(toMonth)}`; } - return `Incoming requests in ${formatMonth(new Date(period.month))}`; + return `Incoming requests in ${formatMonth(parseMonthString(period.month))}`; }; export const RequestSummary: FC = ({ diff --git a/frontend/src/component/admin/network/NetworkTrafficUsage/chart-data-selection.ts b/frontend/src/component/admin/network/NetworkTrafficUsage/chart-data-selection.ts index be332ef76d..6a8df1ffab 100644 --- a/frontend/src/component/admin/network/NetworkTrafficUsage/chart-data-selection.ts +++ b/frontend/src/component/admin/network/NetworkTrafficUsage/chart-data-selection.ts @@ -1,4 +1,5 @@ import { endOfMonth, format, startOfMonth, subMonths } from 'date-fns'; +import { parseMonthString } from './dates'; export type ChartDataSelection = | { @@ -16,7 +17,7 @@ export const toDateRange = ( ): { from: string; to: string } => { const fmt = (date: Date) => format(date, 'yyyy-MM-dd'); if (selection.grouping === 'daily') { - const month = new Date(selection.month); + const month = parseMonthString(selection.month); const from = fmt(month); const to = fmt(endOfMonth(month)); return { from, to }; diff --git a/frontend/src/component/admin/network/NetworkTrafficUsage/chart-functions.ts b/frontend/src/component/admin/network/NetworkTrafficUsage/chart-functions.ts index 56af65ab67..202d5b4c07 100644 --- a/frontend/src/component/admin/network/NetworkTrafficUsage/chart-functions.ts +++ b/frontend/src/component/admin/network/NetworkTrafficUsage/chart-functions.ts @@ -10,7 +10,7 @@ import { differenceInCalendarDays, differenceInCalendarMonths, } from 'date-fns'; -import { formatDay, formatMonth } from './dates'; +import { formatDay, formatMonth, parseDateString } from './dates'; import type { ChartDataSelection } from './chart-data-selection'; export type ChartDatasetType = ChartDataset<'bar'>; @@ -80,8 +80,8 @@ const getLabelsAndRecords = ( >, ) => { if (traffic.grouping === 'monthly') { - const from = new Date(traffic.dateRange.from); - const to = new Date(traffic.dateRange.to); + const from = parseDateString(traffic.dateRange.from); + const to = parseDateString(traffic.dateRange.to); const numMonths = Math.abs(differenceInCalendarMonths(to, from)) + 1; const monthsRec: { [month: string]: number } = {}; for (let i = 0; i < numMonths; i++) { @@ -95,8 +95,8 @@ const getLabelsAndRecords = ( ); return { newRecord: () => ({ ...monthsRec }), labels }; } else { - const from = new Date(traffic.dateRange.from); - const to = new Date(traffic.dateRange.to); + const from = parseDateString(traffic.dateRange.from); + const to = parseDateString(traffic.dateRange.to); const numDays = Math.abs(differenceInCalendarDays(to, from)) + 1; const daysRec: { [day: string]: number } = {}; for (let i = 0; i < numDays; i++) { diff --git a/frontend/src/component/admin/network/NetworkTrafficUsage/dates.ts b/frontend/src/component/admin/network/NetworkTrafficUsage/dates.ts index 0976cd2a12..f44a62efcb 100644 --- a/frontend/src/component/admin/network/NetworkTrafficUsage/dates.ts +++ b/frontend/src/component/admin/network/NetworkTrafficUsage/dates.ts @@ -1,4 +1,4 @@ -import { format, getDaysInMonth } from 'date-fns'; +import { format, getDaysInMonth, parse } from 'date-fns'; export const currentDate = new Date(); @@ -11,3 +11,13 @@ export const daysInCurrentMonth = getDaysInMonth(currentDate); export const formatMonth = (date: Date) => format(date, 'yyyy-MM'); export const formatDay = (date: Date) => format(date, 'yyyy-MM-dd'); + +export const parseMonthString = (month: string): Date => { + // parses a month into a Date starting on the first day of the month, regardless of the current time zone (e.g. works in Norway and Brazil) + return parse(month, 'yyyy-MM', new Date()); +}; + +export const parseDateString = (month: string): Date => { + // parses a date string into a Date, regardless of the current time zone (e.g. works in Norway and Brazil) + return parse(month, 'yyyy-MM-dd', new Date()); +}; diff --git a/frontend/src/component/admin/network/NetworkTrafficUsage/hooks/useChartDataSelection.ts b/frontend/src/component/admin/network/NetworkTrafficUsage/hooks/useChartDataSelection.ts index 9b6618809f..6c317c8639 100644 --- a/frontend/src/component/admin/network/NetworkTrafficUsage/hooks/useChartDataSelection.ts +++ b/frontend/src/component/admin/network/NetworkTrafficUsage/hooks/useChartDataSelection.ts @@ -4,6 +4,7 @@ import { periodsRecord, selectablePeriods } from '../selectable-periods'; import { createBarChartOptions } from '../bar-chart-options'; import useTheme from '@mui/material/styles/useTheme'; import { useLocationSettings } from 'hooks/useLocationSettings'; +import { parseMonthString } from '../dates'; export const useChartDataSelection = (includedTraffic?: number) => { const theme = useTheme(); @@ -35,11 +36,11 @@ export const useChartDataSelection = (includedTraffic?: number) => { }, ); } else { - const timestamp = Date.parse(tooltipItems[0].label); - if (Number.isNaN(timestamp)) { + const month = parseMonthString(tooltipItems[0].label); + if (Number.isNaN(month.getTime())) { return 'Current month to date'; } - return new Date(timestamp).toLocaleDateString( + return month.toLocaleDateString( locationSettings?.locale ?? 'en-US', { month: 'long', diff --git a/frontend/src/component/admin/network/NetworkTrafficUsage/hooks/useStats.ts b/frontend/src/component/admin/network/NetworkTrafficUsage/hooks/useStats.ts index 36bd85e58a..805cea2fad 100644 --- a/frontend/src/component/admin/network/NetworkTrafficUsage/hooks/useStats.ts +++ b/frontend/src/component/admin/network/NetworkTrafficUsage/hooks/useStats.ts @@ -3,8 +3,8 @@ import { useTrafficSearch } from 'hooks/api/getters/useInstanceTrafficMetrics/us import { currentDate } from '../dates'; import { useMemo } from 'react'; import { - toTrafficUsageChartData as newToChartData, toConnectionChartData, + toTrafficUsageChartData, } from '../chart-functions'; import { calculateEstimatedMonthlyCost, @@ -36,7 +36,7 @@ export const useTrafficStats = ( } const traffic = result.data; - const chartData = newToChartData(traffic, filter); + const chartData = toTrafficUsageChartData(traffic, filter); const usageTotal = calculateTotalUsage(traffic); const overageCost = calculateOverageCost( usageTotal, diff --git a/frontend/src/component/admin/network/NetworkTrafficUsage/selectable-periods.ts b/frontend/src/component/admin/network/NetworkTrafficUsage/selectable-periods.ts index c70b0f5e93..f6af8442cc 100644 --- a/frontend/src/component/admin/network/NetworkTrafficUsage/selectable-periods.ts +++ b/frontend/src/component/admin/network/NetworkTrafficUsage/selectable-periods.ts @@ -1,4 +1,4 @@ -import { getDaysInMonth } from 'date-fns'; +import { getDaysInMonth, startOfMonth, subMonths } from 'date-fns'; import { currentDate, formatMonth } from './dates'; import { TRAFFIC_MEASUREMENT_START_DATE } from 'utils/traffic-calculations'; @@ -38,21 +38,18 @@ export const toSelectablePeriod = ( export const generateSelectablePeriodsFromDate = (now: Date) => { const selectablePeriods = [toSelectablePeriod(now, 'Current month')]; + const startOfCurrentMonth = startOfMonth(now); for ( let subtractMonthCount = 1; subtractMonthCount < 12; subtractMonthCount++ ) { - // this complicated calc avoids DST issues - const utcYear = now.getUTCFullYear(); - const utcMonth = now.getUTCMonth(); - const targetMonth = utcMonth - subtractMonthCount; - const targetDate = new Date(Date.UTC(utcYear, targetMonth, 1, 0, 0, 0)); + const targetMonth = subMonths(startOfCurrentMonth, subtractMonthCount); selectablePeriods.push( toSelectablePeriod( - targetDate, + targetMonth, undefined, - targetDate >= TRAFFIC_MEASUREMENT_START_DATE, + targetMonth >= TRAFFIC_MEASUREMENT_START_DATE, ), ); } diff --git a/frontend/src/utils/traffic-calculations.ts b/frontend/src/utils/traffic-calculations.ts index 464ea1801d..3a7cc5c735 100644 --- a/frontend/src/utils/traffic-calculations.ts +++ b/frontend/src/utils/traffic-calculations.ts @@ -4,10 +4,11 @@ import type { } from 'openapi'; import { getDaysInMonth } from 'date-fns'; import { format } from 'date-fns'; +import { parseDateString } from 'component/admin/network/NetworkTrafficUsage/dates'; export const DEFAULT_TRAFFIC_DATA_UNIT_COST = 5; export const DEFAULT_TRAFFIC_DATA_UNIT_SIZE = 1_000_000; -export const TRAFFIC_MEASUREMENT_START_DATE = new Date('2024-05-01'); +export const TRAFFIC_MEASUREMENT_START_DATE = parseDateString('2024-05-01'); export const METERED_TRAFFIC_ENDPOINTS = [ '/api/admin',