From 2c9bf58d1f06bcbc9f78b475ff45c660c9fb7718 Mon Sep 17 00:00:00 2001 From: Truman Gao <106889354+tdgao@users.noreply.github.com> Date: Mon, 8 Jun 2026 16:03:28 -0600 Subject: [PATCH] feat: various analytics updates (#6330) * feat: add button to view user analytics * feat: add "Your projects" preset selection * feat: fix revenue rounding for values under 1 and show full values for all statcards with tooltip * fix: sum rounded value instead of raw value for tooltip total if it's under 1 * fix: show decimal in playtime statcard if under 1 hrs * feat: disable playtime statcard for purely plugin projects * refactor: pnpm prepr --- .../analytics-chart-plot/index.vue | 10 +- .../analytics-dashboard/analytics-messages.ts | 8 + .../analytics-route-query.ts | 39 ++-- .../query-builder/index.vue | 178 +++++++++++++++--- .../stat-cards/StatCard.vue | 4 +- .../stat-cards/StatCards.vue | 86 ++++++++- .../use-analytics-route-sync.ts | 11 +- apps/frontend/src/locales/en-US/index.json | 9 + apps/frontend/src/pages/user/[user].vue | 18 ++ .../analytics/analytics-project-utils.ts | 26 +++ .../providers/analytics/analytics-types.ts | 2 + .../src/providers/analytics/analytics.ts | 151 ++++++++++++++- 12 files changed, 491 insertions(+), 51 deletions(-) diff --git a/apps/frontend/src/components/analytics-dashboard/analytics-chart/analytics-chart-plot/index.vue b/apps/frontend/src/components/analytics-dashboard/analytics-chart/analytics-chart-plot/index.vue index f12b7159a..f9639f742 100644 --- a/apps/frontend/src/components/analytics-dashboard/analytics-chart/analytics-chart-plot/index.vue +++ b/apps/frontend/src/components/analytics-dashboard/analytics-chart/analytics-chart-plot/index.vue @@ -168,6 +168,14 @@ const { onRangeSelected: (start, end, groupBy) => emit('range-select', start, end, groupBy), }) +function getTooltipTotalMetricValue(value: number): number { + if (props.activeStat === 'revenue' && Math.abs(value) < 1) { + return Math.round(value * 100) / 100 + } + + return value +} + const hoverTotalValue = computed(() => { if (hoverState.sliceIndex === null) return 0 const sliceIndex = hoverState.sliceIndex @@ -176,7 +184,7 @@ const hoverTotalValue = computed(() => { return props.currentLegendEntries.reduce((sum, legendEntry) => { if (legendEntry.hidden) return sum const dataset = props.chartDatasetById.get(legendEntry.id) - return sum + (dataset?.data[sliceIndex] ?? 0) + return sum + getTooltipTotalMetricValue(dataset?.data[sliceIndex] ?? 0) }, 0) }) diff --git a/apps/frontend/src/components/analytics-dashboard/analytics-messages.ts b/apps/frontend/src/components/analytics-dashboard/analytics-messages.ts index e16df0e50..2fd92fc53 100644 --- a/apps/frontend/src/components/analytics-dashboard/analytics-messages.ts +++ b/apps/frontend/src/components/analytics-dashboard/analytics-messages.ts @@ -39,6 +39,14 @@ export const analyticsMessages = defineMessages({ id: 'analytics.project.all', defaultMessage: 'All projects', }, + yourProjects: { + id: 'analytics.project.your', + defaultMessage: 'Your projects', + }, + userProjects: { + id: 'analytics.project.user', + defaultMessage: "{username}'s projects", + }, selectProjects: { id: 'analytics.project.select', defaultMessage: 'Select projects', diff --git a/apps/frontend/src/components/analytics-dashboard/analytics-route-query.ts b/apps/frontend/src/components/analytics-dashboard/analytics-route-query.ts index c3df9e8e0..8fdff6900 100644 --- a/apps/frontend/src/components/analytics-dashboard/analytics-route-query.ts +++ b/apps/frontend/src/components/analytics-dashboard/analytics-route-query.ts @@ -146,6 +146,7 @@ const QUERY_KEY_TABLE_SORT = 'a_table_sort' const QUERY_KEY_TABLE_SORT_DIRECTION = 'a_table_sort_direction' const QUERY_KEY_LEGACY_GRAPH_TOP_BREAKDOWN_FILTER = 'a_top_breakdown' const QUERY_KEY_LEGACY_GRAPH_LEGEND_EXPANSION = 'a_legend_expanded' +const PROJECT_SELECTION_ALL_QUERY_VALUE = 'all' const URL_FILTER_CATEGORIES: Exclude[] = [ 'project_status', @@ -405,9 +406,10 @@ export function buildDefaultAnalyticsGraphState( export function buildDefaultAnalyticsQueryBuilderState( availableProjectIds: string[], + defaultProjectIds: string[] = availableProjectIds, ): AnalyticsQueryBuilderState { return { - selectedProjectIds: [...availableProjectIds], + selectedProjectIds: [...defaultProjectIds], selectedTimeframeMode: DEFAULT_TIMEFRAME_MODE, selectedTimeframe: DEFAULT_TIMEFRAME_PRESET, selectedLastTimeframeAmount: DEFAULT_LAST_TIMEFRAME_AMOUNT, @@ -415,7 +417,7 @@ export function buildDefaultAnalyticsQueryBuilderState( selectedCustomTimeframeStartDate: getDefaultCustomStartDate(), selectedCustomTimeframeEndDate: getDefaultCustomEndDate(), selectedGroupBy: DEFAULT_GROUP_BY_PRESET, - selectedBreakdowns: getDefaultAnalyticsBreakdownPresets(availableProjectIds), + selectedBreakdowns: getDefaultAnalyticsBreakdownPresets(defaultProjectIds), selectedFilters: buildEmptySelectedFilters(), } } @@ -475,12 +477,16 @@ export function getAnalyticsBreakdownPresetForProjectSelection( export function isAnalyticsQueryBuilderStateDefault( state: AnalyticsQueryBuilderState, availableProjectIds: string[], + defaultProjectIds: string[] = availableProjectIds, ): boolean { - const defaultState = buildDefaultAnalyticsQueryBuilderState(availableProjectIds) + const defaultState = buildDefaultAnalyticsQueryBuilderState( + availableProjectIds, + defaultProjectIds, + ) const areDefaultProjectsSelected = - availableProjectIds.length === 0 + defaultProjectIds.length === 0 ? state.selectedProjectIds.length === 0 - : areAllProjectsSelected(state.selectedProjectIds, availableProjectIds) + : areAllProjectsSelected(state.selectedProjectIds, defaultProjectIds) return ( areDefaultProjectsSelected && @@ -666,13 +672,19 @@ export function readAnalyticsTableSortState( export function readAnalyticsQueryBuilderState( query: LocationQuery, availableProjectIds: string[], + defaultProjectIds: string[] = availableProjectIds, ): AnalyticsQueryBuilderState { - const defaultState = buildDefaultAnalyticsQueryBuilderState(availableProjectIds) + const defaultState = buildDefaultAnalyticsQueryBuilderState( + availableProjectIds, + defaultProjectIds, + ) const selectedProjectIdsFromQuery = parseListQueryValue(query[QUERY_KEY_PROJECT_IDS]) - const selectedProjectIds = - selectedProjectIdsFromQuery.length > 0 - ? selectedProjectIdsFromQuery - : defaultState.selectedProjectIds + let selectedProjectIds = defaultState.selectedProjectIds + if (selectedProjectIdsFromQuery.includes(PROJECT_SELECTION_ALL_QUERY_VALUE)) { + selectedProjectIds = [...availableProjectIds] + } else if (selectedProjectIdsFromQuery.length > 0) { + selectedProjectIds = selectedProjectIdsFromQuery + } const selectedFilters = buildEmptySelectedFilters() for (const category of URL_FILTER_CATEGORIES) { @@ -779,14 +791,17 @@ export function buildAnalyticsQueryBuilderRouteQuery( state: AnalyticsQueryBuilderState, availableProjectIds: string[], graphState?: AnalyticsGraphState, + defaultProjectIds: string[] = availableProjectIds, ): MutableRouteQuery { const nextRouteQuery = { ...currentRouteQuery, } as MutableRouteQuery - const projectIdsQueryValue = areAllProjectsSelected(state.selectedProjectIds, availableProjectIds) + const projectIdsQueryValue = areAllProjectsSelected(state.selectedProjectIds, defaultProjectIds) ? undefined - : serializeListQueryValue(state.selectedProjectIds) + : areAllProjectsSelected(state.selectedProjectIds, availableProjectIds) + ? PROJECT_SELECTION_ALL_QUERY_VALUE + : serializeListQueryValue(state.selectedProjectIds) const isCustomTimeframeMode = state.selectedTimeframeMode === 'custom_range' || state.selectedTimeframeMode === 'custom_datetime_range' diff --git a/apps/frontend/src/components/analytics-dashboard/query-builder/index.vue b/apps/frontend/src/components/analytics-dashboard/query-builder/index.vue index 2d0a20d2e..aa4842ed6 100644 --- a/apps/frontend/src/components/analytics-dashboard/query-builder/index.vue +++ b/apps/frontend/src/components/analytics-dashboard/query-builder/index.vue @@ -69,6 +69,29 @@