Plot bounce rate and visit duration over time on the Overview page#4251
Plot bounce rate and visit duration over time on the Overview page#4251yancat160 wants to merge 4 commits into
Conversation
|
@yancat160 is attempting to deploy a commit to the Umami Software Team on Vercel. A member of the Team first needs to authorize it. |
Greptile SummaryThis PR adds bounce rate and visit duration time-series charts to the website page, complementing the existing aggregate stat cards. A new metric selector persists the chosen view in the URL (
Confidence Score: 4/5The feature is functionally correct, but every call to the pageviews endpoint now always runs a third DB query even when only the default pageviews chart is displayed. The unconditional getSessionStatsSeries call in route.ts adds a parallel DB query to 100% of website-page loads, not just those where the user has selected bounce rate or visit duration. This is a real and immediate regression for all existing users of the endpoint. src/app/api/websites/[websiteId]/pageviews/route.ts — the third DB query runs unconditionally. Important Files Changed
Sequence DiagramsequenceDiagram
participant User
participant WebsiteChartMetricFilter
participant WebsiteChart
participant API as /api/websites/[id]/pageviews
participant DB as DB (Prisma/ClickHouse)
User->>WebsiteChartMetricFilter: Select metric (pageviews / bouncerate / visitduration)
WebsiteChartMetricFilter->>WebsiteChart: "router.push(?metric=...)"
WebsiteChart->>API: GET /pageviews (always fetches all 3 series)
API->>DB: getPageviewStats()
API->>DB: getSessionStats()
API->>DB: getSessionStatsSeries() [always runs]
DB-->>API: results
API-->>WebsiteChart: "{ pageviews, sessions, bouncerate, visitduration }"
alt "metric === pageviews"
WebsiteChart->>WebsiteChart: render PageviewsChart
else "metric === bouncerate or visitduration"
WebsiteChart->>WebsiteChart: derive metricChartData
WebsiteChart->>WebsiteChart: "render MetricSeriesChart (yMax=100 for bouncerate)"
end
Reviews (2): Last reviewed commit: "address review feedback on bounce rate /..." | Re-trigger Greptile |
Adds a metric selector above the website chart to switch between Visitors / Views
(default), Bounce rate, and Visit duration. Selection is persisted in the URL via
`?metric=...` using the same useNavigation pattern as UnitFilter and the date range,
so it survives refresh and produces shareable links.
Data: new getSessionStatsSeries SQL query (Prisma + Clickhouse parity), exposed via
the existing /api/websites/{id}/pageviews endpoint with compare mode wired through.
Charting: additive renderTooltipValue prop on BarChart so the tooltip can format %
and duration strings. Existing callers are unaffected.
UI: new MetricSeriesChart and WebsiteChartMetricFilter, plumbed into WebsitePage.
- BarChart: add yMax and ySuggestedMax props that wire through to the Chart.js y scale, so consumers can actually cap or hint the axis. - MetricSeriesChart: pass yMax=100 to BarChart for the bouncerate kind so the axis is reliably bounded to 0-100 percent (previously yMax was attached to chartData and silently ignored by Chart.tsx). - MetricSeriesChart: drop locale from the chartData useMemo dependency array since only dateLocale is referenced inside the callback. - MetricSeriesChart: type the data prop as nullable to match the actual shape passed by WebsiteChart while loading.
08564a6 to
1f6f5ea
Compare
|
@greptile re-review this with a new confidence score |
| const [pageviews, sessions, sessionSeries] = await Promise.all([ | ||
| getPageviewStats(websiteId, filters), | ||
| getSessionStats(websiteId, filters), | ||
| getSessionStatsSeries(websiteId, filters), | ||
| ]); |
There was a problem hiding this comment.
Unconditional third DB query on every pageviews request
getSessionStatsSeries now runs in every call to this endpoint, even when the front-end is showing the default pageviews metric and will never read bouncerate or visitduration. The existing endpoint already performed 2 parallel queries; this PR silently adds a third for 100% of website-page loads, not just the subset of users who switch the metric selector. Under load the extra query will increase DB fan-out and latency for all visitors of all websites. The fix is to pass the chosen metric as a query parameter from the front-end and run getSessionStatsSeries only when the requested metric requires it.
There was a problem hiding this comment.
Good catch, applied in f9f0145d. The frontend now sends ?metric=bouncerate or ?metric=visitduration only when one of those metrics is selected, validated server-side via zod. getSessionStatsSeries (and the matching compare-period call) only run when the metric is set, so the default pageviews request is back to two parallel DB queries instead of three. Verified end-to-end: the response for /pageviews (no metric) now omits bouncerate and visitduration entirely, and the bouncerate / visitduration paths still render as before.
Address Greptile P1 review on PR umami-software#4251. The previous tip of this branch ran getSessionStatsSeries unconditionally on every call to the pageviews endpoint, even when the front end was showing the default pageviews metric and would never read bouncerate or visitduration. That added a third parallel DB query to 100% of website-page loads, increasing fan-out and latency for users who never touch the metric selector. Pass the chosen metric as a query parameter from the front end and only run getSessionStatsSeries when the metric is bouncerate or visitduration. Three pieces: - route.ts: accept a metric query param via zod, gate the third query (and the corresponding compare-period query) behind it, and drop bouncerate / visitduration from the response shape when not computed. - useWebsitePageviewsQuery.ts: accept an optional metric arg, mark bouncerate / visitduration as optional in the response type, and pass the param through to the API call when set. Include metric in the React Query cache key so switching metrics refetches. - WebsiteChart.tsx: pass the current metric to the hook, mapping pageviews to undefined so the default request stays parameter-free and the server can short-circuit cleanly. Verified in playwright on the seeded Demo SaaS data: - /websites/<id> with no metric: query string omits metric, response contains only pageviews and sessions. - /websites/<id>?metric=bouncerate: query string includes metric=bouncerate, response includes the bouncerate series, Y axis caps at 100%. - /websites/<id>?metric=visitduration: query string includes metric=visitduration, response includes the visitduration series, Y axis formats as durations.
Why
The website page already shows aggregate Bounce rate (e.g. 35%) and Visit duration (e.g. 1m 42s) cards, but the chart only plots Visitors and Views. There is no built-in way to see when bounce rate spikes or how visit duration evolves over time without leaving the page.
What changes
A small selector above the chart switches between three modes:
Visitors / Views(default, unchanged)Bounce rate(Y axis in%, tooltipXX%)Visit duration(Y axis as0s / 40s / 1m / ..., tooltipXm Ys)The selection is persisted in the URL (
?metric=...) using the sameuseNavigationpattern asUnitFilterand the date range selector, so it survives refresh and produces shareable links.The two new metrics are computed from existing schema fields (
bounces,totaltime,visits) bucketed by the sameunitandtimezonealready used bypageviewsandsessions.Scope
getSessionStatsSeries(Prisma + Clickhouse parity), exposed via the existing/api/websites/{id}/pageviewsendpoint. Compare mode is wired through.renderTooltipValueprop onBarChartso the tooltip can format%or duration strings. Existing callers are unaffected.MetricSeriesChartandWebsiteChartMetricFilter, plumbed intoWebsitePage.Validation
API returns the same number of buckets as
pageviews/sessions, and series mean matches the aggregate stats card to within rounding (35% / 1m 42s on the demo SaaS dataset).Verified visually with the Demo SaaS data (screenshots will be added in a follow-up comment).
pnpm buildandbiome lintboth clean on the touched files.Screenshots
Before
Aggregate cards exist for Bounce rate and Visit duration, but the chart only plots Visitors / Views.
After, default selection (unchanged for existing users)
Bounce rate selected
Y axis capped at 100, range 14% to 57% on this dataset.
Visit duration selected
Y axis formatted as
0s / 40s / 1m / ... / 3m.URL persistence
Loading
?metric=bounceratedirectly puts the selector and chart in sync. Same for?metric=visitduration.Notes
getRealtimeData.tsis untouched; the new query lives alongsidegetSessionStats.excludeBouncefilters are honored by the new query.