@@ -204,4 +204,125 @@ export function registerAnalyticsCommands(app: CLI): void {
204204 process . exit ( 1 )
205205 }
206206 } )
207+
208+ app
209+ . command ( 'analytics:query <siteId>' , 'Query analytics data for a site' )
210+ . option ( '--table <name>' , 'DynamoDB table name' , { default : 'ts-analytics' } )
211+ . option ( '--region <region>' , 'AWS region' , { default : 'us-east-1' } )
212+ . option ( '--limit <n>' , 'Max items to return' , { default : '10' } )
213+ . option ( '--type <type>' , 'Filter by type (pageview, event, etc.)' )
214+ . action ( async ( siteId : string , options : { table : string ; region : string ; limit : string ; type ?: string } ) => {
215+ cli . header ( `Analytics Data: ${ siteId } ` )
216+
217+ try {
218+ const dynamodb = new DynamoDBClient ( options . region )
219+
220+ const spinner = new cli . Spinner ( 'Querying data...' )
221+ spinner . start ( )
222+
223+ const result = await dynamodb . query ( {
224+ TableName : options . table ,
225+ KeyConditionExpression : 'pk = :pk' ,
226+ ExpressionAttributeValues : {
227+ ':pk' : { S : `SITE#${ siteId } ` } ,
228+ } ,
229+ ScanIndexForward : false ,
230+ Limit : parseInt ( options . limit , 10 ) ,
231+ } )
232+
233+ spinner . succeed ( `Found ${ result . Items ?. length || 0 } items` )
234+
235+ if ( ! result . Items || result . Items . length === 0 ) {
236+ cli . info ( 'No data found for this site' )
237+ return
238+ }
239+
240+ const items = result . Items . map ( item => DynamoDBClient . unmarshal ( item ) )
241+
242+ for ( const item of items ) {
243+ cli . info ( '' )
244+ cli . info ( `SK: ${ item . sk } ` )
245+ if ( item . path ) cli . info ( ` Path: ${ item . path } ` )
246+ if ( item . timestamp ) cli . info ( ` Time: ${ item . timestamp } ` )
247+ if ( item . visitorId ) cli . info ( ` Visitor: ${ item . visitorId } ` )
248+ if ( item . browser ) cli . info ( ` Browser: ${ item . browser } ` )
249+ if ( item . country ) cli . info ( ` Country: ${ item . country } ` )
250+ }
251+ }
252+ catch ( error : any ) {
253+ cli . error ( `Failed to query data: ${ error . message } ` )
254+ process . exit ( 1 )
255+ }
256+ } )
257+
258+ app
259+ . command ( 'analytics:realtime <siteId>' , 'Check realtime visitors for a site' )
260+ . option ( '--table <name>' , 'DynamoDB table name' , { default : 'ts-analytics' } )
261+ . option ( '--region <region>' , 'AWS region' , { default : 'us-east-1' } )
262+ . option ( '--minutes <n>' , 'Minutes to look back' , { default : '5' } )
263+ . action ( async ( siteId : string , options : { table : string ; region : string ; minutes : string } ) => {
264+ cli . header ( `Realtime: ${ siteId } ` )
265+
266+ try {
267+ const dynamodb = new DynamoDBClient ( options . region )
268+
269+ const spinner = new cli . Spinner ( 'Checking realtime...' )
270+ spinner . start ( )
271+
272+ const minutesAgo = parseInt ( options . minutes , 10 )
273+ const cutoff = new Date ( Date . now ( ) - minutesAgo * 60 * 1000 )
274+
275+ const result = await dynamodb . query ( {
276+ TableName : options . table ,
277+ KeyConditionExpression : 'pk = :pk AND sk BETWEEN :start AND :end' ,
278+ ExpressionAttributeValues : {
279+ ':pk' : { S : `SITE#${ siteId } ` } ,
280+ ':start' : { S : `PAGEVIEW#${ cutoff . toISOString ( ) } ` } ,
281+ ':end' : { S : 'PAGEVIEW#Z' } ,
282+ } ,
283+ ScanIndexForward : false ,
284+ } )
285+
286+ spinner . succeed ( `Found ${ result . Items ?. length || 0 } pageviews in last ${ minutesAgo } minutes` )
287+
288+ if ( ! result . Items || result . Items . length === 0 ) {
289+ cli . warn ( 'No recent pageviews found' )
290+ cli . info ( '' )
291+ cli . info ( 'Possible issues:' )
292+ cli . info ( ' 1. Tracking script not installed correctly' )
293+ cli . info ( ' 2. Site ID mismatch' )
294+ cli . info ( ' 3. CORS or network issues' )
295+ return
296+ }
297+
298+ const items = result . Items . map ( item => DynamoDBClient . unmarshal ( item ) )
299+ const uniqueVisitors = new Set ( items . map ( i => i . visitorId ) ) . size
300+
301+ cli . info ( '' )
302+ cli . success ( `${ uniqueVisitors } unique visitor(s) online` )
303+ cli . info ( '' )
304+
305+ // Group by path
306+ const byPath : Record < string , number > = { }
307+ for ( const item of items ) {
308+ const path = item . path || '/'
309+ byPath [ path ] = ( byPath [ path ] || 0 ) + 1
310+ }
311+
312+ cli . info ( 'Active pages:' )
313+ for ( const [ path , count ] of Object . entries ( byPath ) . sort ( ( a , b ) => b [ 1 ] - a [ 1 ] ) . slice ( 0 , 5 ) ) {
314+ cli . info ( ` ${ path } : ${ count } view(s)` )
315+ }
316+
317+ cli . info ( '' )
318+ cli . info ( 'Recent pageviews:' )
319+ for ( const item of items . slice ( 0 , 5 ) ) {
320+ cli . info ( ` ${ item . timestamp } - ${ item . path } (${ item . browser || 'Unknown' } )` )
321+ }
322+ }
323+ catch ( error : any ) {
324+ cli . error ( `Failed to check realtime: ${ error . message } ` )
325+ process . exit ( 1 )
326+ }
327+ } )
207328}
0 commit comments