mirror of
https://github.com/modrinth/code.git
synced 2026-08-26 01:26:23 +00:00
feat: improve analytics dashboard (#5897)
* feat: implement cancel/apply for custom timeframe range picker
* feat: implement dot for showing todays date
* feat: add max date to be today and show todays date
* feat: if ratio mode, dont show total
* feat: implement show more batching excess lines into "Other" bucket
* refactor: pnpm prepr
* feat: add pick and plop for date range start/end dates
* feat: implement reset query button
* feat: clear button to clear breakdown
* feat: more aggressively trim allowed minimum group by option
* fix: dont show project status filter when from project settings/analytics
* fix: clear selected X above number when appropriate
* feat: graph style updates and dont show year in x axis unless more than 2 year timeframe
* fix: loading state to include legend in blur
* feat: add project icon to project select
* feat: filter out draft projects from analytics
* feat: implement multiselect sections headers, project select org sections, and project options icons
* feat: implement click and drag to select date range
* feat: implement windows history for query builder
* revert: no longer switch breakdown/filter option if same category
* feat: implement showing project for project version breakdown/filter when there are multiple projects
* feat: implement modrinth sided events
* fix: border radius
* feat: implement analytics range highlight
* fix: loading state showing empty state text
* refactor: pnpm prepr
* feat: improve dropdown filter bar and multiselect performance
* fix: multiselect keyboard use
* fix: graph overflow issues
* fix: loading state text on table
* feat: implement tooltip scroll
* fix: adjust charts event tooltip
* feat: shorten time to not repeat am/pm
* feat: implement query params for graph component settings
* fix: qa
* feat: add reset timeframe button
* fix: legend colors moving between metric by determining color based on only downloads metric index
* feat: implement auto switching temporarily to group by day for renvenue metric and disable revenue metric for time range < 2 days
* fix: change to > 1 day
* fix: custom timeframe picker
* feat: implement big performance improvement for table
* feat: implement hover on legend to highlight graph
* fix: defer commit in query builder/filter and style fixes
* feat: more performance optimization to analytics dashboard state, chart, and table
* feat: add tooltip for other item
* feat: improve custom time frame range select
* feat: implement analytics events admin page
* fix: switch column order
* pnpm prepr
* feat: implement mock analytics events
* feat: improve analytics events admin page
* feat: focus title input on analytics create event modal
* fix: remove labels annoying
* feat: hook up analytics events backend
* fix: type error
* feat: reduce combobox padding
* feat: reduce padding on multiselect
* feat: add overlay scrollbar for combobox
* feat: a bunch of style fixes to combobox, multiselect and dropdown filter bar
* feat: MORE PADDING fixes
* feat: use user_agent for download source
* Revert "feat: use user_agent for download source"
This reverts commit d6dc8a99f1.
* fix: query filter project version lag and borked virtualization
* feat: rename breakdown "none" to "project"
* feat: implement right side checkmark for multiselect
* feat: keep crossed out legend items still shown in tooltip but also crossed out
* fix: focus styles
* fix: focus styles pt2
* feat: implement filter by top 8
* fix: preview is incorrect when selecting same date in range date picker
* feat (playtest): cross out legend items in tooltip and allow hide/show in tooltip
* feat (playtest): table component controls what graph shows
* feat: change download source to use user_agent
* feat: fix click to cross out in legend
* feat: add hover legend item to highlight line in tooltip
* fix: export csv to always be dropdown
* feat: implement breakdown = none
* performance: frontend memory reduction
* performance: reduce memory usage from project versions query by keeping only whats necessary
* fix: table checked items not in graph if 0
* feat: add shift click to select a range in table
* performance: add caching for metric types so switching between them is snappy
* performance: batch analytics requests by 15 project ids, with 150 ms delay between, so backend is happy
* feat: add analytics table search
* refactor: pnpm prepr
* fix: query filter options not coming in from analytics fetch
* feat: remove breakdown = none when there are multiple projects
* feat: improve table sorting
* feat: sort projects in project dropdown
* fix: getting project name for project versions
* fix: add loading state for filter and parallel fetch
* performance: use precomputed map for project version options to remove first hover lag
* feat: dropdown filter always open on one side and improve styles
* fix: custom time range picker being weird
* refactor: pnpm prepr
* fix: add back in batch with 300ms interval for projects to prevent backend rate limiting
* performance: only do queries to populate graph first before other analytics queries
* fix: QA polish issues around style and copy
* feat: dont show select all when its just one item in section
* fix: bugs with ratio mode and hiding chart lines
* fix: adjust padding in combobox and multiselect and fix not unfocusing when deselect
* fix: small styles
* fix: polish admin analytic events
* fix: keep scroll position with selection action row appearing when selecting one
* feat: add subheading in graph for showing N items from table
* feat: add unmonetized explaination tooltip
* performance: implement limit on how many lines can be shown in graph
* feat: mobile pass
* refactor: pnpm prepr
* add clear button
* feat: add time in analytics event and normalize date/time so its correct to timezones
* fix: padding
* feat: implement show prev period toggle
* feat: extract TimeFramePicker to packages/ui
* fix: adjust style
* feat: keep table selected persisted in query parameter
* fix: style on prev item value in legend
* fix: when breakdown switches, reset selected series
* fix: tooltip styles
* feat: change project selection to reset to show top 8 only if reconciled down to 0 items
* feat: implement show top 8 button in graph subheading
* fix: rename download type to download reason
* fix: formatting label for table
* feat: persist table sort by and sort direction
* fix: show top 8 button in graph not defaulting to top 8 for other metrics
* feat: implement prev period analytics fetch into the same current period fetch by shifting start date
* refactor: pnpm prepr
* fix: remove number if its just top 1
* fix: brief select items empty state when switch breakdown
* feat: implement format table playtime column
* feat: update export csv filename
* feat: change playtime column to display in hours
* refactor: pnpm prepr
* fix: still download type in filter
* feat: update analytics tooltip
* fix: wrong all projects icon
* feat: force legend order and graph colour for monetization
* refactor: pnpm prepr
* fix: multiselect and combobox sizes
* fix: chart icon add hover delay
* feat: (to playtest) implement multiple breakdowns
* fix: couple UX things for multiple breakdown
* fix: cannot unpin on page click
* fix: multiple breakdown legend and tooltip labels
* feat: add right side checkmark for dropdown filtr bar
* feat: enabling prev period will cross out prev for current ones already crossed out
* feat-mobile: remove drag to select time frame in graph
* feat-mobile: dropdown filter to replace dropdown for submenus on small screen
* feat-mobile: time frame picker to use different start and end date pickers for mobile
* fix-mobile: fix multiselect scroll on mobile
* feat: consolidate is mobile ref into context
* fix-mobile: combobox and multiselect scroll bug when mobile search bar open, fix timeframe picker mobile pick date, and dropdown filter bar click outside to close
* fix-mobile: smaller metric card font
* fix: dropdown filter bar scroll while search
* feat: implement project side events
* feat: implement better mobile view design for query builder
* feat: handle events overflow
* small: add select none
* feat: remove clear project and breakdown
* fix: event icon hover color
* feat: default hide project events if there are multiple projects, and default show if only 1 project
* feat: implement analytics performance updates, including facets, and v3 user projects
* feat: grey out dimmed lined on legend item hover
* feat-mobile: style fixes
* add close on select prop
* feat: add close on select for time frame picker mobile
* feat: date picker default read only
* refactor: pnpm prepr
* feat: default to projects breakdown instead of no breakdown with multiple projects
* fix-mobile: improve graph touch interactions
* small: 2 sig figs on playtime
* feat: deduplicate version uploads that have same version number and are uploaded on same day
* fix: analytics events grouping causing overflow
* feat: improve performance on analytics events grouping
* fix: tooltip expanding page width briefly
* fix: prevent double tap to zoom on inputs
* feat: add click to show chart event for mobile
* fix: toggle not having touch manipulation
* fix: chart tooltip scroll in mobile
* fix: remove project breakdownoption as it is default breakdown when none are selected
* fix: dropdown filter bar briefly empty when switching pages in mobile
* feat: keep tooltip open after drag in mobile
* fix: using plural instead of single for project breakdown
* fix: date picker scrolling page after picking date in mobile by suppressing focus
* fix: callback to Organization instead of org id
* feat: improve chart tooltip date range label formatter to be much more consistent
* feat: tap to toggle event tooltip
* fix: add user select none on graph and fix zoom into download threshold input
* fix: frontend still filtering after backend already filters
* feat: fix emptys state height content shift
* fix: qa issues
* fix: a number of qa issues
- Hide project events based on visible project legend/table selection
- Filter project status events by end status and add explicit copy for approved, private, and unlisted
- Style Modrinth analytics events with blue icon, marker, guide, and range borders
- Add scroll fade shadows to analytics chart and event tooltips
- Show previous-period date range in the chart tooltip
- Make project breakdown conditional on multiple selected projects and allow no breakdown when none are selected
- Add breakdown selection actions and fix “Group by day” copy
* feat: implement graph controls dropdown
* fix date picker typing into time input
* fix: styles in events table
* small: style
* feat: implement using new backend facets route
* feat: implement user get all projects
* performance: deter non-critical fetches to after analytics is in
* fix: refreshing causes multiple projects to do breakdown=none
* performance: cache project version options to fix lag on open sub menu
* refactor: remove chart event height being controlled by parent
* feat: update controls dropdown to have fainter border
* fix: loading bar not fading away
* fix: cannot click in graph
* feat: dont conditionally show multiselect selection actions
* fix: z-index and padding issues
* fix: project events incorrectly toggling on for first page load
* feat: remove show more and show less in legend, always show all
* fix: playtime y axis labels
* feat: improve y axis formatting for playtime and others
* feat: use tabs for game version select, and remove prev period when change breakdown or project selection
* refactor: pnpm prepr
* feat: change hidden legend items to not contribute to ratio percentages
* feat: event icon consume scroll for tooltip panel
* feat: remove gap inside chart tooltip
* feat: add gap for date picker 2 calendar view
* feat: improve analytics events grouping logic for modrinth events to be close to target
* pnpm prepr
* fix: cant click in gap in toggle
* fix: bugs around selected series from table not persisting with timeframe or filter changes
* refactor: kabab case
* refactor: split up large analytics chart and table component files into smaller components and ts modules
* fix: legend is stale after resetting query
* refactor: split up giant analytics provider with utils
* i18n pass
* revert: format number composable change
* fix: playtime was choosing y axis ticks in seconds instead of hours
* refactor: rename folder that with components to match main component name
* refactor: same rename for analytics table for consistency
* refactor: name main components to index.vue and keep folder name as component name
* refactor: pnpm prepr
* refactor: rename types
* refactor: move query builder types into types file and move components into components/analytics-dashboard
* refactor: colocate query builder url with analytics-dashboard component
* refactor: pnpm prepr:frontend
* fix: download threshold not width fit
* fix: no option to see release/all game versions in selected filter dropdown
* fix: game version dropdown width
---------
Signed-off-by: Truman Gao <106889354+tdgao@users.noreply.github.com>
Co-authored-by: Calum H. (IMB11) <contact@cal.engineer>
This commit is contained in:
@@ -1,493 +0,0 @@
|
||||
import { injectI18n, useDebugLogger } from '@modrinth/ui'
|
||||
import dayjs from 'dayjs'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
import { useTheme } from '~/composables/nuxt-accessors.ts'
|
||||
|
||||
// note: build step can miss unix import for some reason, so
|
||||
// we have to import it like this
|
||||
|
||||
const { unix } = dayjs
|
||||
|
||||
export function useCountryNames(style = 'long') {
|
||||
const { locale } = injectI18n()
|
||||
const displayNames = computed(
|
||||
() => new Intl.DisplayNames([locale.value], { type: 'region', style }),
|
||||
)
|
||||
return function formatCountryName(code) {
|
||||
try {
|
||||
return displayNames.value.of(code) ?? code
|
||||
} catch {
|
||||
return code
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const countryCodeToName = (code) => {
|
||||
const formatCountryName = useCountryNames()
|
||||
|
||||
return formatCountryName(code)
|
||||
}
|
||||
|
||||
export const countryCodeToFlag = (code) => {
|
||||
if (code === 'XX') {
|
||||
return undefined
|
||||
}
|
||||
return `https://flagcdn.com/h240/${code.toLowerCase()}.png`
|
||||
}
|
||||
|
||||
export const formatTimestamp = (timestamp) => {
|
||||
return unix(timestamp).format()
|
||||
}
|
||||
|
||||
export const formatPercent = (value, sum) => {
|
||||
return `${((value / sum) * 100).toFixed(2)}%`
|
||||
}
|
||||
|
||||
const hashProjectId = (projectId) => {
|
||||
return projectId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0) % 30
|
||||
}
|
||||
|
||||
export const defaultColors = [
|
||||
'#ff496e', // Original: Bright pink
|
||||
'#ffa347', // Original: Bright orange
|
||||
'#1bd96a', // Original: Bright green
|
||||
'#4f9cff', // Original: Bright blue
|
||||
'#c78aff', // Original: Bright purple
|
||||
'#ffeb3b', // Added: Bright yellow
|
||||
'#00bcd4', // Added: Bright cyan
|
||||
'#ff5722', // Added: Bright red-orange
|
||||
'#9c27b0', // Added: Bright deep purple
|
||||
'#3f51b5', // Added: Bright indigo
|
||||
'#009688', // Added: Bright teal
|
||||
'#cddc39', // Added: Bright lime
|
||||
'#795548', // Added: Bright brown
|
||||
'#607d8b', // Added: Bright blue-grey
|
||||
]
|
||||
|
||||
/**
|
||||
* @param {string | number} value
|
||||
* @returns {string} color
|
||||
*/
|
||||
export const getDefaultColor = (value) => {
|
||||
if (typeof value === 'string') {
|
||||
value = hashProjectId(value)
|
||||
}
|
||||
return defaultColors[value % defaultColors.length]
|
||||
}
|
||||
|
||||
export const intToRgba = (color, projectId = 'Unknown', theme = 'dark', alpha = '1') => {
|
||||
const hash = hashProjectId(projectId)
|
||||
|
||||
if (!color || color === 0) {
|
||||
return getDefaultColor(hash)
|
||||
}
|
||||
|
||||
// if color is a string, return that instead
|
||||
if (typeof color === 'string') {
|
||||
return color
|
||||
}
|
||||
|
||||
// Extract RGB values
|
||||
let r = (color >> 16) & 255
|
||||
let g = (color >> 8) & 255
|
||||
let b = color & 255
|
||||
|
||||
// Hash function to alter color slightly based on project_id
|
||||
r = (r + hash) % 256
|
||||
g = (g + hash) % 256
|
||||
b = (b + hash) % 256
|
||||
|
||||
// Adjust brightness for theme
|
||||
const brightness = r * 0.299 + g * 0.587 + b * 0.114
|
||||
const threshold = theme === 'dark' ? 50 : 200
|
||||
if (theme === 'dark' && brightness < threshold) {
|
||||
// Increase brightness for dark theme
|
||||
r += threshold / 2
|
||||
g += threshold / 2
|
||||
b += threshold / 2
|
||||
} else if (theme === 'light' && brightness > threshold) {
|
||||
// Decrease brightness for light theme
|
||||
r -= threshold / 4
|
||||
g -= threshold / 4
|
||||
b -= threshold / 4
|
||||
}
|
||||
|
||||
// Ensure RGB values are within 0-255
|
||||
r = Math.min(255, Math.max(0, r))
|
||||
g = Math.min(255, Math.max(0, g))
|
||||
b = Math.min(255, Math.max(0, b))
|
||||
|
||||
return `rgba(${r}, ${g}, ${b}, ${alpha})`
|
||||
}
|
||||
|
||||
const emptyAnalytics = {
|
||||
sum: 0,
|
||||
len: 0,
|
||||
chart: {
|
||||
labels: [],
|
||||
data: [],
|
||||
sumData: [
|
||||
{
|
||||
name: '',
|
||||
data: [],
|
||||
},
|
||||
],
|
||||
colors: [],
|
||||
defaultColors: [],
|
||||
},
|
||||
projectIds: [],
|
||||
}
|
||||
|
||||
export const analyticsSetToCSVString = (analytics) => {
|
||||
if (!analytics) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const newline = '\n'
|
||||
const labels = analytics.chart.labels
|
||||
const projects = analytics.chart.data
|
||||
|
||||
const projectNames = projects.map((p) => p.name)
|
||||
|
||||
const header = ['Date', ...projectNames].join(',')
|
||||
|
||||
const data = labels.map((label, i) => {
|
||||
const values = projects.map((p) => p.data?.[i] || '')
|
||||
return [label, ...values].join(',')
|
||||
})
|
||||
|
||||
return [header, ...data].join(newline)
|
||||
}
|
||||
|
||||
export const processAnalytics = (category, projects, labelFn, sortFn, mapFn, chartName, theme) => {
|
||||
if (!category || !projects) {
|
||||
return emptyAnalytics
|
||||
}
|
||||
|
||||
// Get an intersection of category keys and project ids
|
||||
const projectIds = projects.map((p) => p.id)
|
||||
const loadedProjectIds = Object.keys(category).filter((id) => projectIds.includes(id))
|
||||
|
||||
if (!loadedProjectIds?.length) {
|
||||
return emptyAnalytics
|
||||
}
|
||||
|
||||
const loadedProjectData = loadedProjectIds.map((id) => category[id])
|
||||
|
||||
// Convert each project's data into a list of [unix_ts_str, number] pairs
|
||||
const projectData = loadedProjectData
|
||||
.map((data) => Object.entries(data))
|
||||
.map((data) => data.sort(sortFn))
|
||||
.map((data) => (mapFn ? data.map(mapFn) : data))
|
||||
|
||||
// Each project may not include the same timestamps, so we should use the union of all timestamps
|
||||
const timestamps = Array.from(
|
||||
new Set(projectData.flatMap((data) => data.map(([ts]) => ts))),
|
||||
).sort()
|
||||
|
||||
const chartData = projectData
|
||||
.map((data, i) => {
|
||||
const project = projects.find((p) => p.id === loadedProjectIds[i])
|
||||
if (!project) {
|
||||
throw new Error(`Project ${loadedProjectIds[i]} not found`)
|
||||
}
|
||||
|
||||
return {
|
||||
name: `${project.title}`,
|
||||
data: timestamps.map((ts) => {
|
||||
const entry = data.find(([ets]) => ets === ts)
|
||||
return entry ? entry[1] : 0
|
||||
}),
|
||||
id: project.id,
|
||||
color: project.color,
|
||||
}
|
||||
})
|
||||
.sort(
|
||||
(a, b) =>
|
||||
b.data.reduce((acc, cur) => acc + cur, 0) - a.data.reduce((acc, cur) => acc + cur, 0),
|
||||
)
|
||||
|
||||
const projectIdsSortedBySum = chartData.map((p) => p.id)
|
||||
|
||||
return {
|
||||
// The total count of all the values across all projects
|
||||
sum: projectData.reduce((acc, cur) => acc + cur.reduce((a, c) => a + c[1], 0), 0),
|
||||
len: timestamps.length,
|
||||
chart: {
|
||||
labels: timestamps.map(labelFn),
|
||||
data: chartData.map((x) => ({ name: x.name, data: x.data })),
|
||||
sumData: [
|
||||
{
|
||||
name: chartName,
|
||||
data: timestamps.map((ts) => {
|
||||
const entries = projectData.flat().filter(([ets]) => ets === ts)
|
||||
return entries.reduce((acc, cur) => acc + cur[1], 0)
|
||||
}),
|
||||
},
|
||||
],
|
||||
colors: projectData.map((_, i) => {
|
||||
const project = chartData[i]
|
||||
|
||||
return intToRgba(project.color, project.id, theme)
|
||||
}),
|
||||
defaultColors: projectData.map((_, i) => {
|
||||
const project = chartData[i]
|
||||
return getDefaultColor(project.id)
|
||||
}),
|
||||
},
|
||||
projectIds: projectIdsSortedBySum,
|
||||
}
|
||||
}
|
||||
|
||||
export const processAnalyticsByCountry = (category, projects, sortFn) => {
|
||||
if (!category || !projects) {
|
||||
return {
|
||||
sum: 0,
|
||||
len: 0,
|
||||
data: [],
|
||||
}
|
||||
}
|
||||
|
||||
// Get an intersection of category keys and project ids
|
||||
const projectIds = projects.map((p) => p.id)
|
||||
const loadedProjectIds = Object.keys(category).filter((id) => projectIds.includes(id))
|
||||
|
||||
if (!loadedProjectIds?.length) {
|
||||
return {
|
||||
sum: 0,
|
||||
len: 0,
|
||||
data: [],
|
||||
}
|
||||
}
|
||||
|
||||
const loadedProjectData = loadedProjectIds.map((id) => category[id])
|
||||
|
||||
// Convert each project's data into a list of [countrycode, number] pairs
|
||||
// Fold into a single list with summed values for each country over all projects
|
||||
|
||||
const countrySums = new Map()
|
||||
|
||||
loadedProjectData.forEach((data) => {
|
||||
Object.entries(data).forEach(([country, value]) => {
|
||||
const countryCode = country || 'XX'
|
||||
const current = countrySums.get(countryCode) || 0
|
||||
countrySums.set(countryCode, current + value)
|
||||
})
|
||||
})
|
||||
|
||||
const entries = Array.from(countrySums.entries())
|
||||
|
||||
return {
|
||||
sum: entries.reduce((acc, cur) => acc + cur[1], 0),
|
||||
len: entries.length,
|
||||
data: entries.sort(sortFn),
|
||||
}
|
||||
}
|
||||
|
||||
const sortCount = ([, a], [, b]) => b - a
|
||||
const sortTimestamp = ([a], [b]) => a - b
|
||||
const roundValue = ([ts, value]) => [ts, Math.round(parseFloat(value) * 100) / 100]
|
||||
|
||||
const processCountryAnalytics = (c, projects) => processAnalyticsByCountry(c, projects, sortCount)
|
||||
const processNumberAnalytics = (c, projects, theme) =>
|
||||
processAnalytics(c, projects, formatTimestamp, sortTimestamp, null, 'Downloads', theme)
|
||||
const processRevAnalytics = (c, projects, theme) =>
|
||||
processAnalytics(c, projects, formatTimestamp, sortTimestamp, roundValue, 'Revenue', theme)
|
||||
|
||||
const useFetchAnalytics = (
|
||||
url,
|
||||
baseOptions = {
|
||||
apiVersion: 3,
|
||||
},
|
||||
) => {
|
||||
return useBaseFetch(url, baseOptions)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Ref<any[]>} projects
|
||||
* @param {undefined | () => any} onDataRefresh
|
||||
*/
|
||||
export const useFetchAllAnalytics = (
|
||||
onDataRefresh,
|
||||
projects,
|
||||
selectedProjects,
|
||||
personalRevenue = false,
|
||||
startDate = ref(dayjs().subtract(30, 'days')),
|
||||
endDate = ref(dayjs()),
|
||||
timeResolution = ref(1440),
|
||||
) => {
|
||||
const debug = useDebugLogger('useFetchAllAnalytics')
|
||||
debug('init', {
|
||||
projectCount: projects.value?.length,
|
||||
personalRevenue,
|
||||
startDate: startDate.value?.toISOString(),
|
||||
endDate: endDate.value?.toISOString(),
|
||||
})
|
||||
|
||||
const downloadData = ref(null)
|
||||
const viewData = ref(null)
|
||||
const revenueData = ref(null)
|
||||
const downloadsByCountry = ref(null)
|
||||
const viewsByCountry = ref(null)
|
||||
const loading = ref(true)
|
||||
const error = ref(null)
|
||||
|
||||
const formattedData = computed(() => ({
|
||||
downloads: processNumberAnalytics(downloadData.value, selectedProjects.value),
|
||||
views: processNumberAnalytics(viewData.value, selectedProjects.value),
|
||||
revenue: processRevAnalytics(revenueData.value, selectedProjects.value),
|
||||
downloadsByCountry: processCountryAnalytics(downloadsByCountry.value, selectedProjects.value),
|
||||
viewsByCountry: processCountryAnalytics(viewsByCountry.value, selectedProjects.value),
|
||||
}))
|
||||
|
||||
const theme = useTheme()
|
||||
|
||||
const totalData = computed(() => ({
|
||||
downloads: processNumberAnalytics(downloadData.value, projects.value, theme.active),
|
||||
views: processNumberAnalytics(viewData.value, projects.value, theme.active),
|
||||
revenue: processRevAnalytics(revenueData.value, projects.value, theme.active),
|
||||
}))
|
||||
|
||||
const buildQuery = () => {
|
||||
const q = {
|
||||
start_date: startDate.value.toISOString(),
|
||||
end_date: endDate.value.toISOString(),
|
||||
resolution_minutes: timeResolution.value,
|
||||
}
|
||||
|
||||
if (projects.value?.length) {
|
||||
q.project_ids = JSON.stringify(projects.value.map((p) => p.id))
|
||||
}
|
||||
|
||||
return q
|
||||
}
|
||||
|
||||
const fetchData = async (query) => {
|
||||
debug('fetchData called', { query })
|
||||
const normalQuery = new URLSearchParams(query)
|
||||
const revenueQuery = new URLSearchParams(query)
|
||||
|
||||
if (personalRevenue) {
|
||||
revenueQuery.delete('project_ids')
|
||||
}
|
||||
|
||||
const qs = normalQuery.toString()
|
||||
const revenueQs = revenueQuery.toString()
|
||||
|
||||
try {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
debug('fetching all 5 endpoints...')
|
||||
const responses = await Promise.all([
|
||||
useFetchAnalytics(`analytics/downloads?${qs}`),
|
||||
useFetchAnalytics(`analytics/views?${qs}`),
|
||||
useFetchAnalytics(`analytics/revenue?${revenueQs}`),
|
||||
useFetchAnalytics(`analytics/countries/downloads?${qs}`),
|
||||
useFetchAnalytics(`analytics/countries/views?${qs}`),
|
||||
])
|
||||
debug('all 5 endpoints resolved', {
|
||||
downloads: Object.keys(responses[0] || {}).length,
|
||||
views: Object.keys(responses[1] || {}).length,
|
||||
revenue: Object.keys(responses[2] || {}).length,
|
||||
})
|
||||
|
||||
const projectIds = new Set()
|
||||
if (projects.value) {
|
||||
projects.value.forEach((p) => projectIds.add(p.id))
|
||||
} else {
|
||||
Object.keys(responses[0] || {}).forEach((id) => projectIds.add(id))
|
||||
}
|
||||
|
||||
debug('filtering to projectIds', { count: projectIds.size })
|
||||
|
||||
const filterProjectIds = (data) => {
|
||||
const filtered = {}
|
||||
Object.entries(data).forEach(([id, values]) => {
|
||||
if (projectIds.has(id)) {
|
||||
filtered[id] = values
|
||||
}
|
||||
})
|
||||
return filtered
|
||||
}
|
||||
|
||||
downloadData.value = filterProjectIds(responses[0] || {})
|
||||
viewData.value = filterProjectIds(responses[1] || {})
|
||||
revenueData.value = filterProjectIds(responses[2] || {})
|
||||
|
||||
downloadsByCountry.value = responses[3] || {}
|
||||
viewsByCountry.value = responses[4] || {}
|
||||
} catch (e) {
|
||||
debug('fetchData error', e)
|
||||
error.value = e
|
||||
} finally {
|
||||
loading.value = false
|
||||
debug('fetchData done, loading=false')
|
||||
}
|
||||
}
|
||||
|
||||
const fetch = async () => {
|
||||
debug('fetch() called', { projectCount: projects.value?.length })
|
||||
await fetchData(buildQuery())
|
||||
if (onDataRefresh) {
|
||||
onDataRefresh()
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
[() => startDate.value, () => endDate.value, () => timeResolution.value, () => projects.value],
|
||||
(newVals, oldVals) => {
|
||||
debug('watch triggered', { new: newVals, old: oldVals })
|
||||
fetch()
|
||||
},
|
||||
)
|
||||
|
||||
const validProjectIds = computed(() => {
|
||||
const ids = new Set()
|
||||
|
||||
if (downloadData.value) {
|
||||
Object.keys(downloadData.value).forEach((id) => ids.add(id))
|
||||
}
|
||||
|
||||
if (viewData.value) {
|
||||
Object.keys(viewData.value).forEach((id) => ids.add(id))
|
||||
}
|
||||
|
||||
if (revenueData.value) {
|
||||
// revenue will always have all project ids, but the ids may have an empty object or a ton of keys below a cent (0.00...) as values. We want to filter those out
|
||||
Object.entries(revenueData.value).forEach(([id, data]) => {
|
||||
if (Object.keys(data).length) {
|
||||
if (Object.values(data).some((v) => v >= 0.01)) {
|
||||
ids.add(id)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return Array.from(ids)
|
||||
})
|
||||
|
||||
return {
|
||||
// Configuration
|
||||
timeResolution,
|
||||
|
||||
startDate,
|
||||
endDate,
|
||||
|
||||
// Data
|
||||
downloadData,
|
||||
viewData,
|
||||
revenueData,
|
||||
downloadsByCountry,
|
||||
viewsByCountry,
|
||||
|
||||
// Computed state
|
||||
validProjectIds,
|
||||
formattedData,
|
||||
totalData,
|
||||
loading,
|
||||
error,
|
||||
fetch,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user