From 14833f801cc619008445e39ff17f148a144180b6 Mon Sep 17 00:00:00 2001 From: lukewhchen Date: Wed, 15 Oct 2025 08:30:05 -0700 Subject: [PATCH 1/7] fix(analytics): add more timeframe keys --- .../analytics-utilities/src/types/timeframe-keys.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/analytics/analytics-utilities/src/types/timeframe-keys.ts b/packages/analytics/analytics-utilities/src/types/timeframe-keys.ts index 71c6ed513c..3aca0c6477 100644 --- a/packages/analytics/analytics-utilities/src/types/timeframe-keys.ts +++ b/packages/analytics/analytics-utilities/src/types/timeframe-keys.ts @@ -7,10 +7,15 @@ export enum TimeframeKeys { ONE_DAY = '24h', SEVEN_DAY = '7d', THIRTY_DAY = '30d', + NINETY_DAY = '90d', + ONE_HUNDRED_EIGHTY_DAY = '180d', + THREE_HUNDRED_SIXTY_DAY = '360d', CURRENT_WEEK = 'current_week', CURRENT_MONTH = 'current_month', CURRENT_QUARTER = 'current_quarter', + CURRENT_YEAR = 'current_year', PREVIOUS_WEEK = 'previous_week', PREVIOUS_MONTH = 'previous_month', PREVIOUS_QUARTER = 'previous_quarter', + PREVIOUS_YEAR = 'previous_year', } From 51a29ad06d45cb471fa8e3412629a238b4404423 Mon Sep 17 00:00:00 2001 From: lukewhchen Date: Wed, 15 Oct 2025 13:07:33 -0700 Subject: [PATCH 2/7] feat: more adjust --- .../analytics-utilities/src/timeframes.ts | 128 ++++++++++++++++++ .../src/types/explore/common.ts | 5 + 2 files changed, 133 insertions(+) diff --git a/packages/analytics/analytics-utilities/src/timeframes.ts b/packages/analytics/analytics-utilities/src/timeframes.ts index 3d13c48411..30eb37248c 100644 --- a/packages/analytics/analytics-utilities/src/timeframes.ts +++ b/packages/analytics/analytics-utilities/src/timeframes.ts @@ -207,6 +207,23 @@ class CurrentMonth extends Timeframe { } } +class CurrentYear extends Timeframe { + rawStart(tz?: string): Date { + // `startOfYear` isn't aware of timezones, so the resulting "start of year" time is in the local timezone. + let firstOfTheYear = new Date(this.tzAdjustedDate(tz).getFullYear(), 0, 1) + + if (tz) { + firstOfTheYear = adjustForTz(firstOfTheYear, tz) + } + + return firstOfTheYear + } + + maximumTimeframeLength() { + return 60 * 60 * 24 * 366 + } +} + class PreviousWeek extends Timeframe { rawEnd(tz?: string): Date { // `startOfWeek` isn't aware of timezones, so the resulting "start of month" time is in the local timezone. @@ -259,6 +276,30 @@ class PreviousMonth extends Timeframe { } } +class PerviousYear extends Timeframe { + rawEnd(tz?: string): Date { + // `startOfYear` isn't aware of timezones, so the resulting "start of year" time is in the local timezone. + let thisYear = new Date(this.tzAdjustedDate(tz).getFullYear(), 0, 1) + + if (tz) { + thisYear = adjustForTz(thisYear, tz) + } + + return thisYear + } + + rawStart(tz?: string): Date { + // `startOfYear` isn't aware of timezones, so the resulting "start of year" time is in the local timezone. + let lastYear = new Date(this.tzAdjustedDate(tz).getFullYear() - 1, 0, 1) + + if (tz) { + lastYear = adjustForTz(lastYear, tz) + } + + return lastYear + } +} + // These TimePeriod definitions request a default granularity and can be adjusted // // Using as a temp workaround for TimePeriods.get() potentially returning `undefined` lint issue. @@ -371,6 +412,51 @@ export const TimePeriods = new Map([ allowedGranularitiesOverride: ['hourly', 'twoHourly', 'twelveHourly', 'daily', 'weekly'], }), ], + [ + TimeframeKeys.NINETY_DAY, + new Timeframe({ + key: TimeframeKeys.NINETY_DAY, + display: 'Last 90 days', + timeframeText: '90 days', + timeframeLength: () => 60 * 60 * 24 * 90, + defaultResponseGranularity: 'daily', + dataGranularity: 'daily', + isRelative: true, + fineGrainedDefaultGranularity: 'daily', + allowedTiers: ['trial', 'plus', 'enterprise'], + allowedGranularitiesOverride: ['hourly', 'twoHourly', 'twelveHourly', 'daily', 'weekly'], + }), + ], + [ + TimeframeKeys.ONE_HUNDRED_EIGHTY_DAY, + new Timeframe({ + key: TimeframeKeys.ONE_HUNDRED_EIGHTY_DAY, + display: 'Last 180 days', + timeframeText: '180 days', + timeframeLength: () => 60 * 60 * 24 * 180, + defaultResponseGranularity: 'daily', + dataGranularity: 'daily', + isRelative: true, + fineGrainedDefaultGranularity: 'daily', + allowedTiers: ['trial', 'plus', 'enterprise'], + allowedGranularitiesOverride: ['hourly', 'twoHourly', 'twelveHourly', 'daily', 'weekly'], + }), + ], + [ + TimeframeKeys.THREE_HUNDRED_SIXTY_DAY, + new Timeframe({ + key: TimeframeKeys.THREE_HUNDRED_SIXTY_DAY, + display: 'Last 360 days', + timeframeText: '360 days', + timeframeLength: () => 60 * 60 * 24 * 360, + defaultResponseGranularity: 'daily', + dataGranularity: 'daily', + isRelative: true, + fineGrainedDefaultGranularity: 'daily', + allowedTiers: ['trial', 'plus', 'enterprise'], + allowedGranularitiesOverride: ['hourly', 'twoHourly', 'twelveHourly', 'daily', 'weekly'], + }), + ], [ TimeframeKeys.CURRENT_WEEK, new CurrentWeek({ @@ -411,6 +497,25 @@ export const TimePeriods = new Map([ allowedTiers: ['plus', 'enterprise'], }), ], + [ + TimeframeKeys.CURRENT_YEAR, + new CurrentYear({ + key: TimeframeKeys.CURRENT_YEAR, + display: 'This year', + timeframeText: 'Year', + timeframeLength: () => { + // Jan 1 -> now + const firstOfTheYear = new Date(new Date().getFullYear(), 0, 1) + const end = startOfDay(addDays(new Date(), 1)) + + return (end.getTime() - firstOfTheYear.getTime()) / 1000 + }, + defaultResponseGranularity: 'daily', + dataGranularity: 'daily', + isRelative: false, + allowedTiers: ['plus', 'enterprise'], + }), + ], [ TimeframeKeys.PREVIOUS_WEEK, new PreviousWeek({ @@ -453,6 +558,29 @@ export const TimePeriods = new Map([ allowedTiers: ['plus', 'enterprise'], }), ], + [ + TimeframeKeys.PREVIOUS_YEAR, + new PerviousYear({ + key: TimeframeKeys.PREVIOUS_YEAR, + display: 'Previous year', + timeframeText: 'Year', + timeframeLength: () => { + // Not all years have the same number of days (leap years). + const end = new Date(new Date().getFullYear(), 0, 1) + const start = new Date(new Date().getFullYear() - 1, 0, 1) + let offset = 0 + if (end.getTimezoneOffset() !== start.getTimezoneOffset()) { + offset = dstOffsetHours(end, start) + } + + return 60 * 60 * 24 * (365 + (start.getFullYear() % 4 === 0 ? 1 : 0)) + hoursToSeconds(offset) + }, + defaultResponseGranularity: 'daily', + dataGranularity: 'daily', + isRelative: false, + allowedTiers: ['plus', 'enterprise'], + }), + ], ]) export function datePickerSelectionToTimeframe(datePickerSelection: DatePickerSelection): Timeframe { diff --git a/packages/analytics/analytics-utilities/src/types/explore/common.ts b/packages/analytics/analytics-utilities/src/types/explore/common.ts index ac5d680a71..6d26004244 100644 --- a/packages/analytics/analytics-utilities/src/types/explore/common.ts +++ b/packages/analytics/analytics-utilities/src/types/explore/common.ts @@ -49,6 +49,11 @@ export const relativeTimeRangeValuesV4 = [ '30d', 'current_month', 'previous_month', + '90d', + '180d', + '360d', + 'current_year', + 'previous_year', ] as const export type RelativeTimeRangeValuesV4 = typeof relativeTimeRangeValuesV4[number] From 4dcb99c0c1736a13ca1bd8f32edec10d815204dd Mon Sep 17 00:00:00 2001 From: lukewhchen Date: Wed, 15 Oct 2025 13:32:39 -0700 Subject: [PATCH 3/7] feat: fix --- .../analytics/analytics-utilities/src/timeframes.ts | 13 +++++-------- .../analytics-utilities/src/types/explore/common.ts | 2 +- .../analytics-utilities/src/types/timeframe-keys.ts | 2 +- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/packages/analytics/analytics-utilities/src/timeframes.ts b/packages/analytics/analytics-utilities/src/timeframes.ts index 30eb37248c..3f040f6712 100644 --- a/packages/analytics/analytics-utilities/src/timeframes.ts +++ b/packages/analytics/analytics-utilities/src/timeframes.ts @@ -209,7 +209,6 @@ class CurrentMonth extends Timeframe { class CurrentYear extends Timeframe { rawStart(tz?: string): Date { - // `startOfYear` isn't aware of timezones, so the resulting "start of year" time is in the local timezone. let firstOfTheYear = new Date(this.tzAdjustedDate(tz).getFullYear(), 0, 1) if (tz) { @@ -278,7 +277,6 @@ class PreviousMonth extends Timeframe { class PerviousYear extends Timeframe { rawEnd(tz?: string): Date { - // `startOfYear` isn't aware of timezones, so the resulting "start of year" time is in the local timezone. let thisYear = new Date(this.tzAdjustedDate(tz).getFullYear(), 0, 1) if (tz) { @@ -289,7 +287,6 @@ class PerviousYear extends Timeframe { } rawStart(tz?: string): Date { - // `startOfYear` isn't aware of timezones, so the resulting "start of year" time is in the local timezone. let lastYear = new Date(this.tzAdjustedDate(tz).getFullYear() - 1, 0, 1) if (tz) { @@ -443,12 +440,12 @@ export const TimePeriods = new Map([ }), ], [ - TimeframeKeys.THREE_HUNDRED_SIXTY_DAY, + TimeframeKeys.ONE_YEAR, new Timeframe({ - key: TimeframeKeys.THREE_HUNDRED_SIXTY_DAY, - display: 'Last 360 days', - timeframeText: '360 days', - timeframeLength: () => 60 * 60 * 24 * 360, + key: TimeframeKeys.ONE_YEAR, + display: 'Last 365 days', + timeframeText: '365 days', + timeframeLength: () => 60 * 60 * 24 * 365, defaultResponseGranularity: 'daily', dataGranularity: 'daily', isRelative: true, diff --git a/packages/analytics/analytics-utilities/src/types/explore/common.ts b/packages/analytics/analytics-utilities/src/types/explore/common.ts index 6d26004244..c92a4db30f 100644 --- a/packages/analytics/analytics-utilities/src/types/explore/common.ts +++ b/packages/analytics/analytics-utilities/src/types/explore/common.ts @@ -51,7 +51,7 @@ export const relativeTimeRangeValuesV4 = [ 'previous_month', '90d', '180d', - '360d', + '365d', 'current_year', 'previous_year', ] as const diff --git a/packages/analytics/analytics-utilities/src/types/timeframe-keys.ts b/packages/analytics/analytics-utilities/src/types/timeframe-keys.ts index 3aca0c6477..4ba4cf5146 100644 --- a/packages/analytics/analytics-utilities/src/types/timeframe-keys.ts +++ b/packages/analytics/analytics-utilities/src/types/timeframe-keys.ts @@ -9,7 +9,7 @@ export enum TimeframeKeys { THIRTY_DAY = '30d', NINETY_DAY = '90d', ONE_HUNDRED_EIGHTY_DAY = '180d', - THREE_HUNDRED_SIXTY_DAY = '360d', + ONE_YEAR = '365d', CURRENT_WEEK = 'current_week', CURRENT_MONTH = 'current_month', CURRENT_QUARTER = 'current_quarter', From 5d5b99ef9c14f255c0f71ee849973032bd445341 Mon Sep 17 00:00:00 2001 From: lukewhchen Date: Wed, 15 Oct 2025 13:49:35 -0700 Subject: [PATCH 4/7] fix: timeframe lookup --- packages/analytics/analytics-utilities/src/timeframes.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/analytics/analytics-utilities/src/timeframes.ts b/packages/analytics/analytics-utilities/src/timeframes.ts index 3f040f6712..b1168c73cf 100644 --- a/packages/analytics/analytics-utilities/src/timeframes.ts +++ b/packages/analytics/analytics-utilities/src/timeframes.ts @@ -648,8 +648,13 @@ export const TIMEFRAME_LOOKUP: Record = { '24h': TimeframeKeys.ONE_DAY, '7d': TimeframeKeys.SEVEN_DAY, '30d': TimeframeKeys.THIRTY_DAY, + '90d': TimeframeKeys.NINETY_DAY, + '180d': TimeframeKeys.ONE_HUNDRED_EIGHTY_DAY, + '365d': TimeframeKeys.ONE_YEAR, current_week: TimeframeKeys.CURRENT_WEEK, current_month: TimeframeKeys.CURRENT_MONTH, + current_year: TimeframeKeys.CURRENT_YEAR, previous_week: TimeframeKeys.PREVIOUS_WEEK, previous_month: TimeframeKeys.PREVIOUS_MONTH, + previous_year: TimeframeKeys.PREVIOUS_YEAR, } From 9583ce32fdd852871e48b06206998a88caacb7a3 Mon Sep 17 00:00:00 2001 From: lukewhchen Date: Thu, 16 Oct 2025 11:07:12 -0700 Subject: [PATCH 5/7] fix: typo --- packages/analytics/analytics-utilities/src/timeframes.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/analytics/analytics-utilities/src/timeframes.ts b/packages/analytics/analytics-utilities/src/timeframes.ts index b1168c73cf..f16aa3e628 100644 --- a/packages/analytics/analytics-utilities/src/timeframes.ts +++ b/packages/analytics/analytics-utilities/src/timeframes.ts @@ -275,7 +275,7 @@ class PreviousMonth extends Timeframe { } } -class PerviousYear extends Timeframe { +class PreviousYear extends Timeframe { rawEnd(tz?: string): Date { let thisYear = new Date(this.tzAdjustedDate(tz).getFullYear(), 0, 1) @@ -557,7 +557,7 @@ export const TimePeriods = new Map([ ], [ TimeframeKeys.PREVIOUS_YEAR, - new PerviousYear({ + new PreviousYear({ key: TimeframeKeys.PREVIOUS_YEAR, display: 'Previous year', timeframeText: 'Year', From 9af34ba3658f18f33134e51c32a910cda6ae46d9 Mon Sep 17 00:00:00 2001 From: lukewhchen Date: Thu, 16 Oct 2025 12:29:37 -0700 Subject: [PATCH 6/7] fix: use assertions --- .../analytics/analytics-utilities/src/timeframes.ts | 10 +++++----- .../analytics-utilities/src/types/explore/common.ts | 5 ----- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/packages/analytics/analytics-utilities/src/timeframes.ts b/packages/analytics/analytics-utilities/src/timeframes.ts index f16aa3e628..7ccf4310ab 100644 --- a/packages/analytics/analytics-utilities/src/timeframes.ts +++ b/packages/analytics/analytics-utilities/src/timeframes.ts @@ -412,7 +412,7 @@ export const TimePeriods = new Map([ [ TimeframeKeys.NINETY_DAY, new Timeframe({ - key: TimeframeKeys.NINETY_DAY, + key: TimeframeKeys.NINETY_DAY as RelativeTimeRangeValuesV4, display: 'Last 90 days', timeframeText: '90 days', timeframeLength: () => 60 * 60 * 24 * 90, @@ -427,7 +427,7 @@ export const TimePeriods = new Map([ [ TimeframeKeys.ONE_HUNDRED_EIGHTY_DAY, new Timeframe({ - key: TimeframeKeys.ONE_HUNDRED_EIGHTY_DAY, + key: TimeframeKeys.ONE_HUNDRED_EIGHTY_DAY as RelativeTimeRangeValuesV4, display: 'Last 180 days', timeframeText: '180 days', timeframeLength: () => 60 * 60 * 24 * 180, @@ -442,7 +442,7 @@ export const TimePeriods = new Map([ [ TimeframeKeys.ONE_YEAR, new Timeframe({ - key: TimeframeKeys.ONE_YEAR, + key: TimeframeKeys.ONE_YEAR as RelativeTimeRangeValuesV4, display: 'Last 365 days', timeframeText: '365 days', timeframeLength: () => 60 * 60 * 24 * 365, @@ -497,7 +497,7 @@ export const TimePeriods = new Map([ [ TimeframeKeys.CURRENT_YEAR, new CurrentYear({ - key: TimeframeKeys.CURRENT_YEAR, + key: TimeframeKeys.CURRENT_YEAR as RelativeTimeRangeValuesV4, display: 'This year', timeframeText: 'Year', timeframeLength: () => { @@ -558,7 +558,7 @@ export const TimePeriods = new Map([ [ TimeframeKeys.PREVIOUS_YEAR, new PreviousYear({ - key: TimeframeKeys.PREVIOUS_YEAR, + key: TimeframeKeys.PREVIOUS_YEAR as RelativeTimeRangeValuesV4, display: 'Previous year', timeframeText: 'Year', timeframeLength: () => { diff --git a/packages/analytics/analytics-utilities/src/types/explore/common.ts b/packages/analytics/analytics-utilities/src/types/explore/common.ts index c92a4db30f..ac5d680a71 100644 --- a/packages/analytics/analytics-utilities/src/types/explore/common.ts +++ b/packages/analytics/analytics-utilities/src/types/explore/common.ts @@ -49,11 +49,6 @@ export const relativeTimeRangeValuesV4 = [ '30d', 'current_month', 'previous_month', - '90d', - '180d', - '365d', - 'current_year', - 'previous_year', ] as const export type RelativeTimeRangeValuesV4 = typeof relativeTimeRangeValuesV4[number] From 13143e8cef8c93d4281a2f9fedff1d128862cd4e Mon Sep 17 00:00:00 2001 From: Rocky Fischer Date: Thu, 16 Oct 2025 15:47:21 -0400 Subject: [PATCH 7/7] fix: type of timeframe key --- .../analytics-utilities/src/timeframes.ts | 28 ++++++++++++------- .../src/types/timeframe-options.ts | 6 +++- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/packages/analytics/analytics-utilities/src/timeframes.ts b/packages/analytics/analytics-utilities/src/timeframes.ts index 7ccf4310ab..9d29d4a95d 100644 --- a/packages/analytics/analytics-utilities/src/timeframes.ts +++ b/packages/analytics/analytics-utilities/src/timeframes.ts @@ -10,6 +10,8 @@ import { } from 'date-fns' import { + type ExtendedRelativeTimeRangeValues, + relativeTimeRangeValuesV4, TimeframeKeys, } from './types' @@ -29,10 +31,11 @@ const adjustForTz = (d: Date, tz: string) => { return new Date(d.getTime() - getTimezoneOffset(tz, d)) } + export class Timeframe implements ITimeframe { readonly timeframeText: string - readonly key: RelativeTimeRangeValuesV4 | 'custom' + readonly key: RelativeTimeRangeValuesV4 | ExtendedRelativeTimeRangeValues | 'custom' readonly display: string @@ -154,11 +157,16 @@ export class Timeframe implements ITimeframe { } } - return { - type: 'relative', - time_range: this.key, - tz, + if (relativeTimeRangeValuesV4.includes(this.key as any)) { + return { + type: 'relative', + // Safe assertion; we just checked that key is a member of the union. + time_range: this.key as RelativeTimeRangeValuesV4, + tz, + } } + + throw new Error('Unsupported relative time value for Explore') } protected tzAdjustedDate(tz?: string): Date { @@ -412,7 +420,7 @@ export const TimePeriods = new Map([ [ TimeframeKeys.NINETY_DAY, new Timeframe({ - key: TimeframeKeys.NINETY_DAY as RelativeTimeRangeValuesV4, + key: TimeframeKeys.NINETY_DAY, display: 'Last 90 days', timeframeText: '90 days', timeframeLength: () => 60 * 60 * 24 * 90, @@ -427,7 +435,7 @@ export const TimePeriods = new Map([ [ TimeframeKeys.ONE_HUNDRED_EIGHTY_DAY, new Timeframe({ - key: TimeframeKeys.ONE_HUNDRED_EIGHTY_DAY as RelativeTimeRangeValuesV4, + key: TimeframeKeys.ONE_HUNDRED_EIGHTY_DAY, display: 'Last 180 days', timeframeText: '180 days', timeframeLength: () => 60 * 60 * 24 * 180, @@ -442,7 +450,7 @@ export const TimePeriods = new Map([ [ TimeframeKeys.ONE_YEAR, new Timeframe({ - key: TimeframeKeys.ONE_YEAR as RelativeTimeRangeValuesV4, + key: TimeframeKeys.ONE_YEAR, display: 'Last 365 days', timeframeText: '365 days', timeframeLength: () => 60 * 60 * 24 * 365, @@ -497,7 +505,7 @@ export const TimePeriods = new Map([ [ TimeframeKeys.CURRENT_YEAR, new CurrentYear({ - key: TimeframeKeys.CURRENT_YEAR as RelativeTimeRangeValuesV4, + key: TimeframeKeys.CURRENT_YEAR, display: 'This year', timeframeText: 'Year', timeframeLength: () => { @@ -558,7 +566,7 @@ export const TimePeriods = new Map([ [ TimeframeKeys.PREVIOUS_YEAR, new PreviousYear({ - key: TimeframeKeys.PREVIOUS_YEAR as RelativeTimeRangeValuesV4, + key: TimeframeKeys.PREVIOUS_YEAR, display: 'Previous year', timeframeText: 'Year', timeframeLength: () => { diff --git a/packages/analytics/analytics-utilities/src/types/timeframe-options.ts b/packages/analytics/analytics-utilities/src/types/timeframe-options.ts index 1d8f94a976..7e436e076d 100644 --- a/packages/analytics/analytics-utilities/src/types/timeframe-options.ts +++ b/packages/analytics/analytics-utilities/src/types/timeframe-options.ts @@ -1,7 +1,7 @@ import type { GranularityValues, RelativeTimeRangeValuesV4 } from './explore' export interface TimeframeOptions { - key: RelativeTimeRangeValuesV4 | 'custom' + key: RelativeTimeRangeValuesV4 | ExtendedRelativeTimeRangeValues | 'custom' timeframeText: string display: string defaultResponseGranularity: GranularityValues @@ -14,3 +14,7 @@ export interface TimeframeOptions { allowedGranularitiesOverride?: GranularityValues[] fineGrainedDefaultGranularity?: GranularityValues } + +// Supported by time periods, but not supported in Explore APIs. +export const extendedRelativeTimeRangeValues = ['90d', '180d', '365d', 'current_year', 'previous_year'] as const +export type ExtendedRelativeTimeRangeValues = typeof extendedRelativeTimeRangeValues[number]