@@ -10,6 +10,46 @@ import { StackAssertionError, captureError } from "@stackframe/stack-shared/dist
1010const WINDOW_DAYS = 7 ;
1111const ONE_DAY_MS = 24 * 60 * 60 * 1000 ;
1212
13+ type ProjectWeeklyUsers = {
14+ weekly_users : number ,
15+ daily_users : { date : string , activity : number } [ ] ,
16+ } ;
17+
18+ export function applyProjectWeeklyUsersRows (
19+ byProject : Map < string , ProjectWeeklyUsers > ,
20+ rows : { projectId : string , day : string , users : number } [ ] ,
21+ ) {
22+ // GROUPING SETS emits one rollup row per project with day defaulted to the
23+ // ClickHouse Date epoch ("1970-01-01"); those rows hold the weekly total.
24+ const dailyIndex = new Map < string , Map < string , number > > ( ) ;
25+ for ( const row of rows ) {
26+ const project = byProject . get ( row . projectId ) ;
27+ if ( project == null ) {
28+ continue ;
29+ }
30+ const dayKey = row . day . split ( "T" ) [ 0 ] ;
31+ if ( dayKey === "1970-01-01" ) {
32+ project . weekly_users = Number ( row . users ) ;
33+ continue ;
34+ }
35+ let m = dailyIndex . get ( row . projectId ) ;
36+ if ( ! m ) {
37+ m = new Map ( ) ;
38+ dailyIndex . set ( row . projectId , m ) ;
39+ }
40+ m . set ( dayKey , Number ( row . users ) ) ;
41+ }
42+
43+ for ( const [ id , project ] of byProject ) {
44+ const m = dailyIndex . get ( id ) ;
45+ if ( ! m ) continue ;
46+ project . daily_users = project . daily_users . map ( ( point ) => ( {
47+ date : point . date ,
48+ activity : m . get ( point . date ) ?? 0 ,
49+ } ) ) ;
50+ }
51+ }
52+
1353export const GET = createSmartRouteHandler ( {
1454 metadata : { hidden : true } ,
1555 request : yupObject ( {
@@ -24,7 +64,10 @@ export const GET = createSmartRouteHandler({
2464 statusCode : yupNumber ( ) . oneOf ( [ 200 ] ) . defined ( ) ,
2565 bodyType : yupString ( ) . oneOf ( [ "json" ] ) . defined ( ) ,
2666 body : yupObject ( {
27- projects : yupRecord ( yupString ( ) . defined ( ) , MetricsDataPointsSchema ) . defined ( ) ,
67+ projects : yupRecord ( yupString ( ) . defined ( ) , yupObject ( {
68+ weekly_users : yupNumber ( ) . integer ( ) . defined ( ) ,
69+ daily_users : MetricsDataPointsSchema ,
70+ } ) . defined ( ) ) . defined ( ) ,
2871 } ) . defined ( ) ,
2972 } ) ,
3073 handler : async ( req ) => {
@@ -52,28 +95,39 @@ export const GET = createSmartRouteHandler({
5295 return out ;
5396 } ;
5497
55- const byProject : Record < string , { date : string , activity : number } [ ] > = { } ;
98+ const byProject = new Map < string , ProjectWeeklyUsers > ( ) ;
5699 for ( const id of projectIds ) {
57- byProject [ id ] = emptySeries ( ) ;
100+ byProject . set ( id , {
101+ weekly_users : 0 ,
102+ daily_users : emptySeries ( ) ,
103+ } ) ;
58104 }
105+ const projectsResponse = ( ) => Object . fromEntries ( byProject ) ;
59106
60107 if ( projectIds . length === 0 ) {
61108 return {
62109 statusCode : 200 ,
63110 bodyType : "json" ,
64- body : { projects : byProject } ,
111+ body : { projects : projectsResponse ( ) } ,
65112 } ;
66113 }
67114
68- let rows : { projectId : string , day : string , dau : number } [ ] = [ ] ;
115+ const clickhouseClient = getClickhouseAdminClient ( ) ;
116+ const queryParams = {
117+ projectIds,
118+ branchId : DEFAULT_BRANCH_ID ,
119+ since : since . toISOString ( ) . slice ( 0 , 19 ) ,
120+ untilExclusive : untilExclusive . toISOString ( ) . slice ( 0 , 19 ) ,
121+ } ;
122+
123+ let rows : { projectId : string , day : string , users : number } [ ] = [ ] ;
69124 try {
70- const clickhouseClient = getClickhouseAdminClient ( ) ;
71125 const result = await clickhouseClient . query ( {
72126 query : `
73127 SELECT
74128 project_id AS projectId,
75- toDate(event_at) AS day,
76- uniqExact(assumeNotNull(user_id)) AS dau
129+ toDate(event_at, 'UTC' ) AS day,
130+ uniqExact(assumeNotNull(user_id)) AS users
77131 FROM analytics_internal.events
78132 WHERE event_type = '$token-refresh'
79133 AND project_id IN {projectIds:Array(String)}
@@ -82,55 +136,33 @@ export const GET = createSmartRouteHandler({
82136 AND event_at >= {since:DateTime}
83137 AND event_at < {untilExclusive:DateTime}
84138 AND coalesce(CAST(data.is_anonymous, 'Nullable(UInt8)'), 0) = 0
85- GROUP BY projectId, day
139+ GROUP BY GROUPING SETS (( projectId), (projectId, day))
86140 ` ,
87- query_params : {
88- projectIds,
89- branchId : DEFAULT_BRANCH_ID ,
90- since : since . toISOString ( ) . slice ( 0 , 19 ) ,
91- untilExclusive : untilExclusive . toISOString ( ) . slice ( 0 , 19 ) ,
92- } ,
141+ query_params : queryParams ,
93142 format : "JSONEachRow" ,
94143 } ) ;
95- rows = await result . json ( ) ;
144+ rows = await result . json < { projectId : string , day : string , users : number } > ( ) ;
96145 } catch ( error ) {
97146 const captureId = error instanceof ClickHouseError
98- ? "internal-projects-dau -clickhouse-error"
99- : "internal-projects-dau -unexpected-error" ;
147+ ? "internal-projects-weekly-users -clickhouse-error"
148+ : "internal-projects-weekly-users -unexpected-error" ;
100149 captureError ( captureId , new StackAssertionError (
101- "Failed to load projects DAU ." ,
150+ "Failed to load projects weekly users ." ,
102151 { cause : error , projectCount : projectIds . length } ,
103152 ) ) ;
104153 return {
105154 statusCode : 200 ,
106155 bodyType : "json" ,
107- body : { projects : byProject } ,
156+ body : { projects : projectsResponse ( ) } ,
108157 } ;
109158 }
110- const index = new Map < string , Map < string , number > > ( ) ;
111- for ( const row of rows ) {
112- const dayKey = row . day . split ( "T" ) [ 0 ] ;
113- let m = index . get ( row . projectId ) ;
114- if ( ! m ) {
115- m = new Map ( ) ;
116- index . set ( row . projectId , m ) ;
117- }
118- m . set ( dayKey , Number ( row . dau ) ) ;
119- }
120159
121- for ( const id of projectIds ) {
122- const m = index . get ( id ) ;
123- if ( ! m ) continue ;
124- byProject [ id ] = byProject [ id ] . map ( ( point ) => ( {
125- date : point . date ,
126- activity : m . get ( point . date ) ?? 0 ,
127- } ) ) ;
128- }
160+ applyProjectWeeklyUsersRows ( byProject , rows ) ;
129161
130162 return {
131163 statusCode : 200 ,
132164 bodyType : "json" ,
133- body : { projects : byProject } ,
165+ body : { projects : projectsResponse ( ) } ,
134166 } ;
135167 } ,
136168} ) ;
0 commit comments