diff --git a/packages/analytics/analytics-utilities/src/timeframes.ts b/packages/analytics/analytics-utilities/src/timeframes.ts index 3d13c48411..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 { @@ -207,6 +215,22 @@ class CurrentMonth extends Timeframe { } } +class CurrentYear extends Timeframe { + rawStart(tz?: string): Date { + 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 +283,28 @@ class PreviousMonth extends Timeframe { } } +class PreviousYear extends Timeframe { + rawEnd(tz?: string): Date { + let thisYear = new Date(this.tzAdjustedDate(tz).getFullYear(), 0, 1) + + if (tz) { + thisYear = adjustForTz(thisYear, tz) + } + + return thisYear + } + + rawStart(tz?: string): Date { + 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 +417,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.ONE_YEAR, + new Timeframe({ + key: TimeframeKeys.ONE_YEAR, + display: 'Last 365 days', + timeframeText: '365 days', + timeframeLength: () => 60 * 60 * 24 * 365, + 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 +502,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 +563,29 @@ export const TimePeriods = new Map([ allowedTiers: ['plus', 'enterprise'], }), ], + [ + TimeframeKeys.PREVIOUS_YEAR, + new PreviousYear({ + 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 { @@ -523,8 +656,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, } diff --git a/packages/analytics/analytics-utilities/src/types/timeframe-keys.ts b/packages/analytics/analytics-utilities/src/types/timeframe-keys.ts index 71c6ed513c..4ba4cf5146 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', + ONE_YEAR = '365d', 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', } 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]