Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 23 additions & 32 deletions assets/js/dashboard/stats/graph/fetch-top-stats.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Metric } from '../../../types/query-api'
import {
DashboardState,
dashboardStateDefaultValue,
Expand All @@ -7,7 +8,7 @@ import { ComparisonMode, DashboardPeriod } from '../../dashboard-time-periods'
import { PlausibleSite, siteContextDefaultValue } from '../../site-context'
import { StatsQuery } from '../../stats-query'
import { remapToApiFilters } from '../../util/filters'
import { chooseMetrics, MetricDef, topStatsQueries } from './fetch-top-stats'
import { chooseMetrics, topStatsQueries } from './fetch-top-stats'

const aGoalFilter = ['is', 'goal', ['any goal']] as Filter
const aPageFilter = ['is', 'page', ['/any/page']] as Filter
Expand Down Expand Up @@ -41,7 +42,7 @@ type TestCase = [
Pick<DashboardState, 'filters' | 'period'> &
Partial<{ site?: Pick<PlausibleSite, 'revenueGoals'> }>,
/** expected metrics */
MetricDef[],
Metric[],
/** expected queries */
[StatsQuery, null | StatsQuery]
]
Expand All @@ -50,10 +51,7 @@ const cases: TestCase[] = [
[
'realtime and goal filter',
{ period: DashboardPeriod.realtime, filters: [aGoalFilter] },
[
{ key: 'visitors', label: 'Unique conversions (last 30 min)' },
{ key: 'events', label: 'Total conversions (last 30 min)' }
],
['visitors', 'events'],
[
{
date_range: DashboardPeriod.realtime_30m,
Expand All @@ -70,10 +68,7 @@ const cases: TestCase[] = [
[
'realtime',
{ period: DashboardPeriod.realtime, filters: [] },
[
{ key: 'visitors', label: 'Unique visitors (last 30 min)' },
{ key: 'pageviews', label: 'Pageviews (last 30 min)' }
],
['visitors', 'pageviews'],
[
{
date_range: DashboardPeriod.realtime_30m,
Expand All @@ -100,11 +95,11 @@ const cases: TestCase[] = [
}
},
[
{ key: 'visitors', label: 'Unique conversions' },
{ key: 'events', label: 'Total conversions' },
{ key: 'total_revenue', label: 'Total revenue' },
{ key: 'average_revenue', label: 'Average revenue' },
{ key: 'conversion_rate', label: 'Conversion rate' }
'visitors',
'events',
'total_revenue',
'average_revenue',
'conversion_rate'
],
[
{
Expand All @@ -128,11 +123,7 @@ const cases: TestCase[] = [
[
'goal filter',
{ period: aPeriodNotRealtime, filters: [aGoalFilter] },
[
{ key: 'visitors', label: 'Unique conversions' },
{ key: 'events', label: 'Total conversions' },
{ key: 'conversion_rate', label: 'Conversion rate' }
],
['visitors', 'events', 'conversion_rate'],
[
{
date_range: aPeriodNotRealtime,
Expand All @@ -153,12 +144,12 @@ const cases: TestCase[] = [
filters: [aPageFilter]
},
[
{ key: 'visitors', label: 'Unique visitors' },
{ key: 'visits', label: 'Total visits' },
{ key: 'pageviews', label: 'Total pageviews' },
{ key: 'bounce_rate', label: 'Bounce rate' },
{ key: 'scroll_depth', label: 'Scroll depth' },
{ key: 'time_on_page', label: 'Time on page' }
'visitors',
'visits',
'pageviews',
'bounce_rate',
'scroll_depth',
'time_on_page'
],

[
Expand All @@ -185,12 +176,12 @@ const cases: TestCase[] = [
'default',
{ period: aPeriodNotRealtime, filters: [] },
[
{ key: 'visitors', label: 'Unique visitors' },
{ key: 'visits', label: 'Total visits' },
{ key: 'pageviews', label: 'Total pageviews' },
{ key: 'views_per_visit', label: 'Views per visit' },
{ key: 'bounce_rate', label: 'Bounce rate' },
{ key: 'visit_duration', label: 'Visit duration' }
'visitors',
'visits',
'pageviews',
'views_per_visit',
'bounce_rate',
'visit_duration'
],
[
{
Expand Down
76 changes: 38 additions & 38 deletions assets/js/dashboard/stats/graph/fetch-top-stats.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Metric } from '../../../types/query-api'
import * as api from '../../api'
import { DashboardState } from '../../dashboard-state'
import { getMetricLabel } from '../metrics'
import {
ComparisonMode,
DashboardPeriod,
Expand All @@ -17,7 +18,7 @@ import {

export function topStatsQueries(
dashboardState: DashboardState,
metrics: MetricDef[]
metrics: Metric[]
): [StatsQuery, StatsQuery | null] {
let currentVisitorsQuery = null

Expand Down Expand Up @@ -53,70 +54,69 @@ export async function fetchTopStats(
currentVisitorsPromise
])

return formatTopStatsData(topStatsResponse, currentVisitorsResponse, metrics)
const metricLabelSuffix = isRealTimeDashboard(dashboardState)
? ' (last 30 min)'
: ''

const formattedMetrics = metrics.map((key) => ({
key,
label: `${getMetricLabel(key, {
hasConversionGoalFilter: hasConversionGoalFilter(dashboardState)
})}${metricLabelSuffix}`
}))

return formatTopStatsData(
topStatsResponse,
currentVisitorsResponse,
formattedMetrics
)
}

export type MetricDef = { key: Metric; label: string }

export function chooseMetrics(
site: Pick<PlausibleSite, 'revenueGoals'>,
dashboardState: DashboardState
): MetricDef[] {
const revenueMetrics: MetricDef[] =
site.revenueGoals.length > 0
? [
{ key: 'total_revenue', label: 'Total revenue' },
{ key: 'average_revenue', label: 'Average revenue' }
]
: []
): Metric[] {
const revenueMetrics: Metric[] =
site.revenueGoals.length > 0 ? ['total_revenue', 'average_revenue'] : []

if (
isRealTimeDashboard(dashboardState) &&
hasConversionGoalFilter(dashboardState)
) {
return [
{ key: 'visitors', label: 'Unique conversions (last 30 min)' },
{ key: 'events', label: 'Total conversions (last 30 min)' }
]
return ['visitors', 'events']
} else if (isRealTimeDashboard(dashboardState)) {
return [
{ key: 'visitors', label: 'Unique visitors (last 30 min)' },
{ key: 'pageviews', label: 'Pageviews (last 30 min)' }
]
return ['visitors', 'pageviews']
} else if (hasConversionGoalFilter(dashboardState)) {
return [
{ key: 'visitors', label: 'Unique conversions' },
{ key: 'events', label: 'Total conversions' },
...revenueMetrics,
{ key: 'conversion_rate', label: 'Conversion rate' }
]
return ['visitors', 'events', ...revenueMetrics, 'conversion_rate']
} else if (hasPageFilter(dashboardState)) {
return [
{ key: 'visitors', label: 'Unique visitors' },
{ key: 'visits', label: 'Total visits' },
{ key: 'pageviews', label: 'Total pageviews' },
{ key: 'bounce_rate', label: 'Bounce rate' },
{ key: 'scroll_depth', label: 'Scroll depth' },
{ key: 'time_on_page', label: 'Time on page' }
'visitors',
'visits',
'pageviews',
'bounce_rate',
'scroll_depth',
'time_on_page'
]
} else {
return [
{ key: 'visitors', label: 'Unique visitors' },
{ key: 'visits', label: 'Total visits' },
{ key: 'pageviews', label: 'Total pageviews' },
{ key: 'views_per_visit', label: 'Views per visit' },
{ key: 'bounce_rate', label: 'Bounce rate' },
{ key: 'visit_duration', label: 'Visit duration' }
'visitors',
'visits',
'pageviews',
'views_per_visit',
'bounce_rate',
'visit_duration'
]
}
}

function constructTopStatsQuery(
dashboardState: DashboardState,
metrics: MetricDef[]
metrics: Metric[]
): StatsQuery {
const reportParams: ReportParams = {
metrics: metrics.map((m) => m.key),
metrics,
include: { imports_meta: true }
}

Expand Down
37 changes: 37 additions & 0 deletions assets/js/dashboard/stats/metrics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Metric } from '../../types/query-api'

export const getMetricLabel = (
metric: Metric,
{ hasConversionGoalFilter }: { hasConversionGoalFilter: boolean }
): string => {
Comment on lines +3 to +6
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This signature works for replacing Top Stats, but with breakdowns in mind, I would like to propose:

type MetricLabelOverrides = {
  default: Record<Metric, string>,
  conversionGoalFilter: Record<Metric, string>,
  realtime: Record<Metric, string>
}

export const getMetricLabel = (
  metric: Metric,
  dashboardState: DashboardState,
  // Not sure what's best for MetricLabelOverrides:
  // * an object like {default: Record<Metric, string>, conversionGoalFilter: Record<Metric, string>, realtime: ...}
  // OR
  // * a function (metric, dashboardState) => string
  overrides: MetricLabelOverrides
): string => {...}

Here are a few reasons why:

  • In most goal-filtered breakdown reports, Unique conversions is just Conversions
  • In entry/exit pages reports, visits are labelled Total entrances/exits
  • In the conversions (goals breakdown) report, visitors is labelled Uniques
  • ...

Not saying this should be a part of this PR, but perhaps passing dashboardState instead of {hasConversionGoalFilter} could be a better starting point?

switch (metric) {
case 'visitors':
return hasConversionGoalFilter ? 'Unique conversions' : 'Unique visitors'
case 'events':
return hasConversionGoalFilter ? 'Total conversions' : 'Total events'
case 'visits':
return 'Total visits'
case 'pageviews':
return 'Total pageviews'
case 'views_per_visit':
return 'Views per visit'
case 'bounce_rate':
return 'Bounce rate'
case 'visit_duration':
return 'Visit duration'
case 'time_on_page':
return 'Time on page'
case 'scroll_depth':
return 'Scroll depth'
case 'conversion_rate':
return 'Conversion rate'
case 'total_revenue':
return 'Total revenue'
case 'average_revenue':
return 'Average revenue'
case 'percentage':
return 'Percentage'
case 'group_conversion_rate':
return 'Conversion rate'
}
}
Loading