Skip to content

Conversation

@dankotov
Copy link

Fix percentage calculation in session metrics

Problem

The Location (and other session metrics) panel calculates percentages based on the sum of displayed items rather than total visitors.

For example, with 4,712 total visitors and 888 from the US:

  • Before: 888 / 2,917 (sum of top 10) = 30%
  • After: 888 / 4,712 (total visitors) = 19%

Changes

  • Modified getSessionMetrics to return { data, total } instead of just the data array
  • Updated percentFilter to accept an optional total parameter
  • Updated MetricsTable to use the total from the API response

Note

This fix currently only applies to session metrics (Location, Environment panels due to the getSessionMetrics refactor). Other metrics (getPageviewMetrics, getEventMetrics, getChannelMetrics) still use the old calculation. Happy to implement the same fix for those if the team approves this approach.

Fixes #3912

as opposed to relative to the top-10 countries shown in the breakdown
@vercel
Copy link

vercel bot commented Jan 11, 2026

@dankotov 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
Copy link
Contributor

greptile-apps bot commented Jan 11, 2026

Greptile Overview

Greptile Summary

This PR fixes percentage calculation in session metrics (Location, Environment panels) to show percentages relative to total visitors rather than the sum of displayed items. The implementation adds a parallel SQL query to fetch total unique sessions and modifies getSessionMetrics to return { data, total } instead of just an array.

Key Changes:

  • getSessionMetrics now executes two queries: one for paginated data and one for total count
  • percentFilter accepts an optional total parameter to override the default sum-based calculation
  • Components handle both old (array) and new (object with data/total) formats for backward compatibility
  • Export route extracts .data from the new format before CSV generation

Approach:
The fix correctly calculates totals by executing a separate COUNT query without GROUP BY, LIMIT, or OFFSET clauses. Both Prisma and ClickHouse implementations properly handle the two query paths (with/without event filters). The backward-compatible approach ensures existing consumers continue to work while new consumers can use the total field.

Scope:
As noted in the PR description, this fix only applies to session metrics (browser, os, device, country, screen, language, city, region). Other metric types (pageview, event, channel) still use sum-based percentage calculation.

Confidence Score: 5/5

  • Safe to merge - implements correct percentage calculation with proper backward compatibility
  • The implementation correctly adds a separate total count query, maintains backward compatibility through format detection, and properly handles all edge cases. The SQL queries are sound (total without GROUP BY/LIMIT, data with GROUP BY/LIMIT), type definitions are updated, and all consumers are adapted. No bugs or blocking issues found.
  • No files require special attention

Important Files Changed

File Analysis

Filename Score Overview
src/queries/sql/sessions/getSessionMetrics.ts 5/5 Modified to return { data, total } instead of array; added SQL queries to calculate total unique sessions for both Prisma and ClickHouse
src/lib/filters.ts 5/5 Updated percentFilter to accept optional total parameter for calculating percentages relative to total instead of sum
src/components/metrics/MetricsTable.tsx 5/5 Added backward compatibility to handle both old array format and new { data, total } format; passes total to percentFilter
src/components/metrics/WorldMap.tsx 5/5 Updated to handle both old array format and new { data, total } format; passes total to percentFilter for correct percentage calculation
src/app/api/websites/[websiteId]/export/route.ts 5/5 Added .then(r => r.data) to extract data array from new getSessionMetrics response format for CSV export compatibility
src/components/hooks/queries/useWebsiteMetricsQuery.ts 5/5 Updated WebsiteMetricsData type to accept both old array format and new { data, total } format

Sequence Diagram

sequenceDiagram
    participant Client as MetricsTable/WorldMap
    participant Hook as useWebsiteMetricsQuery
    participant API as /api/websites/[id]/metrics
    participant Query as getSessionMetrics
    participant DB as Database (Prisma/ClickHouse)

    Client->>Hook: Request metrics (type, limit, offset)
    Hook->>API: GET /metrics?type=country&limit=10
    API->>Query: getSessionMetrics(websiteId, params, filters)
    
    par Fetch paginated data
        Query->>DB: SELECT metric, COUNT(DISTINCT session_id)<br/>GROUP BY metric<br/>ORDER BY count DESC<br/>LIMIT 10
        DB-->>Query: [{x: 'US', y: 888}, ...]
    and Fetch total sessions
        Query->>DB: SELECT COUNT(DISTINCT session_id)<br/>(no GROUP BY, no LIMIT)
        DB-->>Query: [{total: 4712}]
    end
    
    Query-->>API: {data: [...], total: 4712}
    API-->>Hook: {data: [...], total: 4712}
    Hook-->>Client: {data: [...], total: 4712}
    
    Client->>Client: percentFilter(data, total)<br/>Calculate: 888/4712 = 18.8%
    Client->>Client: Render with correct percentages
Loading

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

3 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Jan 11, 2026

Additional Comments (3)

src/app/api/websites/[websiteId]/export/route.ts
The export functionality will break for session metrics. getSessionMetrics now returns { data: [...], total: number } but Papa.unparse() on lines 52-58 expects a flat array structure.

When Papa.unparse receives { data: [...], total: 1000 }, it will not correctly convert this to CSV format. This will break CSV exports for browsers, os, devices, and countries.

Fix needed: Extract the .data property from session metrics results before passing to parse():

    getSessionMetrics(websiteId, { type: 'browser' }, filters).then(r => r.data),
    getSessionMetrics(websiteId, { type: 'os' }, filters).then(r => r.data),
    getSessionMetrics(websiteId, { type: 'device' }, filters).then(r => r.data),
    getSessionMetrics(websiteId, { type: 'country' }, filters).then(r => r.data),
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/api/websites/[websiteId]/export/route.ts
Line: 37:40

Comment:
The export functionality will break for session metrics. `getSessionMetrics` now returns `{ data: [...], total: number }` but `Papa.unparse()` on lines 52-58 expects a flat array structure.

When Papa.unparse receives `{ data: [...], total: 1000 }`, it will not correctly convert this to CSV format. This will break CSV exports for browsers, os, devices, and countries.

**Fix needed:** Extract the `.data` property from session metrics results before passing to `parse()`:

```suggestion
    getSessionMetrics(websiteId, { type: 'browser' }, filters).then(r => r.data),
    getSessionMetrics(websiteId, { type: 'os' }, filters).then(r => r.data),
    getSessionMetrics(websiteId, { type: 'device' }, filters).then(r => r.data),
    getSessionMetrics(websiteId, { type: 'country' }, filters).then(r => r.data),
```

How can I resolve this? If you propose a fix, please make it concise.

src/components/metrics/WorldMap.tsx
WorldMap is not updated to handle the new { data, total } format from getSessionMetrics. Since 'country' is in SESSION_COLUMNS, the API now returns { data: [...], total: number }, but this code:

  1. Casts the entire response as any[] which will fail
  2. Doesn't extract the total field
  3. Doesn't pass total to percentFilter

This means WorldMap will show incorrect percentages (based on sum of displayed countries rather than total visitors), defeating the purpose of this PR.

Fix needed: Handle the new format similar to how MetricsTable does it:

  const metrics = useMemo(() => {
    const source = data || mapData;
    if (!source) return [];
    
    // Handle both old format (array) and new format ({ data, total })
    const items = Array.isArray(source) ? source : source.data;
    const total = Array.isArray(source) ? undefined : source.total;
    
    return percentFilter(items, total);
  }, [data, mapData]);
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/components/metrics/WorldMap.tsx
Line: 35:38

Comment:
WorldMap is not updated to handle the new `{ data, total }` format from `getSessionMetrics`. Since 'country' is in `SESSION_COLUMNS`, the API now returns `{ data: [...], total: number }`, but this code:
1. Casts the entire response as `any[]` which will fail
2. Doesn't extract the `total` field
3. Doesn't pass `total` to `percentFilter`

This means WorldMap will show incorrect percentages (based on sum of displayed countries rather than total visitors), defeating the purpose of this PR.

**Fix needed:** Handle the new format similar to how MetricsTable does it:

```suggestion
  const metrics = useMemo(() => {
    const source = data || mapData;
    if (!source) return [];
    
    // Handle both old format (array) and new format ({ data, total })
    const items = Array.isArray(source) ? source : source.data;
    const total = Array.isArray(source) ? undefined : source.total;
    
    return percentFilter(items, total);
  }, [data, mapData]);
```

How can I resolve this? If you propose a fix, please make it concise.

src/components/hooks/queries/useWebsiteMetricsQuery.ts
Type definition is now inaccurate. The API returns different formats depending on the metric type:

  • SESSION_COLUMNS (browser, os, device, country, city, region, language): { data: { x: string; y: number }[], total: number }
  • EVENT_COLUMNS (path, entry, exit, referrer, etc.): { x: string; y: number }[]
  • channel: { x: string; y: number }[]

This creates a type safety issue where TypeScript expects an array but runtime can return an object.

Suggested fix: Update the type to handle both formats:

export type WebsiteMetricsData = 
  | { x: string; y: number }[]
  | { data: { x: string; y: number }[]; total: number };
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/components/hooks/queries/useWebsiteMetricsQuery.ts
Line: 7:10

Comment:
Type definition is now inaccurate. The API returns different formats depending on the metric type:
- **SESSION_COLUMNS** (browser, os, device, country, city, region, language): `{ data: { x: string; y: number }[], total: number }`
- **EVENT_COLUMNS** (path, entry, exit, referrer, etc.): `{ x: string; y: number }[]`
- **channel**: `{ x: string; y: number }[]`

This creates a type safety issue where TypeScript expects an array but runtime can return an object.

**Suggested fix:** Update the type to handle both formats:

```suggestion
export type WebsiteMetricsData = 
  | { x: string; y: number }[]
  | { data: { x: string; y: number }[]; total: number };
```

How can I resolve this? If you propose a fix, please make it concise.

getSessionMetrics now returns { data, total } but Papa.unparse()
expects a flat array for CSV export
WorldMap now correctly extracts data and total from the new
{ data, total } response format for accurate percentage calculation.
Type now correctly represents both array format (event/pageview metrics)
and { data, total } format (session metrics).
@dankotov
Copy link
Author

@greptileai

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

No files reviewed, no comments

Edit Code Review Agent Settings | Greptile

@mikecao mikecao changed the base branch from master to dev January 17, 2026 06:16
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.

Incorrect Location Breakdown Numbers

1 participant