From 8371ff641a471c1fbe12069ed63d0da58cf586c2 Mon Sep 17 00:00:00 2001 From: Truman Gao <106889354+tdgao@users.noreply.github.com> Date: Wed, 3 Jun 2026 12:27:31 -0600 Subject: [PATCH] fix: analytics post release bugs (#6291) * 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 --- .../AnalyticsChartLegend.vue | 4 +- .../AnalyticsChartEvents.vue | 2 +- .../AnalyticsChartTooltip.vue | 15 +- .../use-analytics-chart-events.ts | 1 - .../analytics-chart/analytics-chart-utils.ts | 15 +- .../analytics-dashboard/analytics-messages.ts | 13 ++ .../analytics-table-columns.ts | 2 + .../analytics-table-formatting.ts | 8 +- .../analytics-table-row-builder.ts | 18 ++- .../analytics-dashboard/breakdown.ts | 12 +- .../query-builder/TimeframePicker.vue | 2 + .../query-builder/index.vue | 14 +- .../query-builder/timeframe.ts | 9 +- .../stat-cards/StatCards.vue | 153 +++++++++++++++--- apps/frontend/src/locales/en-US/index.json | 9 ++ .../analytics/analytics-data-utils.ts | 15 +- .../analytics/analytics-project-utils.ts | 1 + .../providers/analytics/analytics-types.ts | 2 + .../src/providers/analytics/analytics.ts | 121 +++++++++++--- .../ui/src/components/base/Admonition.vue | 2 +- packages/ui/src/components/base/Chips.vue | 3 +- .../ui/src/components/base/DatePicker.vue | 9 ++ 22 files changed, 343 insertions(+), 87 deletions(-) diff --git a/apps/frontend/src/components/analytics-dashboard/analytics-chart/analytics-chart-header/AnalyticsChartLegend.vue b/apps/frontend/src/components/analytics-dashboard/analytics-chart/analytics-chart-header/AnalyticsChartLegend.vue index 22c9580ae..c5c4d3112 100644 --- a/apps/frontend/src/components/analytics-dashboard/analytics-chart/analytics-chart-header/AnalyticsChartLegend.vue +++ b/apps/frontend/src/components/analytics-dashboard/analytics-chart/analytics-chart-header/AnalyticsChartLegend.vue @@ -10,7 +10,7 @@ >
@@ -99,7 +99,7 @@ > diff --git a/apps/frontend/src/components/analytics-dashboard/analytics-chart/analytics-chart-plot/AnalyticsChartEvents.vue b/apps/frontend/src/components/analytics-dashboard/analytics-chart/analytics-chart-plot/AnalyticsChartEvents.vue index cd3a95766..7ee3e7d7a 100644 --- a/apps/frontend/src/components/analytics-dashboard/analytics-chart/analytics-chart-plot/AnalyticsChartEvents.vue +++ b/apps/frontend/src/components/analytics-dashboard/analytics-chart/analytics-chart-plot/AnalyticsChartEvents.vue @@ -132,7 +132,7 @@ :href="event.announcement_url" target="_blank" rel="noopener noreferrer" - class="mt-1.5 inline-flex items-center gap-1 text-sm font-medium text-primary underline !transition-all hover:text-contrast" + class="my-0.5 inline-flex items-center gap-1 text-xs font-medium text-primary underline !transition-all hover:text-contrast" > {{ formatMessage(analyticsChartMessages.seeAnnouncement) }} diff --git a/apps/frontend/src/components/analytics-dashboard/analytics-chart/analytics-chart-plot/AnalyticsChartTooltip.vue b/apps/frontend/src/components/analytics-dashboard/analytics-chart/analytics-chart-plot/AnalyticsChartTooltip.vue index 8f70acc3b..c51fda5d5 100644 --- a/apps/frontend/src/components/analytics-dashboard/analytics-chart/analytics-chart-plot/AnalyticsChartTooltip.vue +++ b/apps/frontend/src/components/analytics-dashboard/analytics-chart/analytics-chart-plot/AnalyticsChartTooltip.vue @@ -18,7 +18,7 @@ ({{ durationLabel }}) - + {{ previousRangeLabel }} {{ formatMessage(analyticsChartMessages.previousPeriodShort) }} @@ -197,6 +197,7 @@ function getEntryAriaLabel(entry: AnalyticsChartTooltipEntry) { const ONE_DAY_MS = 24 * 60 * 60 * 1000 const ONE_HOUR_MS = 60 * 60 * 1000 const ONE_MINUTE_MS = 60 * 1000 +const DATE_LOCALE = 'en-US' function formatRangeLabel( start: Date, @@ -225,13 +226,13 @@ function formatRangeLabel( } if (includeTime) { - const startLabel = new Intl.DateTimeFormat(undefined, startOptions).format(start) - const endLabel = new Intl.DateTimeFormat(undefined, timeOptions).format(end) + const startLabel = new Intl.DateTimeFormat(DATE_LOCALE, startOptions).format(start) + const endLabel = new Intl.DateTimeFormat(DATE_LOCALE, timeOptions).format(end) const range = `${startLabel}–${endLabel}` if (!showTrailingYear) return range - const yearLabel = new Intl.DateTimeFormat(undefined, { year: 'numeric' }).format(end) + const yearLabel = new Intl.DateTimeFormat(DATE_LOCALE, { year: 'numeric' }).format(end) return `${range}, ${yearLabel}` } @@ -244,13 +245,13 @@ function formatRangeLabel( endOptions = { day: 'numeric' } } - const startLabel = new Intl.DateTimeFormat(undefined, startOptions).format(start) - const endLabel = new Intl.DateTimeFormat(undefined, endOptions).format(end) + const startLabel = new Intl.DateTimeFormat(DATE_LOCALE, startOptions).format(start) + const endLabel = new Intl.DateTimeFormat(DATE_LOCALE, endOptions).format(end) const range = `${startLabel}–${endLabel}` if (!showTrailingYear) return range - const yearLabel = new Intl.DateTimeFormat(undefined, { year: 'numeric' }).format(end) + const yearLabel = new Intl.DateTimeFormat(DATE_LOCALE, { year: 'numeric' }).format(end) return `${range}, ${yearLabel}` } diff --git a/apps/frontend/src/components/analytics-dashboard/analytics-chart/analytics-chart-plot/use-analytics-chart-events.ts b/apps/frontend/src/components/analytics-dashboard/analytics-chart/analytics-chart-plot/use-analytics-chart-events.ts index 4ac5d3e9d..c0ca6489c 100644 --- a/apps/frontend/src/components/analytics-dashboard/analytics-chart/analytics-chart-plot/use-analytics-chart-events.ts +++ b/apps/frontend/src/components/analytics-dashboard/analytics-chart/analytics-chart-plot/use-analytics-chart-events.ts @@ -40,7 +40,6 @@ export function useAnalyticsChartEvents( placeholderData: [], refetchOnMount: 'always', retry: false, - staleTime: 0, }) const localAnalyticsChartEvents = computed(() => analyticsEvents.value ?? []) diff --git a/apps/frontend/src/components/analytics-dashboard/analytics-chart/analytics-chart-utils.ts b/apps/frontend/src/components/analytics-dashboard/analytics-chart/analytics-chart-utils.ts index 2e5420755..a472c532c 100644 --- a/apps/frontend/src/components/analytics-dashboard/analytics-chart/analytics-chart-utils.ts +++ b/apps/frontend/src/components/analytics-dashboard/analytics-chart/analytics-chart-utils.ts @@ -109,7 +109,7 @@ function getRegionDisplayNames(locale: string): Intl.DisplayNames | null { function formatCountryCode(countryCode: string, formatMessage: FormatMessage): string { const normalized = countryCode.trim().toUpperCase() if (normalized === OTHER_COUNTRY_CODE) { - return formatMessage(analyticsMessages.unknown) + return formatMessage(analyticsMessages.other) } if (!REGION_CODE_PATTERN.test(normalized)) { @@ -146,6 +146,9 @@ export function formatBreakdownLabel( normalizedLowercaseValue === 'other' || normalizedLowercaseValue === 'unknown' ) { + if (selectedBreakdown === 'country') { + return formatMessage(analyticsMessages.other) + } return formatMessage(analyticsMessages.unknown) } if (selectedBreakdown === 'country') { @@ -753,7 +756,7 @@ export function formatMetricValue( case 'playtime': { const hours = value / 3600 return formatMessage(analyticsStatCardMessages.playtimeHours, { - hours: hours.toFixed(1), + hours: Math.abs(hours) < 1 ? hours.toFixed(2) : hours.toFixed(1), }) } case 'views': @@ -770,7 +773,11 @@ function formatSmallAxisNumber(value: number): string { } const formattedValue = Math.abs(value) < 1 ? value.toFixed(2) : value.toFixed(1) - return formattedValue.replace(/\.?0+$/, '') + return trimTrailingFractionZeros(formattedValue) +} + +function trimTrailingFractionZeros(value: string): string { + return value.replace(/(\.\d*?)0+$/, '$1').replace(/\.$/, '') } const COMPACT_AXIS_UNITS = [ @@ -814,7 +821,7 @@ function formatCompactAxisValue(value: number): string { return String(truncatedValue) } - return roundedValue.toFixed(fractionDigitCount).replace(/\.?0+$/, '') + return trimTrailingFractionZeros(roundedValue.toFixed(fractionDigitCount)) } export function formatAxisValue( diff --git a/apps/frontend/src/components/analytics-dashboard/analytics-messages.ts b/apps/frontend/src/components/analytics-dashboard/analytics-messages.ts index 460151bde..e16df0e50 100644 --- a/apps/frontend/src/components/analytics-dashboard/analytics-messages.ts +++ b/apps/frontend/src/components/analytics-dashboard/analytics-messages.ts @@ -224,6 +224,19 @@ export const analyticsGraphTitleMessages = defineMessages({ }) export const analyticsStatCardMessages = defineMessages({ + monetizationBannerTitle: { + id: 'analytics.stat.monetization-banner.title', + defaultMessage: 'How does monetization work?', + }, + monetizationBannerBody: { + id: 'analytics.stat.monetization-banner.body', + defaultMessage: + 'Only views and downloads made through Modrinth are eligible for monetization and must pass fraud-prevention filtering. Modrinth App downloads also require the user to be logged in. Because all projects have a similar ratio of monetized downloads, your revenue would not meaningfully change if all downloads were counted.', + }, + monetizationBannerLearnMore: { + id: 'analytics.stat.monetization-banner.learn-more', + defaultMessage: 'Learn more', + }, revenueValue: { id: 'analytics.stat.revenue-value', defaultMessage: '${value}', diff --git a/apps/frontend/src/components/analytics-dashboard/analytics-table/analytics-table-columns.ts b/apps/frontend/src/components/analytics-dashboard/analytics-table/analytics-table-columns.ts index 2ed74ec54..1f5f414d0 100644 --- a/apps/frontend/src/components/analytics-dashboard/analytics-table/analytics-table-columns.ts +++ b/apps/frontend/src/components/analytics-dashboard/analytics-table/analytics-table-columns.ts @@ -66,6 +66,7 @@ export function buildAnalyticsTableColumns({ key: getAnalyticsTableBreakdownColumnKey(breakdown), label: getAnalyticsTableBreakdownColumnLabel(breakdown, formatMessage), enableSorting: true, + width: breakdown === 'project' ? '25%' : undefined, }) } } @@ -75,6 +76,7 @@ export function buildAnalyticsTableColumns({ key: 'project', label: formatAnalyticsBreakdownLabel('project', formatMessage), enableSorting: true, + width: '25%', }) } diff --git a/apps/frontend/src/components/analytics-dashboard/analytics-table/analytics-table-formatting.ts b/apps/frontend/src/components/analytics-dashboard/analytics-table/analytics-table-formatting.ts index e233ef544..256d57c54 100644 --- a/apps/frontend/src/components/analytics-dashboard/analytics-table/analytics-table-formatting.ts +++ b/apps/frontend/src/components/analytics-dashboard/analytics-table/analytics-table-formatting.ts @@ -40,10 +40,12 @@ export function formatAnalyticsTableCompactPlaytime( formatMessage: FormatMessage, ): string { const totalSeconds = Math.max(0, Math.round(value)) + const hours = totalSeconds / SECONDS_PER_HOUR + const fractionDigits = hours < 1 ? 2 : 1 return formatMessage(analyticsStatCardMessages.playtimeHours, { - hours: (totalSeconds / SECONDS_PER_HOUR).toLocaleString(undefined, { - minimumFractionDigits: 1, - maximumFractionDigits: 1, + hours: hours.toLocaleString(undefined, { + minimumFractionDigits: fractionDigits, + maximumFractionDigits: fractionDigits, }), }) } diff --git a/apps/frontend/src/components/analytics-dashboard/analytics-table/analytics-table-row-builder.ts b/apps/frontend/src/components/analytics-dashboard/analytics-table/analytics-table-row-builder.ts index 714506a0c..04c7d0a12 100644 --- a/apps/frontend/src/components/analytics-dashboard/analytics-table/analytics-table-row-builder.ts +++ b/apps/frontend/src/components/analytics-dashboard/analytics-table/analytics-table-row-builder.ts @@ -12,6 +12,7 @@ import { getSliceCount, } from '../analytics-chart/analytics-chart-utils' import type { FormatMessage } from '../analytics-messages' +import { analyticsMessages } from '../analytics-messages' import { ALL_BREAKDOWN_VALUE, COMBINED_BREAKDOWN_LABEL_SEPARATOR, @@ -64,6 +65,8 @@ export function buildAnalyticsTableRows({ const timeRange = fetchRequest.time_range const sliceCount = getSliceCount(timeRange, timeSlices.length) + const currentTimeSlices = + timeSlices.length > sliceCount ? timeSlices.slice(timeSlices.length - sliceCount) : timeSlices const includeDate = mode === 'date_breakdown' const breakdownDisplayValues = new Map