mirror of
https://github.com/modrinth/code.git
synced 2026-08-27 01:54:47 +00:00
* fix: previous period data was included in the table * fix: revenue displaying stale data when viewing it from different metric and grouped by 6 hour or 1 hour * fix: remove staletime on analytics query so switching tabs does not refersh query * feat: add monetization alert * fix-small: missing space in tooltip * fix: incorrect y-axis formatting for trailing decimal 0s * fix: switching tabs resets table series selection due to other refetches * fix: always show month first in chart tooltip * fix: change all time start date to be project published date * fix: increase length on project name column * fix: unknown download source data points not showing for download source breakdown * fix: double unknown for loader * fix: no data on country labeling incorrectly as "Unknown" instead of "Other" * fix: date picker number inputs showing arrows * fix: stat card showing enormous percentage for prev period by switching it to absolute value difference after 1000% * fix: decimal values for playtime being rounded badly, resulting in 0.04 becoming 0.0 * fix: chips having stroke * refactor: pnpm prepr * fix: spacing in annoucement link * fix: legend scroll shadow on top of event tooltip
305 lines
8.0 KiB
TypeScript
305 lines
8.0 KiB
TypeScript
import {
|
|
type AnalyticsGroupByPreset,
|
|
type AnalyticsLastTimeframeUnit,
|
|
type AnalyticsTimeframeMode,
|
|
type AnalyticsTimeframePreset,
|
|
injectAnalyticsDashboardContext,
|
|
} from '~/providers/analytics/analytics'
|
|
|
|
const MIN_RANGE_MS = 60 * 60 * 1000
|
|
const TIME_RANGE_ROUNDING_MS = 60 * 1000
|
|
export const MAX_ANALYTICS_TIME_SLICES = 256
|
|
|
|
const GROUP_BY_PRESET_MINUTES: Record<AnalyticsGroupByPreset, number> = {
|
|
'1h': 60,
|
|
'6h': 360,
|
|
day: 24 * 60,
|
|
week: 7 * 24 * 60,
|
|
month: 30 * 24 * 60,
|
|
year: 365 * 24 * 60,
|
|
}
|
|
|
|
export type AnalyticsTimeRange = {
|
|
start: Date
|
|
end: Date
|
|
}
|
|
|
|
export function startOfDay(date: Date): Date {
|
|
const nextDate = new Date(date)
|
|
nextDate.setHours(0, 0, 0, 0)
|
|
return nextDate
|
|
}
|
|
|
|
export function getRoundedNow(timestamp: number): Date {
|
|
const roundedTimestamp = Math.floor(timestamp / TIME_RANGE_ROUNDING_MS) * TIME_RANGE_ROUNDING_MS
|
|
return new Date(roundedTimestamp)
|
|
}
|
|
|
|
export function getDateInputValue(date: Date): string {
|
|
const year = date.getFullYear()
|
|
const month = String(date.getMonth() + 1).padStart(2, '0')
|
|
const day = String(date.getDate()).padStart(2, '0')
|
|
return `${year}-${month}-${day}`
|
|
}
|
|
|
|
export function parseDateInputValue(value: string): Date {
|
|
const parsedDate = new Date(`${value}T00:00:00`)
|
|
return Number.isNaN(parsedDate.getTime()) ? startOfDay(new Date()) : parsedDate
|
|
}
|
|
|
|
export function parseDateTimeInputValue(value: string): Date {
|
|
const parsedDate = new Date(value)
|
|
return Number.isNaN(parsedDate.getTime()) ? getRoundedNow(Date.now()) : parsedDate
|
|
}
|
|
|
|
export function addDays(date: Date, days: number): Date {
|
|
const nextDate = new Date(date)
|
|
nextDate.setDate(nextDate.getDate() + days)
|
|
return nextDate
|
|
}
|
|
|
|
function isStartOfDay(date: Date): boolean {
|
|
return (
|
|
date.getHours() === 0 &&
|
|
date.getMinutes() === 0 &&
|
|
date.getSeconds() === 0 &&
|
|
date.getMilliseconds() === 0
|
|
)
|
|
}
|
|
|
|
export function getInclusiveEndDateInputValue(end: Date): string {
|
|
return getDateInputValue(isStartOfDay(end) ? addDays(end, -1) : end)
|
|
}
|
|
|
|
function subtractCalendarMonths(date: Date, months: number): Date {
|
|
const nextDate = new Date(date)
|
|
const day = nextDate.getDate()
|
|
nextDate.setDate(1)
|
|
nextDate.setMonth(nextDate.getMonth() - months)
|
|
const daysInMonth = new Date(nextDate.getFullYear(), nextDate.getMonth() + 1, 0).getDate()
|
|
nextDate.setDate(Math.min(day, daysInMonth))
|
|
return nextDate
|
|
}
|
|
|
|
export function getTimeRangeForPreset(
|
|
preset: AnalyticsTimeframePreset,
|
|
nowTimestamp: number,
|
|
allTimeStartDate: Date = new Date(Date.UTC(2023, 0, 1, 0, 0, 0, 0)),
|
|
): AnalyticsTimeRange {
|
|
const now = getRoundedNow(nowTimestamp)
|
|
const end = new Date(now)
|
|
|
|
switch (preset) {
|
|
case 'today':
|
|
return { start: startOfDay(now), end }
|
|
case 'yesterday': {
|
|
const todayStart = startOfDay(now)
|
|
return {
|
|
start: new Date(todayStart.getTime() - 24 * 60 * 60 * 1000),
|
|
end: todayStart,
|
|
}
|
|
}
|
|
case 'last_7_days':
|
|
return {
|
|
start: new Date(end.getTime() - 7 * 24 * 60 * 60 * 1000),
|
|
end,
|
|
}
|
|
case 'last_14_days':
|
|
return {
|
|
start: new Date(end.getTime() - 14 * 24 * 60 * 60 * 1000),
|
|
end,
|
|
}
|
|
case 'last_30_days':
|
|
return {
|
|
start: new Date(end.getTime() - 30 * 24 * 60 * 60 * 1000),
|
|
end,
|
|
}
|
|
case 'last_90_days':
|
|
return {
|
|
start: new Date(end.getTime() - 90 * 24 * 60 * 60 * 1000),
|
|
end,
|
|
}
|
|
case 'last_180_days':
|
|
return {
|
|
start: new Date(end.getTime() - 180 * 24 * 60 * 60 * 1000),
|
|
end,
|
|
}
|
|
case 'year_to_date': {
|
|
const yearStart = new Date(now.getFullYear(), 0, 1)
|
|
yearStart.setHours(0, 0, 0, 0)
|
|
return { start: yearStart, end }
|
|
}
|
|
case 'all_time':
|
|
return {
|
|
start: new Date(allTimeStartDate),
|
|
end,
|
|
}
|
|
default:
|
|
return {
|
|
start: new Date(end.getTime() - 24 * 60 * 60 * 1000),
|
|
end,
|
|
}
|
|
}
|
|
}
|
|
|
|
export function getTimeRangeForLastTimeframe(
|
|
amountValue: number,
|
|
unit: AnalyticsLastTimeframeUnit,
|
|
nowTimestamp: number,
|
|
): AnalyticsTimeRange {
|
|
const end = getRoundedNow(nowTimestamp)
|
|
const amount = Math.max(1, Math.floor(amountValue))
|
|
|
|
switch (unit) {
|
|
case 'hours':
|
|
return { start: new Date(end.getTime() - amount * 60 * 60 * 1000), end }
|
|
case 'days':
|
|
return { start: new Date(end.getTime() - amount * 24 * 60 * 60 * 1000), end }
|
|
case 'weeks':
|
|
return { start: new Date(end.getTime() - amount * 7 * 24 * 60 * 60 * 1000), end }
|
|
case 'months':
|
|
return { start: subtractCalendarMonths(end, amount), end }
|
|
default:
|
|
return { start: new Date(end.getTime() - 24 * 60 * 60 * 1000), end }
|
|
}
|
|
}
|
|
|
|
export function getTimeRangeForCustomDateRange(
|
|
startDate: string,
|
|
endDate: string,
|
|
): AnalyticsTimeRange {
|
|
const start = parseDateInputValue(startDate)
|
|
const inclusiveEnd = parseDateInputValue(endDate)
|
|
return {
|
|
start,
|
|
end: addDays(inclusiveEnd, 1),
|
|
}
|
|
}
|
|
|
|
export function getTimeRangeForCustomDateTimeRange(
|
|
startDateTime: string,
|
|
endDateTime: string,
|
|
): AnalyticsTimeRange {
|
|
return {
|
|
start: parseDateTimeInputValue(startDateTime),
|
|
end: parseDateTimeInputValue(endDateTime),
|
|
}
|
|
}
|
|
|
|
export function getAnalyticsTimeRange({
|
|
mode,
|
|
preset,
|
|
lastAmount,
|
|
lastUnit,
|
|
customStartDate,
|
|
customEndDate,
|
|
nowTimestamp,
|
|
allTimeStartDate,
|
|
}: {
|
|
mode: AnalyticsTimeframeMode
|
|
preset: AnalyticsTimeframePreset
|
|
lastAmount: number
|
|
lastUnit: AnalyticsLastTimeframeUnit
|
|
customStartDate: string
|
|
customEndDate: string
|
|
nowTimestamp: number
|
|
allTimeStartDate?: Date
|
|
}): AnalyticsTimeRange {
|
|
switch (mode) {
|
|
case 'last':
|
|
return getTimeRangeForLastTimeframe(lastAmount, lastUnit, nowTimestamp)
|
|
case 'custom_range':
|
|
return getTimeRangeForCustomDateRange(customStartDate, customEndDate)
|
|
case 'custom_datetime_range':
|
|
return getTimeRangeForCustomDateTimeRange(customStartDate, customEndDate)
|
|
case 'preset':
|
|
default:
|
|
return getTimeRangeForPreset(preset, nowTimestamp, allTimeStartDate)
|
|
}
|
|
}
|
|
|
|
export function getDefaultAnalyticsGroupByForDurationMinutes(
|
|
durationMinutes: number,
|
|
): AnalyticsGroupByPreset {
|
|
const days = durationMinutes / (24 * 60)
|
|
if (days <= 2) return '1h'
|
|
if (days <= 7) return '6h'
|
|
if (days <= 90) return 'day'
|
|
if (days <= 365) return 'week'
|
|
if (days <= 365 * 3) return 'month'
|
|
return 'year'
|
|
}
|
|
|
|
export function getAnalyticsGroupByPresetMinutes(preset: AnalyticsGroupByPreset): number {
|
|
return GROUP_BY_PRESET_MINUTES[preset]
|
|
}
|
|
|
|
export function isAnalyticsGroupByAvailableForDurationMinutes(
|
|
preset: AnalyticsGroupByPreset,
|
|
durationMinutes: number,
|
|
): boolean {
|
|
const groupByMinutes = getAnalyticsGroupByPresetMinutes(preset)
|
|
const isTooCoarse = groupByMinutes >= durationMinutes
|
|
const isTooFine = durationMinutes / groupByMinutes > MAX_ANALYTICS_TIME_SLICES
|
|
|
|
return !isTooCoarse && !isTooFine
|
|
}
|
|
|
|
export function ensureMinimumTimeRange(start: Date, end: Date): AnalyticsTimeRange {
|
|
if (end.getTime() <= start.getTime()) {
|
|
return {
|
|
start: new Date(end.getTime() - MIN_RANGE_MS),
|
|
end,
|
|
}
|
|
}
|
|
|
|
if (end.getTime() - start.getTime() < MIN_RANGE_MS) {
|
|
return {
|
|
start: new Date(end.getTime() - MIN_RANGE_MS),
|
|
end,
|
|
}
|
|
}
|
|
|
|
return { start, end }
|
|
}
|
|
|
|
export function useSelectedAnalyticsTimeRange() {
|
|
const {
|
|
selectedTimeframeMode,
|
|
selectedTimeframe,
|
|
selectedLastTimeframeAmount,
|
|
selectedLastTimeframeUnit,
|
|
selectedCustomTimeframeStartDate,
|
|
selectedCustomTimeframeEndDate,
|
|
queryRefreshTimestamp,
|
|
analyticsAllTimeStartDate,
|
|
} = injectAnalyticsDashboardContext()
|
|
|
|
const selectedTimeRange = computed(() =>
|
|
getAnalyticsTimeRange({
|
|
mode: selectedTimeframeMode.value,
|
|
preset: selectedTimeframe.value,
|
|
lastAmount: selectedLastTimeframeAmount.value,
|
|
lastUnit: selectedLastTimeframeUnit.value,
|
|
customStartDate: selectedCustomTimeframeStartDate.value,
|
|
customEndDate: selectedCustomTimeframeEndDate.value,
|
|
nowTimestamp: queryRefreshTimestamp.value,
|
|
allTimeStartDate: analyticsAllTimeStartDate.value,
|
|
}),
|
|
)
|
|
|
|
const selectedTimeframeDurationMinutes = computed(() => {
|
|
const { start, end } = ensureMinimumTimeRange(
|
|
selectedTimeRange.value.start,
|
|
selectedTimeRange.value.end,
|
|
)
|
|
const durationMs = end.getTime() - start.getTime()
|
|
return Math.max(1, Math.floor(durationMs / (60 * 1000)))
|
|
})
|
|
|
|
return {
|
|
selectedTimeRange,
|
|
selectedTimeframeDurationMinutes,
|
|
}
|
|
}
|