@@ -21,7 +21,6 @@ import {
2121 MetricsRecentUserSchema ,
2222} from "@stackframe/stack-shared/dist/interface/admin-metrics" ;
2323import { captureError , StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors" ;
24- import { stringCompare } from "@stackframe/stack-shared/dist/utils/strings" ;
2524import { adaptSchema , adminAuthTypeSchema , yupArray , yupNumber , yupObject , yupRecord , yupString } from "@stackframe/stack-shared/dist/schema-fields" ;
2625import { userFullInclude , userPrismaToCrud , usersCrudHandlers } from "../../users/crud" ;
2726
@@ -123,10 +122,10 @@ async function loadUsersByCountry(tenancy: Tenancy, includeAnonymous: boolean =
123122 ) ;
124123}
125124
126- // ClickHouse sample size per country. Small enough to keep the event-table
127- // scan cheap, large enough for the dashboard globe to pick ~1-5 distinct
128- // avatars per country based on the country's visible area .
129- const ACTIVE_USERS_BY_COUNTRY_SAMPLE = 8 ;
125+ // Max live users returned per country. Small enough to keep the Postgres join
126+ // cheap, large enough for the dashboard globe and satellite bubbles to pick a
127+ // few distinct avatars per country.
128+ const ACTIVE_USERS_BY_COUNTRY_LIMIT = 8 ;
130129// "Live" window used to classify users as currently active for the globe
131130// ping layer. Token-refresh fires every few minutes for each open session,
132131// so a 2-minute window gives a genuine "who's online right now" read while
@@ -145,27 +144,45 @@ async function loadActiveUsersByCountry(
145144 query : `
146145 SELECT
147146 country_code,
148- groupArraySample({sample:UInt32}) (user_id) AS user_ids
147+ groupArray (user_id) AS user_ids
149148 FROM (
150149 SELECT
151- user_id ,
152- argMax(cc, event_at) AS country_code
150+ country_code ,
151+ user_id
153152 FROM (
154153 SELECT
154+ country_code,
155155 user_id,
156- event_at,
157- CAST(data.ip_info.country_code, 'Nullable(String)') AS cc,
158- coalesce(CAST(data.is_anonymous, 'Nullable(UInt8)'), 0) AS is_anonymous
159- FROM analytics_internal.events
160- WHERE event_type = '$token-refresh'
161- AND project_id = {projectId:String}
162- AND branch_id = {branchId:String}
163- AND user_id IS NOT NULL
164- AND event_at >= {since:DateTime}
156+ row_number() OVER (
157+ PARTITION BY country_code
158+ ORDER BY last_event_at DESC, user_id ASC
159+ ) AS country_rank
160+ FROM (
161+ SELECT
162+ user_id,
163+ argMax(cc, event_at) AS country_code,
164+ max(event_at) AS last_event_at
165+ FROM (
166+ SELECT
167+ user_id,
168+ event_at,
169+ CAST(data.ip_info.country_code, 'Nullable(String)') AS cc,
170+ coalesce(CAST(data.is_anonymous, 'Nullable(UInt8)'), 0) AS is_anonymous
171+ FROM analytics_internal.events
172+ WHERE event_type = '$token-refresh'
173+ AND project_id = {projectId:String}
174+ AND branch_id = {branchId:String}
175+ AND user_id IS NOT NULL
176+ AND event_at >= {since:DateTime}
177+ )
178+ WHERE cc IS NOT NULL
179+ AND ({includeAnonymous:UInt8} = 1 OR is_anonymous = 0)
180+ GROUP BY user_id
181+ )
182+ WHERE country_code IS NOT NULL
165183 )
166- WHERE cc IS NOT NULL
167- AND ({includeAnonymous:UInt8} = 1 OR is_anonymous = 0)
168- GROUP BY user_id
184+ WHERE country_rank <= {limit:UInt32}
185+ ORDER BY country_code ASC, country_rank ASC
169186 )
170187 WHERE country_code IS NOT NULL
171188 GROUP BY country_code
@@ -175,13 +192,13 @@ async function loadActiveUsersByCountry(
175192 branchId : tenancy . branchId ,
176193 includeAnonymous : includeAnonymous ? 1 : 0 ,
177194 since : formatClickhouseDateTimeParam ( since ) ,
178- sample : ACTIVE_USERS_BY_COUNTRY_SAMPLE ,
195+ limit : ACTIVE_USERS_BY_COUNTRY_LIMIT ,
179196 } ,
180197 format : "JSONEachRow" ,
181198 } ) ;
182199 const rows : { country_code : string , user_ids : string [ ] } [ ] = await res . json ( ) ;
183200
184- // Collect every sampled UUID once so we only hit Postgres with a single
201+ // Collect every selected UUID once so we only hit Postgres with a single
185202 // `IN (...)` lookup, then re-attach them to their country buckets.
186203 const allIds = new Set < string > ( ) ;
187204 const countryToIds = new Map < string , string [ ] > ( ) ;
@@ -232,15 +249,6 @@ async function loadActiveUsersByCountry(
232249 if ( user != null ) users . push ( user ) ;
233250 }
234251 if ( users . length > 0 ) {
235- // Sort so the response is stable — `groupArraySample()` returns users
236- // in random order, which is fine for the globe UI but flakes snapshot
237- // tests. Primary key is `primary_email` (stable across test runs);
238- // `id` is a tiebreaker for anonymous users where email is null. The
239- // globe doesn't rely on any particular order.
240- users . sort ( ( a , b ) => {
241- const emailCmp = stringCompare ( a . primary_email ?? "" , b . primary_email ?? "" ) ;
242- return emailCmp !== 0 ? emailCmp : stringCompare ( a . id , b . id ) ;
243- } ) ;
244252 result [ country ] = users ;
245253 }
246254 }
0 commit comments