@@ -5,7 +5,7 @@ import { processResults } from "../api/analytics/utils.js";
55import { clickhouse } from "../db/clickhouse/clickhouse.js" ;
66import { db } from "../db/postgres/postgres.js" ;
77import { member , organization , sites , user } from "../db/postgres/schema.js" ;
8- import { IS_CLOUD } from "../lib/const.js" ;
8+ import { DEFAULT_EVENT_LIMIT , IS_CLOUD } from "../lib/const.js" ;
99import { sendLimitExceededEmail } from "../lib/email/email.js" ;
1010import { createServiceLogger } from "../lib/logger/logger.js" ;
1111import { getBestSubscription } from "../lib/subscriptionUtils.js" ;
@@ -86,18 +86,21 @@ class UsageService {
8686 }
8787
8888 /**
89- * Gets all site IDs for an organization
89+ * Gets all sites with their organization IDs (excludes sites without an organization)
9090 */
91- private async getSiteIdsForOrganization ( organizationId : string ) : Promise < number [ ] > {
91+ private async getAllSites ( ) : Promise < Array < { siteId : number ; organizationId : string } > > {
9292 try {
93- const siteRecords = await db
94- . select ( { siteId : sites . siteId } )
95- . from ( sites )
96- . where ( eq ( sites . organizationId , organizationId ) ) ;
93+ const allSites = await db
94+ . select ( {
95+ siteId : sites . siteId ,
96+ organizationId : sites . organizationId ,
97+ } )
98+ . from ( sites ) ;
9799
98- return siteRecords . map ( record => record . siteId ) ;
100+ // Filter out sites without an organization ID
101+ return allSites . filter ( site => site . organizationId !== null ) as Array < { siteId : number ; organizationId : string } > ;
99102 } catch ( error ) {
100- this . logger . error ( error as Error , `Error getting sites for organization ${ organizationId } ` ) ;
103+ this . logger . error ( error as Error , `Error getting all sites ` ) ;
101104 return [ ] ;
102105 }
103106 }
@@ -114,7 +117,7 @@ class UsageService {
114117 name : string ;
115118 } ) : Promise < [ number , string | null ] > {
116119 // Special case for specific organizations
117- if ( orgData . name === "tomato 2 " || orgData . name === "Zam" ) {
120+ if ( orgData . name === "rybbit " || orgData . name === "Zam" ) {
118121 return [ Infinity , this . getStartOfMonth ( ) ] ;
119122 }
120123
@@ -138,68 +141,40 @@ class UsageService {
138141 }
139142
140143 /**
141- * Gets monthly event count from ClickHouse for the given site IDs
142- * Sites with ID < 2000 are grandfathered in and only count pageviews
143- * Sites with ID >= 2000 count all event types (pageview, custom_event, performance)
144+ * Gets monthly event counts for all sites in a single query (for current month)
145+ * Returns a map of site_id -> event count
144146 */
145- private async getMonthlyEventCount ( siteIds : number [ ] , startDate : string | null ) : Promise < number > {
146- if ( ! siteIds . length ) {
147- return 0 ;
148- }
149-
150- // If no startDate is provided (e.g., no subscription), default to start of month
151- const periodStart = startDate || this . getStartOfMonth ( ) ;
152-
153- // Split sites into grandfathered (< 2000) and new (>= 2000)
154- const grandfatheredSites = siteIds . filter ( id => id < 2000 ) ;
155- const newSites = siteIds . filter ( id => id >= 2000 ) ;
156-
147+ private async getAllSiteEventCounts ( ) : Promise < Map < number , number > > {
157148 try {
158- let totalCount = 0 ;
159-
160- // Count pageviews only for grandfathered sites
161- if ( grandfatheredSites . length > 0 ) {
162- const grandfatheredResult = await clickhouse . query ( {
163- query : `
164- SELECT COUNT(*) as count
165- FROM events
166- WHERE site_id IN {siteIds:Array(Int32)} AND type = 'pageview'
149+ const periodStart = this . getStartOfMonth ( ) ;
150+
151+ const result = await clickhouse . query ( {
152+ query : `
153+ SELECT
154+ site_id,
155+ COUNT(*) as count
156+ FROM events
157+ WHERE type IN ('pageview', 'custom_event', 'performance')
167158 AND timestamp >= toDate({periodStart:String})
168- ` ,
169- format : "JSONEachRow" ,
170- query_params : {
171- siteIds : grandfatheredSites ,
172- periodStart : periodStart ,
173- } ,
174- } ) ;
175- const grandfatheredRows = await processResults < { count : string } > ( grandfatheredResult ) ;
176- totalCount += parseInt ( grandfatheredRows [ 0 ] . count , 10 ) ;
177- }
159+ GROUP BY site_id
160+ ` ,
161+ format : "JSONEachRow" ,
162+ query_params : {
163+ periodStart : periodStart ,
164+ } ,
165+ } ) ;
178166
179- // Count all events (pageview, custom_event, performance) for new sites
180- if ( newSites . length > 0 ) {
181- const newSitesResult = await clickhouse . query ( {
182- query : `
183- SELECT COUNT(*) as count
184- FROM events
185- WHERE site_id IN {siteIds:Array(Int32)}
186- AND type IN ('pageview', 'custom_event', 'performance')
187- AND timestamp >= toDate({periodStart:String})
188- ` ,
189- format : "JSONEachRow" ,
190- query_params : {
191- siteIds : newSites ,
192- periodStart : periodStart ,
193- } ,
194- } ) ;
195- const newSitesRows = await processResults < { count : string } > ( newSitesResult ) ;
196- totalCount += parseInt ( newSitesRows [ 0 ] . count , 10 ) ;
167+ const rows = await processResults < { site_id : number ; count : string } > ( result ) ;
168+
169+ const eventCountMap = new Map < number , number > ( ) ;
170+ for ( const row of rows ) {
171+ eventCountMap . set ( row . site_id , parseInt ( row . count , 10 ) ) ;
197172 }
198173
199- return totalCount ;
174+ return eventCountMap ;
200175 } catch ( error ) {
201- this . logger . error ( error as Error , ` Error querying ClickHouse for events for sites ${ siteIds } ` ) ;
202- return 0 ;
176+ this . logger . error ( error as Error , " Error querying ClickHouse for event counts" ) ;
177+ return new Map ( ) ;
203178 }
204179 }
205180
@@ -210,7 +185,22 @@ class UsageService {
210185 this . logger . info ( "Starting check of monthly event usage for organizations..." ) ;
211186
212187 try {
213- // Get all organizations (both with and without Stripe customer IDs)
188+ // Step 1: Get all sites with their organization IDs
189+ const allSites = await this . getAllSites ( ) ;
190+
191+ // Step 2: Get event counts for all sites in a single query (current month)
192+ const eventCountMap = await this . getAllSiteEventCounts ( ) ;
193+
194+ // Step 3: Build a map of organizationId -> { siteIds, eventCount }
195+ const orgDataMap = new Map < string , { siteIds : number [ ] ; eventCount : number } > ( ) ;
196+ for ( const site of allSites ) {
197+ const orgData = orgDataMap . get ( site . organizationId ) || { siteIds : [ ] , eventCount : 0 } ;
198+ orgData . siteIds . push ( site . siteId ) ;
199+ orgData . eventCount += eventCountMap . get ( site . siteId ) || 0 ;
200+ orgDataMap . set ( site . organizationId , orgData ) ;
201+ }
202+
203+ // Step 4: Get all organizations
214204 const organizations = await db
215205 . select ( {
216206 id : organization . id ,
@@ -221,24 +211,30 @@ class UsageService {
221211 } )
222212 . from ( organization ) ;
223213
214+ // Step 5: Process each organization
224215 for ( const orgData of organizations ) {
225216 try {
226- // Get site IDs for this organization
227- const siteIds = await this . getSiteIdsForOrganization ( orgData . id ) ;
228-
229- // If organization has no sites, continue to next organization
230- if ( ! siteIds . length ) {
231- continue ;
217+ const orgStats = orgDataMap . get ( orgData . id ) ;
218+ const eventCount = orgStats ?. eventCount || 0 ;
219+ const siteIds = orgStats ?. siteIds || [ ] ;
220+
221+ // Only fetch subscription info for organizations with > 3000 events
222+ // This avoids slow Stripe API calls for low-usage orgs
223+ let eventLimit : number ;
224+ let isOverLimit : boolean ;
225+
226+ if ( eventCount <= DEFAULT_EVENT_LIMIT ) {
227+ // Free tier limit is 3000, so they're definitely not over limit
228+ eventLimit = DEFAULT_EVENT_LIMIT ;
229+ isOverLimit = false ;
230+ this . logger . debug ( `Organization ${ orgData . name } has ${ eventCount } events, skipping subscription check` ) ;
231+ } else {
232+ // High usage - need to check their actual subscription
233+ const [ fetchedLimit , periodStart ] = await this . getOrganizationSubscriptionInfo ( orgData ) ;
234+ eventLimit = fetchedLimit ;
235+ isOverLimit = eventCount > eventLimit ;
232236 }
233237
234- // Get organization's subscription information (limit and period start)
235- const [ eventLimit , periodStart ] = await this . getOrganizationSubscriptionInfo ( orgData ) ;
236-
237- // Get monthly event count from ClickHouse using the billing period start date
238- const eventCount = await this . getMonthlyEventCount ( siteIds , periodStart ) ;
239-
240- // Check if over limit and update global set
241- const isOverLimit = eventCount > eventLimit ;
242238 const wasOverLimit = orgData . overMonthlyLimit ?? false ;
243239
244240 // Update organization's monthlyEventCount and overMonthlyLimit fields
@@ -286,13 +282,8 @@ class UsageService {
286282 }
287283 }
288284
289- // Format additional date info for logging if available
290- const periodInfo = periodStart ? `period started ${ periodStart } ` : "this month" ;
291-
292285 this . logger . info (
293- `Updated organization ${
294- orgData . name
295- } : ${ eventCount . toLocaleString ( ) } events, limit ${ eventLimit . toLocaleString ( ) } , ${ periodInfo } `
286+ `Updated organization ${ orgData . name } : ${ eventCount . toLocaleString ( ) } events, limit ${ eventLimit . toLocaleString ( ) } `
296287 ) ;
297288 } catch ( error ) {
298289 this . logger . error ( error as Error , `Error processing organization ${ orgData . id } ` ) ;
0 commit comments