@@ -10,6 +10,48 @@ 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 , weeklyUsers : number } [ ] ,
21+ dailyRows : { projectId : string , day : string , dailyUsers : number } [ ] ,
22+ ) {
23+ for ( const row of rows ) {
24+ const project = byProject . get ( row . projectId ) ;
25+ if ( project == null ) {
26+ continue ;
27+ }
28+ project . weekly_users = Number ( row . weeklyUsers ) ;
29+ }
30+
31+ const dailyIndex = new Map < string , Map < string , number > > ( ) ;
32+ for ( const row of dailyRows ) {
33+ if ( ! byProject . has ( row . projectId ) ) {
34+ continue ;
35+ }
36+ const dayKey = row . day . split ( "T" ) [ 0 ] ;
37+ let m = dailyIndex . get ( row . projectId ) ;
38+ if ( ! m ) {
39+ m = new Map ( ) ;
40+ dailyIndex . set ( row . projectId , m ) ;
41+ }
42+ m . set ( dayKey , Number ( row . dailyUsers ) ) ;
43+ }
44+
45+ for ( const [ id , project ] of byProject ) {
46+ const m = dailyIndex . get ( id ) ;
47+ if ( ! m ) continue ;
48+ project . daily_users = project . daily_users . map ( ( point ) => ( {
49+ date : point . date ,
50+ activity : m . get ( point . date ) ?? 0 ,
51+ } ) ) ;
52+ }
53+ }
54+
1355export const GET = createSmartRouteHandler ( {
1456 metadata : { hidden : true } ,
1557 request : yupObject ( {
@@ -55,76 +97,77 @@ export const GET = createSmartRouteHandler({
5597 return out ;
5698 } ;
5799
58- const byProject : Record < string , { weekly_users : number , daily_users : { date : string , activity : number } [ ] } > = { } ;
100+ const byProject = new Map < string , ProjectWeeklyUsers > ( ) ;
59101 for ( const id of projectIds ) {
60- byProject [ id ] = {
102+ byProject . set ( id , {
61103 weekly_users : 0 ,
62104 daily_users : emptySeries ( ) ,
63- } ;
105+ } ) ;
64106 }
107+ const projectsResponse = ( ) => Object . fromEntries ( byProject ) ;
65108
66109 if ( projectIds . length === 0 ) {
67110 return {
68111 statusCode : 200 ,
69112 bodyType : "json" ,
70- body : { projects : byProject } ,
113+ body : { projects : projectsResponse ( ) } ,
71114 } ;
72115 }
73116
117+ const clickhouseClient = getClickhouseAdminClient ( ) ;
118+ const queryParams = {
119+ projectIds,
120+ branchId : DEFAULT_BRANCH_ID ,
121+ since : since . toISOString ( ) . slice ( 0 , 19 ) ,
122+ untilExclusive : untilExclusive . toISOString ( ) . slice ( 0 , 19 ) ,
123+ } ;
124+
74125 let rows : { projectId : string , weeklyUsers : number } [ ] = [ ] ;
75126 let dailyRows : { projectId : string , day : string , dailyUsers : number } [ ] = [ ] ;
76127 try {
77- const clickhouseClient = getClickhouseAdminClient ( ) ;
78- const result = await clickhouseClient . query ( {
79- query : `
80- SELECT
81- project_id AS projectId,
82- uniqExact(assumeNotNull(user_id)) AS weeklyUsers
83- FROM analytics_internal.events
84- WHERE event_type = '$token-refresh'
85- AND project_id IN {projectIds:Array(String)}
86- AND branch_id = {branchId:String}
87- AND user_id IS NOT NULL
88- AND event_at >= {since:DateTime}
89- AND event_at < {untilExclusive:DateTime}
90- AND coalesce(CAST(data.is_anonymous, 'Nullable(UInt8)'), 0) = 0
91- GROUP BY projectId
92- ` ,
93- query_params : {
94- projectIds,
95- branchId : DEFAULT_BRANCH_ID ,
96- since : since . toISOString ( ) . slice ( 0 , 19 ) ,
97- untilExclusive : untilExclusive . toISOString ( ) . slice ( 0 , 19 ) ,
98- } ,
99- format : "JSONEachRow" ,
100- } ) ;
101- rows = await result . json ( ) ;
102-
103- const dailyResult = await clickhouseClient . query ( {
104- query : `
105- SELECT
106- project_id AS projectId,
107- toDate(event_at) AS day,
108- uniqExact(assumeNotNull(user_id)) AS dailyUsers
109- FROM analytics_internal.events
110- WHERE event_type = '$token-refresh'
111- AND project_id IN {projectIds:Array(String)}
112- AND branch_id = {branchId:String}
113- AND user_id IS NOT NULL
114- AND event_at >= {since:DateTime}
115- AND event_at < {untilExclusive:DateTime}
116- AND coalesce(CAST(data.is_anonymous, 'Nullable(UInt8)'), 0) = 0
117- GROUP BY projectId, day
118- ` ,
119- query_params : {
120- projectIds,
121- branchId : DEFAULT_BRANCH_ID ,
122- since : since . toISOString ( ) . slice ( 0 , 19 ) ,
123- untilExclusive : untilExclusive . toISOString ( ) . slice ( 0 , 19 ) ,
124- } ,
125- format : "JSONEachRow" ,
126- } ) ;
127- dailyRows = await dailyResult . json ( ) ;
128+ const [ weeklyResult , dailyResult ] = await Promise . all ( [
129+ clickhouseClient . query ( {
130+ query : `
131+ SELECT
132+ project_id AS projectId,
133+ uniqExact(assumeNotNull(user_id)) AS weeklyUsers
134+ FROM analytics_internal.events
135+ WHERE event_type = '$token-refresh'
136+ AND project_id IN {projectIds:Array(String)}
137+ AND branch_id = {branchId:String}
138+ AND user_id IS NOT NULL
139+ AND event_at >= {since:DateTime}
140+ AND event_at < {untilExclusive:DateTime}
141+ AND coalesce(CAST(data.is_anonymous, 'Nullable(UInt8)'), 0) = 0
142+ GROUP BY projectId
143+ ` ,
144+ query_params : queryParams ,
145+ format : "JSONEachRow" ,
146+ } ) ,
147+ clickhouseClient . query ( {
148+ query : `
149+ SELECT
150+ project_id AS projectId,
151+ toDate(event_at) AS day,
152+ uniqExact(assumeNotNull(user_id)) AS dailyUsers
153+ FROM analytics_internal.events
154+ WHERE event_type = '$token-refresh'
155+ AND project_id IN {projectIds:Array(String)}
156+ AND branch_id = {branchId:String}
157+ AND user_id IS NOT NULL
158+ AND event_at >= {since:DateTime}
159+ AND event_at < {untilExclusive:DateTime}
160+ AND coalesce(CAST(data.is_anonymous, 'Nullable(UInt8)'), 0) = 0
161+ GROUP BY projectId, day
162+ ` ,
163+ query_params : queryParams ,
164+ format : "JSONEachRow" ,
165+ } ) ,
166+ ] ) ;
167+ [ rows , dailyRows ] = await Promise . all ( [
168+ weeklyResult . json < { projectId : string , weeklyUsers : number } > ( ) ,
169+ dailyResult . json < { projectId : string , day : string , dailyUsers : number } > ( ) ,
170+ ] ) ;
128171 } catch ( error ) {
129172 const captureId = error instanceof ClickHouseError
130173 ? "internal-projects-weekly-users-clickhouse-error"
@@ -136,37 +179,16 @@ export const GET = createSmartRouteHandler({
136179 return {
137180 statusCode : 200 ,
138181 bodyType : "json" ,
139- body : { projects : byProject } ,
182+ body : { projects : projectsResponse ( ) } ,
140183 } ;
141184 }
142- for ( const row of rows ) {
143- byProject [ row . projectId ] . weekly_users = Number ( row . weeklyUsers ) ;
144- }
145185
146- const dailyIndex = new Map < string , Map < string , number > > ( ) ;
147- for ( const row of dailyRows ) {
148- const dayKey = row . day . split ( "T" ) [ 0 ] ;
149- let m = dailyIndex . get ( row . projectId ) ;
150- if ( ! m ) {
151- m = new Map ( ) ;
152- dailyIndex . set ( row . projectId , m ) ;
153- }
154- m . set ( dayKey , Number ( row . dailyUsers ) ) ;
155- }
156-
157- for ( const id of projectIds ) {
158- const m = dailyIndex . get ( id ) ;
159- if ( ! m ) continue ;
160- byProject [ id ] . daily_users = byProject [ id ] . daily_users . map ( ( point ) => ( {
161- date : point . date ,
162- activity : m . get ( point . date ) ?? 0 ,
163- } ) ) ;
164- }
186+ applyProjectWeeklyUsersRows ( byProject , rows , dailyRows ) ;
165187
166188 return {
167189 statusCode : 200 ,
168190 bodyType : "json" ,
169- body : { projects : byProject } ,
191+ body : { projects : projectsResponse ( ) } ,
170192 } ;
171193 } ,
172194} ) ;
0 commit comments