@@ -8,7 +8,7 @@
|
|||||||
{{ analytics.error.value }}
|
{{ analytics.error.value }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="!isInitialized || analytics.loading.value" class="universal-card">
|
<div v-else-if="analytics.loading.value" class="universal-card">
|
||||||
<h2>
|
<h2>
|
||||||
<span class="label__title">Loading analytics...</span>
|
<span class="label__title">Loading analytics...</span>
|
||||||
</h2>
|
</h2>
|
||||||
@@ -315,6 +315,7 @@ import {
|
|||||||
Card,
|
Card,
|
||||||
DropdownSelect,
|
DropdownSelect,
|
||||||
useCompactNumber,
|
useCompactNumber,
|
||||||
|
useDebugLogger,
|
||||||
useFormatMoney,
|
useFormatMoney,
|
||||||
useFormatNumber,
|
useFormatNumber,
|
||||||
} from '@modrinth/ui'
|
} from '@modrinth/ui'
|
||||||
@@ -332,6 +333,7 @@ import {
|
|||||||
intToRgba,
|
intToRgba,
|
||||||
} from '~/utils/analytics.js'
|
} from '~/utils/analytics.js'
|
||||||
|
|
||||||
|
const debug = useDebugLogger('ChartDisplay')
|
||||||
const formatNumber = useFormatNumber()
|
const formatNumber = useFormatNumber()
|
||||||
const { formatCompactNumber } = useCompactNumber()
|
const { formatCompactNumber } = useCompactNumber()
|
||||||
const formatMoney = useFormatMoney()
|
const formatMoney = useFormatMoney()
|
||||||
@@ -339,6 +341,8 @@ const formatMoney = useFormatMoney()
|
|||||||
const router = useNativeRouter()
|
const router = useNativeRouter()
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
|
|
||||||
|
debug('setup start', { server: import.meta.server, client: import.meta.client })
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
projects?: any[]
|
projects?: any[]
|
||||||
@@ -359,6 +363,11 @@ const props = withDefaults(
|
|||||||
|
|
||||||
const projects = computed(() => props.projects || [])
|
const projects = computed(() => props.projects || [])
|
||||||
|
|
||||||
|
debug('projects from props', {
|
||||||
|
count: projects.value.length,
|
||||||
|
ids: projects.value.map((p: any) => p.id),
|
||||||
|
})
|
||||||
|
|
||||||
// const selectedChart = ref('downloads')
|
// const selectedChart = ref('downloads')
|
||||||
const selectedChart = computed({
|
const selectedChart = computed({
|
||||||
get: () => {
|
get: () => {
|
||||||
@@ -438,46 +447,47 @@ const isUsingProjectColors = computed({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const startDate = ref(dayjs().startOf('day'))
|
const defaultRange = props.ranges.find(
|
||||||
const endDate = ref(dayjs().endOf('day'))
|
(r) => r.getLabel([dayjs(), dayjs()]) === 'Previous 30 days',
|
||||||
const timeResolution = ref(30)
|
)!
|
||||||
const isInitialized = ref(false)
|
const initialDates = defaultRange.getDates(dayjs())
|
||||||
|
|
||||||
|
const internalRange: Ref<RangeObject> = ref(defaultRange)
|
||||||
|
const startDate = ref(initialDates.startDate)
|
||||||
|
const endDate = ref(initialDates.endDate)
|
||||||
|
const timeResolution = ref(defaultRange.timeResolution)
|
||||||
|
|
||||||
|
debug('default range initialized', {
|
||||||
|
range: defaultRange.getLabel([dayjs(), dayjs()]),
|
||||||
|
startDate: startDate.value.toISOString(),
|
||||||
|
endDate: endDate.value.toISOString(),
|
||||||
|
timeResolution: timeResolution.value,
|
||||||
|
})
|
||||||
|
|
||||||
onBeforeMount(() => {
|
onBeforeMount(() => {
|
||||||
// Load cached data and range from localStorage - cache.
|
debug('onBeforeMount')
|
||||||
if (import.meta.client) {
|
if (import.meta.client) {
|
||||||
const rangeLabel = localStorage.getItem('analyticsSelectedRange')
|
const rangeLabel = localStorage.getItem('analyticsSelectedRange')
|
||||||
|
debug('localStorage range', { rangeLabel })
|
||||||
if (rangeLabel) {
|
if (rangeLabel) {
|
||||||
const range = props.ranges.find((r) => r.getLabel([dayjs(), dayjs()]) === rangeLabel)!
|
const range = props.ranges.find((r) => r.getLabel([dayjs(), dayjs()]) === rangeLabel)
|
||||||
|
|
||||||
if (range !== undefined) {
|
if (range) {
|
||||||
internalRange.value = range
|
internalRange.value = range
|
||||||
const ranges = range.getDates(dayjs())
|
const dates = range.getDates(dayjs())
|
||||||
timeResolution.value = range.timeResolution
|
timeResolution.value = range.timeResolution
|
||||||
startDate.value = ranges.startDate
|
startDate.value = dates.startDate
|
||||||
endDate.value = ranges.endDate
|
endDate.value = dates.endDate
|
||||||
|
debug('range overridden from localStorage', {
|
||||||
|
startDate: dates.startDate.toISOString(),
|
||||||
|
endDate: dates.endDate.toISOString(),
|
||||||
|
timeResolution: range.timeResolution,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
if (internalRange.value === null) {
|
|
||||||
internalRange.value = props.ranges.find(
|
|
||||||
(r) => r.getLabel([dayjs(), dayjs()]) === 'Previous 30 days',
|
|
||||||
)!
|
|
||||||
}
|
|
||||||
|
|
||||||
const ranges = selectedRange.value.getDates(dayjs())
|
|
||||||
startDate.value = ranges.startDate
|
|
||||||
endDate.value = ranges.endDate
|
|
||||||
timeResolution.value = selectedRange.value.timeResolution
|
|
||||||
|
|
||||||
isInitialized.value = true
|
|
||||||
})
|
|
||||||
|
|
||||||
const internalRange: Ref<RangeObject> = ref(null as unknown as RangeObject)
|
|
||||||
|
|
||||||
const selectedRange = computed({
|
const selectedRange = computed({
|
||||||
get: () => {
|
get: () => {
|
||||||
return internalRange.value
|
return internalRange.value
|
||||||
@@ -499,6 +509,7 @@ const selectedRange = computed({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
debug('calling useFetchAllAnalytics')
|
||||||
const analytics = useFetchAllAnalytics(
|
const analytics = useFetchAllAnalytics(
|
||||||
resetCharts,
|
resetCharts,
|
||||||
projects,
|
projects,
|
||||||
@@ -507,9 +518,15 @@ const analytics = useFetchAllAnalytics(
|
|||||||
startDate,
|
startDate,
|
||||||
endDate,
|
endDate,
|
||||||
timeResolution,
|
timeResolution,
|
||||||
isInitialized,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
debug('awaiting analytics.fetch()')
|
||||||
|
await analytics.fetch()
|
||||||
|
debug('analytics.fetch() resolved', {
|
||||||
|
loading: analytics.loading.value,
|
||||||
|
error: analytics.error.value,
|
||||||
|
})
|
||||||
|
|
||||||
const formattedCategorySubtitle = computed(() => {
|
const formattedCategorySubtitle = computed(() => {
|
||||||
return (
|
return (
|
||||||
selectedRange.value?.getLabel([dayjs(startDate.value), dayjs(endDate.value)]) ?? 'Loading...'
|
selectedRange.value?.getLabel([dayjs(startDate.value), dayjs(endDate.value)]) ?? 'Loading...'
|
||||||
|
|||||||
@@ -1,15 +1,23 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<ChartDisplay :projects="projects ?? undefined" :personal="true" />
|
<Suspense>
|
||||||
|
<ChartDisplay :projects="projects" :personal="true" />
|
||||||
|
<template #fallback>
|
||||||
|
<div class="universal-card">
|
||||||
|
<h2><span class="label__title">Loading analytics...</span></h2>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { injectModrinthClient } from '@modrinth/ui'
|
import { injectModrinthClient, useDebugLogger } from '@modrinth/ui'
|
||||||
import { useQuery } from '@tanstack/vue-query'
|
|
||||||
|
|
||||||
import ChartDisplay from '~/components/ui/charts/ChartDisplay.vue'
|
import ChartDisplay from '~/components/ui/charts/ChartDisplay.vue'
|
||||||
|
|
||||||
|
const debug = useDebugLogger('analytics.vue')
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
middleware: 'auth',
|
middleware: 'auth',
|
||||||
})
|
})
|
||||||
@@ -22,9 +30,9 @@ const auth = await useAuth()
|
|||||||
const client = injectModrinthClient()
|
const client = injectModrinthClient()
|
||||||
const id = auth.value?.user?.id
|
const id = auth.value?.user?.id
|
||||||
|
|
||||||
const { data: projects } = useQuery({
|
debug('auth resolved', { id })
|
||||||
queryKey: computed(() => ['user', id, 'projects']),
|
|
||||||
queryFn: () => client.labrinth.users_v2.getProjects(id),
|
const projects = await client.labrinth.users_v2.getProjects(id)
|
||||||
enabled: computed(() => !!id),
|
|
||||||
})
|
debug('projects resolved', { count: projects?.length })
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -195,7 +195,7 @@ const { data: transparencyInformation } = useQuery({
|
|||||||
queryFn: () => client.labrinth.payouts_v3.getPlatformRevenue(),
|
queryFn: () => client.labrinth.payouts_v3.getPlatformRevenue(),
|
||||||
})
|
})
|
||||||
|
|
||||||
const platformRevenue = computed(() => (transparencyInformation.value as any)?.all_time)
|
const platformRevenue = computed(() => Number((transparencyInformation.value as any)?.all_time))
|
||||||
const platformRevenueData = computed(
|
const platformRevenueData = computed(
|
||||||
() => (transparencyInformation.value as any)?.data?.slice(0, 5) ?? [],
|
() => (transparencyInformation.value as any)?.data?.slice(0, 5) ?? [],
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { injectI18n } from '@modrinth/ui'
|
import { injectI18n, useDebugLogger } from '@modrinth/ui'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
|
|
||||||
@@ -314,8 +314,15 @@ export const useFetchAllAnalytics = (
|
|||||||
startDate = ref(dayjs().subtract(30, 'days')),
|
startDate = ref(dayjs().subtract(30, 'days')),
|
||||||
endDate = ref(dayjs()),
|
endDate = ref(dayjs()),
|
||||||
timeResolution = ref(1440),
|
timeResolution = ref(1440),
|
||||||
isInitialized = ref(false),
|
|
||||||
) => {
|
) => {
|
||||||
|
const debug = useDebugLogger('useFetchAllAnalytics')
|
||||||
|
debug('init', {
|
||||||
|
projectCount: projects.value?.length,
|
||||||
|
personalRevenue,
|
||||||
|
startDate: startDate.value?.toISOString(),
|
||||||
|
endDate: endDate.value?.toISOString(),
|
||||||
|
})
|
||||||
|
|
||||||
const downloadData = ref(null)
|
const downloadData = ref(null)
|
||||||
const viewData = ref(null)
|
const viewData = ref(null)
|
||||||
const revenueData = ref(null)
|
const revenueData = ref(null)
|
||||||
@@ -340,7 +347,22 @@ export const useFetchAllAnalytics = (
|
|||||||
revenue: processRevAnalytics(revenueData.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) => {
|
const fetchData = async (query) => {
|
||||||
|
debug('fetchData called', { query })
|
||||||
const normalQuery = new URLSearchParams(query)
|
const normalQuery = new URLSearchParams(query)
|
||||||
const revenueQuery = new URLSearchParams(query)
|
const revenueQuery = new URLSearchParams(query)
|
||||||
|
|
||||||
@@ -355,6 +377,7 @@ export const useFetchAllAnalytics = (
|
|||||||
loading.value = true
|
loading.value = true
|
||||||
error.value = null
|
error.value = null
|
||||||
|
|
||||||
|
debug('fetching all 5 endpoints...')
|
||||||
const responses = await Promise.all([
|
const responses = await Promise.all([
|
||||||
useFetchAnalytics(`analytics/downloads?${qs}`),
|
useFetchAnalytics(`analytics/downloads?${qs}`),
|
||||||
useFetchAnalytics(`analytics/views?${qs}`),
|
useFetchAnalytics(`analytics/views?${qs}`),
|
||||||
@@ -362,16 +385,21 @@ export const useFetchAllAnalytics = (
|
|||||||
useFetchAnalytics(`analytics/countries/downloads?${qs}`),
|
useFetchAnalytics(`analytics/countries/downloads?${qs}`),
|
||||||
useFetchAnalytics(`analytics/countries/views?${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,
|
||||||
|
})
|
||||||
|
|
||||||
// collect project ids from projects.value into a set
|
|
||||||
const projectIds = new Set()
|
const projectIds = new Set()
|
||||||
if (projects.value) {
|
if (projects.value) {
|
||||||
projects.value.forEach((p) => projectIds.add(p.id))
|
projects.value.forEach((p) => projectIds.add(p.id))
|
||||||
} else {
|
} else {
|
||||||
// if projects.value is not set, we assume that we want all project ids
|
|
||||||
Object.keys(responses[0] || {}).forEach((id) => projectIds.add(id))
|
Object.keys(responses[0] || {}).forEach((id) => projectIds.add(id))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
debug('filtering to projectIds', { count: projectIds.size })
|
||||||
|
|
||||||
const filterProjectIds = (data) => {
|
const filterProjectIds = (data) => {
|
||||||
const filtered = {}
|
const filtered = {}
|
||||||
Object.entries(data).forEach(([id, values]) => {
|
Object.entries(data).forEach(([id, values]) => {
|
||||||
@@ -389,43 +417,27 @@ export const useFetchAllAnalytics = (
|
|||||||
downloadsByCountry.value = responses[3] || {}
|
downloadsByCountry.value = responses[3] || {}
|
||||||
viewsByCountry.value = responses[4] || {}
|
viewsByCountry.value = responses[4] || {}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
debug('fetchData error', e)
|
||||||
error.value = e
|
error.value = e
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
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(
|
watch(
|
||||||
[
|
[() => startDate.value, () => endDate.value, () => timeResolution.value, () => projects.value],
|
||||||
() => startDate.value,
|
(newVals, oldVals) => {
|
||||||
() => endDate.value,
|
debug('watch triggered', { new: newVals, old: oldVals })
|
||||||
() => timeResolution.value,
|
fetch()
|
||||||
() => projects.value,
|
|
||||||
() => isInitialized.value,
|
|
||||||
],
|
|
||||||
async () => {
|
|
||||||
if (!isInitialized.value) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
|
|
||||||
await fetchData(q)
|
|
||||||
|
|
||||||
if (onDataRefresh) {
|
|
||||||
onDataRefresh()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
immediate: true,
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -474,6 +486,6 @@ export const useFetchAllAnalytics = (
|
|||||||
totalData,
|
totalData,
|
||||||
loading,
|
loading,
|
||||||
error,
|
error,
|
||||||
isInitialized,
|
fetch,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user