@@ -15,6 +15,8 @@ import type {
1515type VideoRow = typeof videos . $inferSelect ;
1616type OrgId = VideoRow [ "orgId" ] ;
1717type VideoId = VideoRow [ "id" ] ;
18+ type SpaceVideoRow = typeof spaceVideos . $inferSelect ;
19+ type SpaceOrOrgId = SpaceVideoRow [ "spaceId" ] ;
1820
1921type CountSeriesRow = { bucket : string ; count : number } ;
2022type ViewSeriesRow = { bucket : string ; views : number } ;
@@ -31,15 +33,19 @@ type TinybirdAnalyticsData = {
3133 topCapsRaw : TopCapRow [ ] ;
3234} ;
3335
34- const RANGE_CONFIG : Record <
35- AnalyticsRange ,
36+ type RollingAnalyticsRange = Exclude < AnalyticsRange , "lifetime" > ;
37+
38+ const ROLLING_RANGE_CONFIG : Record <
39+ RollingAnalyticsRange ,
3640 { hours : number ; bucket : "hour" | "day" }
3741> = {
3842 "24h" : { hours : 24 , bucket : "hour" } ,
3943 "7d" : { hours : 7 * 24 , bucket : "day" } ,
4044 "30d" : { hours : 30 * 24 , bucket : "day" } ,
4145} ;
4246
47+ const LIFETIME_FALLBACK_DAYS = 30 ;
48+
4349const escapeLiteral = ( value : string ) => value . replace ( / ' / g, "''" ) ;
4450const toDateString = ( date : Date ) => date . toISOString ( ) . slice ( 0 , 10 ) ;
4551const toDateTimeString = ( date : Date ) =>
@@ -80,6 +86,47 @@ const buildBuckets = (from: Date, to: Date, bucket: "hour" | "day") => {
8086 return buckets ;
8187} ;
8288
89+ const getLifetimeRangeStart = async (
90+ orgId : OrgId ,
91+ videoIds ?: VideoId [ ] ,
92+ ) : Promise < Date | undefined > => {
93+ const whereClause = videoIds && videoIds . length > 0
94+ ? and ( eq ( videos . orgId , orgId ) , inArray ( videos . id , videoIds ) )
95+ : eq ( videos . orgId , orgId ) ;
96+
97+ const rows = await db ( )
98+ . select ( { minCreatedAt : sql < Date > `MIN(${ videos . createdAt } )` } )
99+ . from ( videos )
100+ . where ( whereClause )
101+ . limit ( 1 ) ;
102+
103+ const candidate = rows [ 0 ] ?. minCreatedAt ;
104+ if ( ! candidate ) return undefined ;
105+ return candidate instanceof Date ? candidate : new Date ( candidate ) ;
106+ } ;
107+
108+ const resolveRangeBounds = async (
109+ range : AnalyticsRange ,
110+ orgId : OrgId ,
111+ videoIds ?: VideoId [ ] ,
112+ ) : Promise < { from : Date ; to : Date ; bucket : "hour" | "day" } > => {
113+ const to = new Date ( ) ;
114+
115+ if ( range === "lifetime" ) {
116+ const lifetimeStart = await getLifetimeRangeStart ( orgId , videoIds ) ;
117+ const fallbackFrom = new Date (
118+ to . getTime ( ) - LIFETIME_FALLBACK_DAYS * 24 * 60 * 60 * 1000 ,
119+ ) ;
120+ const from =
121+ lifetimeStart && lifetimeStart < to ? lifetimeStart : fallbackFrom ;
122+ return { from, to, bucket : "day" } ;
123+ }
124+
125+ const config = ROLLING_RANGE_CONFIG [ range ] ;
126+ const from = new Date ( to . getTime ( ) - config . hours * 60 * 60 * 1000 ) ;
127+ return { from, to, bucket : config . bucket } ;
128+ } ;
129+
83130const fallbackIfEmpty = < Row > (
84131 primary : Effect . Effect < Row [ ] , never , never > ,
85132 fallback ?: Effect . Effect < Row [ ] , never , never > ,
@@ -101,27 +148,21 @@ const withTinybirdFallback = <Row>(
101148 return Effect . succeed < { data : Row [ ] } > ( { data : [ ] as Row [ ] } ) ;
102149 } ) ,
103150 Effect . map ( ( res ) => {
104- console . log ( "tinybird raw response" , JSON . stringify ( res , null , 2 ) ) ;
105- const response = res as { data : unknown [ ] } ;
151+ const response = res as { data ?: unknown [ ] } ;
106152 const data = response . data ?? [ ] ;
107- console . log ( "tinybird data array" , JSON . stringify ( data , null , 2 ) ) ;
108- const filtered = data . filter ( ( item ) : item is Row => {
109- const isObject = typeof item === "object" && item !== null ;
110- if ( ! isObject ) {
111- console . log ( "filtered out non-object item" , typeof item , item ) ;
112- }
113- return isObject ;
114- } ) as Row [ ] ;
115- console . log ( "tinybird filtered rows" , JSON . stringify ( filtered , null , 2 ) ) ;
116- return filtered ;
153+ return data . filter ( ( item ) : item is Row =>
154+ typeof item === "object" && item !== null ,
155+ ) as Row [ ] ;
117156 } ) ,
118157 ) ;
119158
120- const getSpaceVideoIds = async ( spaceId : string ) : Promise < VideoId [ ] > => {
159+ const getSpaceVideoIds = async (
160+ spaceId : SpaceOrOrgId ,
161+ ) : Promise < VideoId [ ] > => {
121162 const rows = await db ( )
122163 . select ( { videoId : spaceVideos . videoId } )
123164 . from ( spaceVideos )
124- . where ( eq ( spaceVideos . spaceId , spaceId as any ) ) ;
165+ . where ( eq ( spaceVideos . spaceId , spaceId ) ) ;
125166 return rows . map ( ( row ) => row . videoId ) ;
126167} ;
127168
@@ -131,16 +172,21 @@ export const getOrgAnalyticsData = async (
131172 spaceId ?: string ,
132173 capId ?: string ,
133174) : Promise < OrgAnalyticsResponse > => {
134- const rangeConfig = RANGE_CONFIG [ range ] ;
135- const to = new Date ( ) ;
136- const from = new Date ( to . getTime ( ) - rangeConfig . hours * 60 * 60 * 1000 ) ;
137- const buckets = buildBuckets ( from , to , rangeConfig . bucket ) ;
138175 const typedOrgId = orgId as OrgId ;
139176
140- const spaceVideoIds = spaceId ? await getSpaceVideoIds ( spaceId ) : undefined ;
177+ const spaceVideoIds = spaceId
178+ ? await getSpaceVideoIds ( spaceId as SpaceOrOrgId )
179+ : undefined ;
141180 const capVideoIds = capId ? [ capId as VideoId ] : undefined ;
142181 const videoIds = capVideoIds || spaceVideoIds ;
143182
183+ const { from, to, bucket } = await resolveRangeBounds (
184+ range ,
185+ typedOrgId ,
186+ videoIds ,
187+ ) ;
188+ const buckets = buildBuckets ( from , to , bucket ) ;
189+
144190 if (
145191 ( spaceId && spaceVideoIds && spaceVideoIds . length === 0 ) ||
146192 ( capId && ! capVideoIds )
@@ -177,48 +223,37 @@ export const getOrgAnalyticsData = async (
177223 }
178224
179225 const [ capsSeries , commentSeries , reactionSeries ] = await Promise . all ( [
180- queryVideoSeries ( typedOrgId , from , to , rangeConfig . bucket , videoIds ) ,
226+ queryVideoSeries ( typedOrgId , from , to , bucket , videoIds ) ,
181227 queryCommentsSeries (
182228 typedOrgId ,
183229 from ,
184230 to ,
185231 "text" ,
186- rangeConfig . bucket ,
232+ bucket ,
187233 videoIds ,
188234 ) ,
189235 queryCommentsSeries (
190236 typedOrgId ,
191237 from ,
192238 to ,
193239 "emoji" ,
194- rangeConfig . bucket ,
240+ bucket ,
195241 videoIds ,
196242 ) ,
197243 ] ) ;
198244
199- const tinybirdData = await runPromise (
200- Effect . gen ( function * ( ) {
201- const tinybird = yield * Tinybird ;
202- console . log ( "getOrgAnalyticsData - orgId:" , orgId , "range:" , range ) ;
203- console . log (
204- "getOrgAnalyticsData - from:" ,
205- from . toISOString ( ) ,
206- "to:" ,
207- to . toISOString ( ) ,
208- ) ;
245+ const tinybirdData = await runPromise (
246+ Effect . gen ( function * ( ) {
247+ const tinybird = yield * Tinybird ;
209248
210249 const viewSeries = yield * queryViewSeries (
211250 tinybird ,
212251 typedOrgId ,
213252 from ,
214253 to ,
215- rangeConfig . bucket ,
254+ bucket ,
216255 videoIds ,
217256 ) ;
218- console . log (
219- "getOrgAnalyticsData - viewSeries:" ,
220- JSON . stringify ( viewSeries , null , 2 ) ,
221- ) ;
222257
223258 const countries = yield * queryCountries (
224259 tinybird ,
@@ -227,10 +262,6 @@ export const getOrgAnalyticsData = async (
227262 to ,
228263 videoIds ,
229264 ) ;
230- console . log (
231- "getOrgAnalyticsData - countries:" ,
232- JSON . stringify ( countries , null , 2 ) ,
233- ) ;
234265
235266 const cities = yield * queryCities (
236267 tinybird ,
@@ -239,10 +270,6 @@ export const getOrgAnalyticsData = async (
239270 to ,
240271 videoIds ,
241272 ) ;
242- console . log (
243- "getOrgAnalyticsData - cities:" ,
244- JSON . stringify ( cities , null , 2 ) ,
245- ) ;
246273
247274 const browsers = yield * queryBrowsers (
248275 tinybird ,
@@ -251,10 +278,6 @@ export const getOrgAnalyticsData = async (
251278 to ,
252279 videoIds ,
253280 ) ;
254- console . log (
255- "getOrgAnalyticsData - browsers:" ,
256- JSON . stringify ( browsers , null , 2 ) ,
257- ) ;
258281
259282 const devices = yield * queryDevices (
260283 tinybird ,
@@ -263,10 +286,6 @@ export const getOrgAnalyticsData = async (
263286 to ,
264287 videoIds ,
265288 ) ;
266- console . log (
267- "getOrgAnalyticsData - devices:" ,
268- JSON . stringify ( devices , null , 2 ) ,
269- ) ;
270289
271290 const operatingSystems = yield * queryOperatingSystems (
272291 tinybird ,
@@ -275,18 +294,10 @@ export const getOrgAnalyticsData = async (
275294 to ,
276295 videoIds ,
277296 ) ;
278- console . log (
279- "getOrgAnalyticsData - operatingSystems:" ,
280- JSON . stringify ( operatingSystems , null , 2 ) ,
281- ) ;
282297
283298 const topCapsRaw = capId
284299 ? [ ]
285300 : yield * queryTopCaps ( tinybird , typedOrgId , from , to , videoIds ) ;
286- console . log (
287- "getOrgAnalyticsData - topCapsRaw:" ,
288- JSON . stringify ( topCapsRaw , null , 2 ) ,
289- ) ;
290301
291302 return {
292303 viewSeries,
0 commit comments