From 02363c27a21bfff13d44015d06564c02197a3f93 Mon Sep 17 00:00:00 2001 From: Truman Gao <106889354+tdgao@users.noreply.github.com> Date: Fri, 29 May 2026 15:41:43 -0600 Subject: [PATCH] fix: download threshold (#6242) * fix: download threshold * fix: download threshold for projects select * refactor: pnpm prepr * feat: handle facets not adding count * feat: remove getting facets download count field entirely * feat: update facets to match new backend shape --- .../query-builder/QueryFilter.vue | 45 +++++++---- .../query-builder/index.vue | 2 +- .../analytics/analytics-data-utils.ts | 79 +++++++++++++++++++ .../analytics/analytics-filter-utils.ts | 69 +++++----------- .../src/providers/analytics/analytics.ts | 53 +++++++++++-- .../api-client/src/modules/labrinth/types.ts | 45 +++++------ 6 files changed, 195 insertions(+), 98 deletions(-) diff --git a/apps/frontend/src/components/analytics-dashboard/query-builder/QueryFilter.vue b/apps/frontend/src/components/analytics-dashboard/query-builder/QueryFilter.vue index de635ed3c..ef0d77f44 100644 --- a/apps/frontend/src/components/analytics-dashboard/query-builder/QueryFilter.vue +++ b/apps/frontend/src/components/analytics-dashboard/query-builder/QueryFilter.vue @@ -232,7 +232,13 @@ import { type AnalyticsFilterValueCategory = Exclude type GameVersionType = 'release' | 'all' type SetDropdownFilterValues = (values: string[]) => void -type ApplyDownloadsThreshold = (setSelectedValues: SetDropdownFilterValues) => void +type DownloadsThresholdSelection = { + categoryKey: DownloadsThresholdFilterCategory + selectedValues: string[] +} +type ApplyDownloadsThreshold = ( + setSelectedValues: SetDropdownFilterValues, +) => DownloadsThresholdSelection | null type CloseDownloadsThresholdMenu = (event?: Event) => void const props = withDefaults( @@ -763,48 +769,46 @@ function compareOptionalDateStringsDescending( function applyGameVersionDownloadsThreshold(setSelectedValues: SetDropdownFilterValues) { const threshold = gameVersionDownloadsThreshold.value if (threshold === null) { - return + return null } const selectedValues = gameVersionFilterOptions.value .filter((gameVersion) => { - return (gameVersionDownloadsByVersion.value.get(gameVersion.value) ?? 0) >= threshold + return (gameVersionDownloadsByVersion.value.get(gameVersion.value) ?? 0) > threshold }) .map((gameVersion) => gameVersion.value) - setDownloadsThresholdSelectedValues('game_version', selectedValues, setSelectedValues) + return setDownloadsThresholdSelectedValues('game_version', selectedValues, setSelectedValues) } function applyCountryDownloadsThreshold(setSelectedValues: SetDropdownFilterValues) { const threshold = countryDownloadsThreshold.value if (threshold === null) { - return + return null } const selectedValues = countryFilterOptions.value .filter((country) => { - return ( - (countryDownloadsByCode.value.get(country.value.trim().toUpperCase()) ?? 0) >= threshold - ) + return (countryDownloadsByCode.value.get(country.value.trim().toUpperCase()) ?? 0) > threshold }) .map((country) => country.value) - setDownloadsThresholdSelectedValues('country', selectedValues, setSelectedValues) + return setDownloadsThresholdSelectedValues('country', selectedValues, setSelectedValues) } function applyProjectVersionDownloadsThreshold(setSelectedValues: SetDropdownFilterValues) { const threshold = projectVersionDownloadsThreshold.value if (threshold === null) { - return + return null } const selectedValues = projectVersionFilterOptions.value .filter((version) => { - return (projectVersionDownloadsById.value.get(version.value) ?? 0) >= threshold + return (projectVersionDownloadsById.value.get(version.value) ?? 0) > threshold }) .map((version) => version.value) - setDownloadsThresholdSelectedValues('version_id', selectedValues, setSelectedValues) + return setDownloadsThresholdSelectedValues('version_id', selectedValues, setSelectedValues) } function setCountryDownloadsThreshold( @@ -876,12 +880,18 @@ function setDownloadsThresholdSelectedValues( categoryKey: DownloadsThresholdFilterCategory, selectedValues: string[], setSelectedValues: SetDropdownFilterValues, -) { +): DownloadsThresholdSelection { + const normalizedSelectedValues = normalizeSelectedFilterValues(categoryKey, selectedValues, []) downloadsThresholdSelections.value = { ...downloadsThresholdSelections.value, - [categoryKey]: normalizeSelectedFilterValues(categoryKey, selectedValues, []), + [categoryKey]: normalizedSelectedValues, } setSelectedValues(selectedValues) + + return { + categoryKey, + selectedValues: normalizedSelectedValues, + } } function clearDownloadsThreshold(categoryKey: DownloadsThresholdFilterCategory) { @@ -923,8 +933,13 @@ async function runDownloadsThresholdQuery( closeMenu: CloseDownloadsThresholdMenu, event?: KeyboardEvent, ) { - applyDownloadsThreshold(setSelectedValues) + const selection = applyDownloadsThreshold(setSelectedValues) closeMenu(event) + if (selection) { + const nextFilters = cloneSelectedFilters(draftSelectedFilters.value) + nextFilters[selection.categoryKey] = selection.selectedValues + draftSelectedFilters.value = nextFilters + } await scheduleSelectedFiltersCommit() await refreshAnalyticsQuery() } 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 1a4dd6e45..422dea931 100644 --- a/apps/frontend/src/components/analytics-dashboard/query-builder/index.vue +++ b/apps/frontend/src/components/analytics-dashboard/query-builder/index.vue @@ -753,7 +753,7 @@ function applyProjectDownloadsThreshold(threshold: number | null) { } const projectIds = projects.value - .filter((project) => (projectDownloadsById.value.get(project.id) ?? 0) >= threshold) + .filter((project) => (projectDownloadsById.value.get(project.id) ?? 0) > threshold) .map((project) => project.id) projectDownloadsThresholdProjectIds.value = projectIds diff --git a/apps/frontend/src/providers/analytics/analytics-data-utils.ts b/apps/frontend/src/providers/analytics/analytics-data-utils.ts index ae450dac1..f78d89499 100644 --- a/apps/frontend/src/providers/analytics/analytics-data-utils.ts +++ b/apps/frontend/src/providers/analytics/analytics-data-utils.ts @@ -476,6 +476,85 @@ export function computeTotals( return totals } +export function getProjectDownloadsByIdFromTimeSlices( + timeSlices: Labrinth.Analytics.v3.TimeSlice[], +): Map { + const projectDownloadsById = new Map() + + for (const timeSlice of timeSlices) { + for (const dataPoint of timeSlice) { + if (!isProjectAnalyticsPoint(dataPoint) || dataPoint.metric_kind !== 'downloads') { + continue + } + + projectDownloadsById.set( + dataPoint.source_project, + (projectDownloadsById.get(dataPoint.source_project) ?? 0) + dataPoint.downloads, + ) + } + } + + return projectDownloadsById +} + +function getDownloadFieldCountsFromTimeSlices( + timeSlices: Labrinth.Analytics.v3.TimeSlice[], + getKey: ( + dataPoint: Extract, + ) => string | null | undefined, +): Map { + const downloadsByValue = new Map() + + for (const timeSlice of timeSlices) { + for (const dataPoint of timeSlice) { + if (!isProjectAnalyticsPoint(dataPoint) || dataPoint.metric_kind !== 'downloads') { + continue + } + + const key = getKey(dataPoint)?.trim() + if (!key) { + continue + } + + downloadsByValue.set(key, (downloadsByValue.get(key) ?? 0) + dataPoint.downloads) + } + } + + return downloadsByValue +} + +export function getProjectVersionDownloadsByIdFromTimeSlices( + timeSlices: Labrinth.Analytics.v3.TimeSlice[], +): Map { + return getDownloadFieldCountsFromTimeSlices(timeSlices, (dataPoint) => dataPoint.version_id) +} + +export function getGameVersionDownloadsByVersionFromTimeSlices( + timeSlices: Labrinth.Analytics.v3.TimeSlice[], +): Map { + return getDownloadFieldCountsFromTimeSlices(timeSlices, (dataPoint) => dataPoint.game_version) +} + +export function getCountryDownloadsByCodeFromTimeSlices( + timeSlices: Labrinth.Analytics.v3.TimeSlice[], +): Map { + const countryDownloadsByCode = new Map() + const downloadsByCountry = getDownloadFieldCountsFromTimeSlices( + timeSlices, + (dataPoint) => dataPoint.country, + ) + + for (const [country, downloads] of downloadsByCountry.entries()) { + const countryCode = country.toUpperCase() + countryDownloadsByCode.set( + countryCode, + (countryDownloadsByCode.get(countryCode) ?? 0) + downloads, + ) + } + + return countryDownloadsByCode +} + export function cloneAnalyticsFetchRequest( fetchRequest: Labrinth.Analytics.v3.FetchRequest | null, ): Labrinth.Analytics.v3.FetchRequest | null { diff --git a/apps/frontend/src/providers/analytics/analytics-filter-utils.ts b/apps/frontend/src/providers/analytics/analytics-filter-utils.ts index 41473fb13..d0efbce7d 100644 --- a/apps/frontend/src/providers/analytics/analytics-filter-utils.ts +++ b/apps/frontend/src/providers/analytics/analytics-filter-utils.ts @@ -174,28 +174,8 @@ function getEmptyAnalyticsFacetsFilterOptionSummary(): AnalyticsFacetsFilterOpti } } -function getAnalyticsFacetValues( - facets: Labrinth.Analytics.v3.AnalyticsFacet[] | null | undefined, -): T[] { - return facets?.map((facet) => facet.value) ?? [] -} - -function getAnalyticsFacetDownloadsByValue( - facets: Labrinth.Analytics.v3.AnalyticsFacet[] | null | undefined, - getKey: (value: T) => string, -): Map { - const downloadsByValue = new Map() - for (const facet of facets ?? []) { - const key = getKey(facet.value) - if (key.length === 0) { - continue - } - - const downloads = Number.isFinite(facet.downloads) ? facet.downloads : 0 - downloadsByValue.set(key, (downloadsByValue.get(key) ?? 0) + downloads) - } - - return downloadsByValue +function getAnalyticsFacetValues(facets: T[] | null | undefined): T[] { + return facets ? [...facets] : [] } export function getAnalyticsFacetsFilterOptionSummary( @@ -205,15 +185,18 @@ export function getAnalyticsFacetsFilterOptionSummary( return getEmptyAnalyticsFacetsFilterOptionSummary() } - const downloadCountries = getAnalyticsFacetValues(facets.project_downloads.country) - const downloadGameVersions = getAnalyticsFacetValues(facets.project_downloads.game_version) - const downloadLoaders = getAnalyticsFacetValues(facets.project_downloads.loader) - const downloadVersionIds = getAnalyticsFacetValues(facets.project_downloads.version_id) - const viewCountries = getAnalyticsFacetValues(facets.project_views.country) - const playtimeCountries = getAnalyticsFacetValues(facets.project_playtime.country) - const playtimeGameVersions = getAnalyticsFacetValues(facets.project_playtime.game_version) - const playtimeLoaders = getAnalyticsFacetValues(facets.project_playtime.loader) - const playtimeVersionIds = getAnalyticsFacetValues(facets.project_playtime.version_id) + const projectDownloadFacets = facets.project_downloads + const projectViewFacets = facets.project_views + const projectPlaytimeFacets = facets.project_playtime + const downloadCountries = getAnalyticsFacetValues(projectDownloadFacets?.country) + const downloadGameVersions = getAnalyticsFacetValues(projectDownloadFacets?.game_version) + const downloadLoaders = getAnalyticsFacetValues(projectDownloadFacets?.loader) + const downloadVersionIds = getAnalyticsFacetValues(projectDownloadFacets?.version_id) + const viewCountries = getAnalyticsFacetValues(projectViewFacets?.country) + const playtimeCountries = getAnalyticsFacetValues(projectPlaytimeFacets?.country) + const playtimeGameVersions = getAnalyticsFacetValues(projectPlaytimeFacets?.game_version) + const playtimeLoaders = getAnalyticsFacetValues(projectPlaytimeFacets?.loader) + const playtimeVersionIds = getAnalyticsFacetValues(projectPlaytimeFacets?.version_id) const countries = new Set([...viewCountries, ...downloadCountries, ...playtimeCountries]) const gameVersions = new Set([...downloadGameVersions, ...playtimeGameVersions]) const loaderTypes = new Set() @@ -230,8 +213,8 @@ export function getAnalyticsFacetsFilterOptionSummary( .map((country) => country.trim().toUpperCase()) .filter((country) => country.length > 0), ), - downloadSources: sortStringValues(getAnalyticsFacetValues(facets.project_downloads.user_agent)), - downloadReasons: sortStringValues(getAnalyticsFacetValues(facets.project_downloads.reason)), + downloadSources: sortStringValues(getAnalyticsFacetValues(projectDownloadFacets?.user_agent)), + downloadReasons: sortStringValues(getAnalyticsFacetValues(projectDownloadFacets?.reason)), gameVersions: sortStringValues( [...gameVersions] .map((gameVersion) => gameVersion.trim()) @@ -239,22 +222,10 @@ export function getAnalyticsFacetsFilterOptionSummary( ), loaderTypes: sortStringValues([...loaderTypes]), versionIds: sortStringValues([...new Set([...downloadVersionIds, ...playtimeVersionIds])]), - projectDownloadsById: getAnalyticsFacetDownloadsByValue( - facets.project_downloads.project_id, - (projectId) => projectId.trim(), - ), - projectVersionDownloadsById: getAnalyticsFacetDownloadsByValue( - facets.project_downloads.version_id, - (versionId) => versionId.trim(), - ), - gameVersionDownloadsByVersion: getAnalyticsFacetDownloadsByValue( - facets.project_downloads.game_version, - (gameVersion) => gameVersion.trim(), - ), - countryDownloadsByCode: getAnalyticsFacetDownloadsByValue( - facets.project_downloads.country, - (country) => country.trim().toUpperCase(), - ), + projectDownloadsById: new Map(), + projectVersionDownloadsById: new Map(), + gameVersionDownloadsByVersion: new Map(), + countryDownloadsByCode: new Map(), } } diff --git a/apps/frontend/src/providers/analytics/analytics.ts b/apps/frontend/src/providers/analytics/analytics.ts index c89b38eab..dcb3f63a9 100644 --- a/apps/frontend/src/providers/analytics/analytics.ts +++ b/apps/frontend/src/providers/analytics/analytics.ts @@ -42,7 +42,11 @@ import { fetchAnalyticsTimeSlices, getAnalyticsProjectEventsInTimeRange, getAnalyticsTimeframeDurationMs, + getCountryDownloadsByCodeFromTimeSlices, + getGameVersionDownloadsByVersionFromTimeSlices, getPercentChange, + getProjectDownloadsByIdFromTimeSlices, + getProjectVersionDownloadsByIdFromTimeSlices, isAnalyticsFetchRequestReady, isRevenueHourlyGroupBy, REVENUE_MIN_TIMEFRAME_MS, @@ -1017,6 +1021,35 @@ export function createAnalyticsDashboardContext( gcTime: ANALYTICS_FILTER_OPTIONS_GC_TIME_MS, }) + const { data: analyticsDownloadCountTimeSlices } = useQuery({ + queryKey: computed(() => [ + 'analytics', + 'dashboard', + analyticsQueryUserId.value, + 'filter-options', + 'download-counts-fallback', + analyticsFacetsRequest.value, + queryRefreshTimestamp.value, + ]), + queryFn: () => { + const nextRequest = analyticsFacetsRequest.value + if (!isAnalyticsFetchRequestReady(nextRequest)) { + return [] + } + + return fetchAnalyticsTimeSlices(nextRequest, (request) => + client.labrinth.analytics_v3.fetch(request), + ) + }, + enabled: computed( + () => + hasFetchedAnalyticsFilterOptions.value && + isAnalyticsFetchRequestReady(analyticsFacetsRequest.value), + ), + placeholderData: [], + gcTime: ANALYTICS_FILTER_OPTIONS_GC_TIME_MS, + }) + const { data: filterOptionProjectVersions, isFetched: hasFetchedFilterOptionProjectVersions } = useQuery({ queryKey: computed(() => [ @@ -1245,17 +1278,21 @@ export function createAnalyticsDashboardContext( } return versionProjectIconUrls }) - const projectDownloadsById = computed( - () => analyticsFacetsFilterOptionSummary.value.projectDownloadsById, + const downloadCountTimeSlices = computed(() => { + const countTimeSlices = analyticsDownloadCountTimeSlices.value ?? [] + return countTimeSlices.length > 0 ? countTimeSlices : timeSlices.value + }) + const projectDownloadsById = computed(() => + getProjectDownloadsByIdFromTimeSlices(downloadCountTimeSlices.value), ) - const projectVersionDownloadsById = computed( - () => analyticsFacetsFilterOptionSummary.value.projectVersionDownloadsById, + const projectVersionDownloadsById = computed(() => + getProjectVersionDownloadsByIdFromTimeSlices(downloadCountTimeSlices.value), ) - const countryDownloadsByCode = computed( - () => analyticsFacetsFilterOptionSummary.value.countryDownloadsByCode, + const countryDownloadsByCode = computed(() => + getCountryDownloadsByCodeFromTimeSlices(downloadCountTimeSlices.value), ) - const gameVersionDownloadsByVersion = computed( - () => analyticsFacetsFilterOptionSummary.value.gameVersionDownloadsByVersion, + const gameVersionDownloadsByVersion = computed(() => + getGameVersionDownloadsByVersionFromTimeSlices(downloadCountTimeSlices.value), ) const selectedProjectIdSet = computed(() => new Set(selectedProjectIds.value)) diff --git a/packages/api-client/src/modules/labrinth/types.ts b/packages/api-client/src/modules/labrinth/types.ts index 172dcb2cb..2a3600868 100644 --- a/packages/api-client/src/modules/labrinth/types.ts +++ b/packages/api-client/src/modules/labrinth/types.ts @@ -375,41 +375,36 @@ export namespace Labrinth { facets: AnalyticsFacets } - export type AnalyticsFacet = { - value: T - downloads: number - } - export type AnalyticsFacets = { - project_views: ProjectViewsFacets - project_downloads: ProjectDownloadsFacets - project_playtime: ProjectPlaytimeFacets + project_views?: Partial + project_downloads?: Partial + project_playtime?: Partial } export type ProjectViewsFacets = { - domain: AnalyticsFacet[] - site_path: AnalyticsFacet[] - monetized: AnalyticsFacet[] - country: AnalyticsFacet[] + domain: string[] + site_path: string[] + monetized: boolean[] + country: string[] } export type ProjectDownloadsFacets = { - project_id: AnalyticsFacet[] - domain: AnalyticsFacet[] - user_agent: AnalyticsFacet[] - version_id: AnalyticsFacet[] - monetized: AnalyticsFacet[] - country: AnalyticsFacet[] - reason: AnalyticsFacet[] - game_version: AnalyticsFacet[] - loader: AnalyticsFacet[] + project_id: string[] + domain: string[] + user_agent: string[] + version_id: string[] + monetized: boolean[] + country: string[] + reason: DownloadReason[] + game_version: string[] + loader: string[] } export type ProjectPlaytimeFacets = { - version_id: AnalyticsFacet[] - loader: AnalyticsFacet[] - game_version: AnalyticsFacet[] - country: AnalyticsFacet[] + version_id: string[] + loader: string[] + game_version: string[] + country: string[] } export type TimeSlice = AnalyticsData[]