-
Notifications
You must be signed in to change notification settings - Fork 7.3k
Plot bounce rate and visit duration over time on the Overview page #4251
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
9195a4a
1f6f5ea
f9f0145
31b4c3e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| import { ListItem, Row, Select } from '@umami/react-zen'; | ||
| import { useMessages, useNavigation } from '@/components/hooks'; | ||
| import type { WebsiteChartMetric } from './WebsiteChart'; | ||
| import { DEFAULT_WEBSITE_CHART_METRIC } from './WebsiteChart'; | ||
|
|
||
| export function WebsiteChartMetricFilter() { | ||
| const { t, labels } = useMessages(); | ||
| const { router, query, updateParams } = useNavigation(); | ||
|
|
||
| const options: { id: WebsiteChartMetric; label: string }[] = [ | ||
| { id: 'pageviews', label: `${t(labels.visitors)} / ${t(labels.views)}` }, | ||
| { id: 'bouncerate', label: t(labels.bounceRate) }, | ||
| { id: 'visitduration', label: t(labels.visitDuration) }, | ||
| ]; | ||
|
|
||
| const selected = (query.metric as WebsiteChartMetric) ?? DEFAULT_WEBSITE_CHART_METRIC; | ||
|
|
||
| const handleChange = (value: string) => { | ||
| router.push(updateParams({ metric: value })); | ||
| }; | ||
|
|
||
| return ( | ||
| <Row> | ||
| <Select | ||
| value={selected} | ||
| onChange={handleChange} | ||
| popoverProps={{ placement: 'bottom right' }} | ||
| style={{ width: 180 }} | ||
| > | ||
| {options.map(({ id, label }) => ( | ||
| <ListItem key={id} id={id}> | ||
| {label} | ||
| </ListItem> | ||
| ))} | ||
| </Select> | ||
| </Row> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,16 +1,20 @@ | ||
| import { z } from 'zod'; | ||
| import { getCompareDate } from '@/lib/date'; | ||
| import { getQueryFilters, parseRequest } from '@/lib/request'; | ||
| import { json, unauthorized } from '@/lib/response'; | ||
| import { filterParams, withDateRange } from '@/lib/schema'; | ||
| import { canViewWebsite } from '@/permissions'; | ||
| import { getPageviewStats, getSessionStats } from '@/queries/sql'; | ||
| import { getPageviewStats, getSessionStats, getSessionStatsSeries } from '@/queries/sql'; | ||
|
|
||
| const seriesMetric = z.enum(['bouncerate', 'visitduration']); | ||
|
|
||
| export async function GET( | ||
| request: Request, | ||
| { params }: { params: Promise<{ websiteId: string }> }, | ||
| ) { | ||
| const schema = withDateRange({ | ||
| ...filterParams, | ||
| metric: seriesMetric.optional(), | ||
| }); | ||
|
|
||
| const { auth, query, error } = await parseRequest(request, schema); | ||
|
|
@@ -26,20 +30,39 @@ export async function GET( | |
| } | ||
|
|
||
| const filters = await getQueryFilters(query, websiteId); | ||
| // Only the bouncerate / visitduration metrics need the per-bucket session | ||
| // series. Skip the extra DB roundtrip for the default pageviews metric so a | ||
| // standard website-page load stays at two queries instead of three. | ||
| const wantsSessionSeries = query.metric === 'bouncerate' || query.metric === 'visitduration'; | ||
|
|
||
| const [pageviews, sessions] = await Promise.all([ | ||
| const [pageviews, sessions, sessionSeries] = await Promise.all([ | ||
| getPageviewStats(websiteId, filters), | ||
| getSessionStats(websiteId, filters), | ||
| wantsSessionSeries ? getSessionStatsSeries(websiteId, filters) : Promise.resolve(null), | ||
| ]); | ||
|
Comment on lines
+38
to
42
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch, applied in |
||
|
|
||
| const bouncerate = sessionSeries | ||
| ? sessionSeries.map(({ x, visits, bounces }) => ({ | ||
| x, | ||
| y: Number(visits) > 0 ? (Number(bounces) / Number(visits)) * 100 : 0, | ||
| })) | ||
| : undefined; | ||
|
|
||
| const visitduration = sessionSeries | ||
| ? sessionSeries.map(({ x, visits, totaltime }) => ({ | ||
| x, | ||
| y: Number(visits) > 0 ? Number(totaltime) / Number(visits) : 0, | ||
| })) | ||
| : undefined; | ||
|
|
||
| if (filters.compare) { | ||
| const { startDate: compareStartDate, endDate: compareEndDate } = getCompareDate( | ||
| filters.compare, | ||
| filters.startDate, | ||
| filters.endDate, | ||
| ); | ||
|
|
||
| const [comparePageviews, compareSessions] = await Promise.all([ | ||
| const [comparePageviews, compareSessions, compareSeries] = await Promise.all([ | ||
| getPageviewStats(websiteId, { | ||
| ...filters, | ||
| startDate: compareStartDate, | ||
|
|
@@ -50,21 +73,51 @@ export async function GET( | |
| startDate: compareStartDate, | ||
| endDate: compareEndDate, | ||
| }), | ||
| wantsSessionSeries | ||
| ? getSessionStatsSeries(websiteId, { | ||
| ...filters, | ||
| startDate: compareStartDate, | ||
| endDate: compareEndDate, | ||
| }) | ||
| : Promise.resolve(null), | ||
| ]); | ||
|
|
||
| const compareBouncerate = compareSeries | ||
| ? compareSeries.map(({ x, visits, bounces }) => ({ | ||
| x, | ||
| y: Number(visits) > 0 ? (Number(bounces) / Number(visits)) * 100 : 0, | ||
| })) | ||
| : undefined; | ||
|
|
||
| const compareVisitduration = compareSeries | ||
| ? compareSeries.map(({ x, visits, totaltime }) => ({ | ||
| x, | ||
| y: Number(visits) > 0 ? Number(totaltime) / Number(visits) : 0, | ||
| })) | ||
| : undefined; | ||
|
|
||
| return json({ | ||
| pageviews, | ||
| sessions, | ||
| ...(bouncerate && { bouncerate }), | ||
| ...(visitduration && { visitduration }), | ||
| startDate: filters.startDate, | ||
| endDate: filters.endDate, | ||
| compare: { | ||
| pageviews: comparePageviews, | ||
| sessions: compareSessions, | ||
| ...(compareBouncerate && { bouncerate: compareBouncerate }), | ||
| ...(compareVisitduration && { visitduration: compareVisitduration }), | ||
| startDate: compareStartDate, | ||
| endDate: compareEndDate, | ||
| }, | ||
| }); | ||
| } | ||
|
|
||
| return json({ pageviews, sessions }); | ||
| return json({ | ||
| pageviews, | ||
| sessions, | ||
| ...(bouncerate && { bouncerate }), | ||
| ...(visitduration && { visitduration }), | ||
| }); | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.