Skip to content

Plot bounce rate and visit duration over time on the Overview page#4251

Open
yancat160 wants to merge 4 commits into
umami-software:devfrom
yancat160:feat/plot-bounce-rate-visit-duration
Open

Plot bounce rate and visit duration over time on the Overview page#4251
yancat160 wants to merge 4 commits into
umami-software:devfrom
yancat160:feat/plot-bounce-rate-visit-duration

Conversation

@yancat160

@yancat160 yancat160 commented May 7, 2026

Copy link
Copy Markdown
Contributor

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 %, tooltip XX%)
  • Visit duration (Y axis as 0s / 40s / 1m / ..., tooltip Xm Ys)

The selection is persisted in the URL (?metric=...) using the same useNavigation pattern as UnitFilter and 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 same unit and timezone already used by pageviews and sessions.

Scope

  1. Data: new SQL query getSessionStatsSeries (Prisma + Clickhouse parity), exposed via the existing /api/websites/{id}/pageviews endpoint. Compare mode is wired through.
  2. Charting: additive renderTooltipValue prop on BarChart so the tooltip can format % or duration strings. Existing callers are unaffected.
  3. UI: new MetricSeriesChart and WebsiteChartMetricFilter, plumbed into WebsitePage.

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 build and biome lint both clean on the touched files.

    Screenshots

    Before

    Aggregate cards exist for Bounce rate and Visit duration, but the chart only plots Visitors / Views.

01-before

After, default selection (unchanged for existing users)

02-after-default-pageviews

Bounce rate selected

Y axis capped at 100, range 14% to 57% on this dataset.

03-after-bouncerate

Visit duration selected

Y axis formatted as 0s / 40s / 1m / ... / 3m.

04-after-visitduration

URL persistence

Loading ?metric=bouncerate directly puts the selector and chart in sync. Same for ?metric=visitduration.

08-url-persisted-visitduration

Notes

  • getRealtimeData.ts is untouched; the new query lives alongside getSessionStats.
  • Cohort, session-join, and excludeBounce filters are honored by the new query.

@vercel

vercel Bot commented May 7, 2026

Copy link
Copy Markdown

@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-apps

greptile-apps Bot commented May 7, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This 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 (?metric=...) using the existing useNavigation pattern.

  • Data layer: getSessionStatsSeries (new Prisma + ClickHouse query) computes per-bucket visits, bounces, and totaltime; results are mapped to bouncerate/visitduration series in the existing /api/websites/[websiteId]/pageviews route and included in both normal and compare-mode responses.
  • Charting: BarChart gains yMax, ySuggestedMax, and renderTooltipValue props (additive, no breaking changes); a new MetricSeriesChart uses these to cap the bounce-rate Y-axis at 100 and format duration labels correctly.
  • UI: WebsiteChartMetricFilter (new Select component) and MetricSeriesChart are wired into WebsiteChart and WebsitePage; the default view is unchanged for existing users.

Confidence Score: 4/5

The 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

Filename Overview
src/app/api/websites/[websiteId]/pageviews/route.ts Wires getSessionStatsSeries into the existing endpoint and derives bouncerate/visitduration series. The third DB query runs unconditionally on every request, adding unnecessary load when the pageviews metric is selected.
src/queries/sql/sessions/getSessionStatsSeries.ts New SQL query mirroring getSessionStats; Prisma and ClickHouse (standard + hourly-rollup) paths are both implemented. Logic for bounce detection and totaltime are consistent with existing patterns.
src/components/metrics/MetricSeriesChart.tsx New chart component for bounce rate and visit duration series; yMax is now correctly propagated to BarChart, tooltip formatting and Y-axis labels are properly customized per kind.
src/app/(main)/websites/[websiteId]/WebsiteChart.tsx Reads metric from URL query param without runtime validation; invalid values silently fall through to visit-duration behavior. Chart switching logic is otherwise correct.
src/app/(main)/websites/[websiteId]/WebsiteChartMetricFilter.tsx New metric selector filter component; reads/writes metric URL param following existing useNavigation patterns. Options are statically declared, so no invalid values can be written from the UI.
src/components/charts/BarChart.tsx Adds yMax, ySuggestedMax, and renderTooltipValue props; correctly wires them into chartOptions y-scale and includes renderTooltipValue in the handleTooltip dependency array. Existing callers are unaffected.
src/app/(main)/websites/[websiteId]/WebsitePage.tsx Adds WebsiteChartMetricFilter above UnitFilter in the panel header; minimal change, looks correct.
src/components/hooks/queries/useWebsitePageviewsQuery.ts Extends WebsitePageviewsData interface with bouncerate and visitduration fields; straightforward type update.
src/queries/sql/index.ts Re-exports getSessionStatsSeries from the SQL index; trivial change.

Sequence Diagram

sequenceDiagram
    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
Loading

Reviews (2): Last reviewed commit: "address review feedback on bounce rate /..." | Re-trigger Greptile

Comment thread src/components/metrics/MetricSeriesChart.tsx Outdated
Comment thread src/components/metrics/MetricSeriesChart.tsx Outdated
Comment thread src/app/(main)/websites/[websiteId]/WebsiteChart.tsx
yancat160 added 2 commits May 7, 2026 17:19
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.
@yancat160 yancat160 force-pushed the feat/plot-bounce-rate-visit-duration branch from 08564a6 to 1f6f5ea Compare May 7, 2026 21:20
@mikecao

mikecao commented May 8, 2026

Copy link
Copy Markdown
Member

@greptile re-review this with a new confidence score

Comment on lines +30 to 34
const [pageviews, sessions, sessionSeries] = await Promise.all([
getPageviewStats(websiteId, filters),
getSessionStats(websiteId, filters),
getSessionStatsSeries(websiteId, filters),
]);

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.

P1 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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.
@yancat160 yancat160 changed the title Plot bounce rate and visit duration over time on the website page Plot bounce rate and visit duration over time on the Overview page May 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants