/** * Query: GetEntityAnalyticsQuery * * Retrieves analytics data for an entity (league, driver, team, race). * Returns metrics formatted for display to sponsors and admins. */ import type { AsyncUseCase } from '@gridpilot/shared/application'; import type { Logger } from '@gridpilot/shared/application'; import type { IPageViewRepository } from '../../domain/repositories/IPageViewRepository'; import type { IEngagementRepository } from '../../domain/repositories/IEngagementRepository'; import type { IAnalyticsSnapshotRepository } from '../../domain/repositories/IAnalyticsSnapshotRepository'; import type { EntityType } from '../../domain/types/PageView'; import type { SnapshotPeriod } from '../../domain/types/AnalyticsSnapshot'; export interface GetEntityAnalyticsInput { entityType: EntityType; entityId: string; period?: SnapshotPeriod; since?: Date; } export interface EntityAnalyticsOutput { entityType: EntityType; entityId: string; summary: { totalPageViews: number; uniqueVisitors: number; sponsorClicks: number; engagementScore: number; trustIndicator: 'high' | 'medium' | 'low'; exposureValue: number; }; trends: { pageViewsChange: number; uniqueVisitorsChange: number; engagementChange: number; }; period: { start: Date; end: Date; label: string; }; } export class GetEntityAnalyticsQuery implements AsyncUseCase { constructor( private readonly pageViewRepository: IPageViewRepository, private readonly engagementRepository: IEngagementRepository, private readonly snapshotRepository: IAnalyticsSnapshotRepository, private readonly logger: Logger ) {} async execute(input: GetEntityAnalyticsInput): Promise { this.logger.debug(`Executing GetEntityAnalyticsQuery with input: ${JSON.stringify(input)}`); const period = input.period ?? 'weekly'; const now = new Date(); const since = input.since ?? this.getPeriodStartDate(now, period); this.logger.debug(`Calculated period: ${period}, now: ${now.toISOString()}, since: ${since.toISOString()}`); // Get current metrics let totalPageViews = 0; try { totalPageViews = await this.pageViewRepository.countByEntityId( input.entityType, input.entityId, since ); this.logger.debug(`Total page views for entity ${input.entityId}: ${totalPageViews}`); } catch (error) { this.logger.error(`Error counting total page views for entity ${input.entityId}: ${error.message}`); throw error; } let uniqueVisitors = 0; try { uniqueVisitors = await this.pageViewRepository.countUniqueVisitors( input.entityType, input.entityId, since ); this.logger.debug(`Unique visitors for entity ${input.entityId}: ${uniqueVisitors}`); } catch (error) { this.logger.error(`Error counting unique visitors for entity ${input.entityId}: ${error.message}`); throw error; } let sponsorClicks = 0; try { sponsorClicks = await this.engagementRepository.getSponsorClicksForEntity( input.entityId, since ); this.logger.debug(`Sponsor clicks for entity ${input.entityId}: ${sponsorClicks}`); } catch (error) { this.logger.error(`Error getting sponsor clicks for entity ${input.entityId}: ${error.message}`); throw error; } // Calculate engagement score (weighted sum of actions) let engagementScore = 0; try { engagementScore = await this.calculateEngagementScore(input.entityId, since); this.logger.debug(`Engagement score for entity ${input.entityId}: ${engagementScore}`); } catch (error) { this.logger.error(`Error calculating engagement score for entity ${input.entityId}: ${error.message}`); throw error; } // Determine trust indicator const trustIndicator = this.determineTrustIndicator(totalPageViews, uniqueVisitors, engagementScore); this.logger.debug(`Trust indicator for entity ${input.entityId}: ${trustIndicator}`); // Calculate exposure value (for sponsor ROI) const exposureValue = this.calculateExposureValue(totalPageViews, uniqueVisitors, sponsorClicks); this.logger.debug(`Exposure value for entity ${input.entityId}: ${exposureValue}`); // Get previous period for trends const previousPeriodStart = this.getPreviousPeriodStart(since, period); this.logger.debug(`Previous period start: ${previousPeriodStart.toISOString()}`); let previousPageViews = 0; try { const fullPreviousPageViews = await this.pageViewRepository.countByEntityId( input.entityType, input.entityId, previousPeriodStart ); previousPageViews = fullPreviousPageViews - totalPageViews; // This calculates change, not just previous period's total this.logger.debug(`Previous period full page views: ${fullPreviousPageViews}, change: ${previousPageViews}`); } catch (error) { this.logger.error(`Error counting previous period page views for entity ${input.entityId}: ${error.message}`); throw error; } let previousUniqueVisitors = 0; try { const fullPreviousUniqueVisitors = await this.pageViewRepository.countUniqueVisitors( input.entityType, input.entityId, previousPeriodStart ); previousUniqueVisitors = fullPreviousUniqueVisitors - uniqueVisitors; // This calculates change, not just previous period's total this.logger.debug(`Previous period full unique visitors: ${fullPreviousUniqueVisitors}, change: ${previousUniqueVisitors}`); } catch (error) { this.logger.error(`Error counting previous period unique visitors for entity ${input.entityId}: ${error.message}`); throw error; } const result: EntityAnalyticsOutput = { entityType: input.entityType, entityId: input.entityId, summary: { totalPageViews, uniqueVisitors, sponsorClicks, engagementScore, trustIndicator, exposureValue, }, trends: { pageViewsChange: this.calculatePercentageChange(previousPageViews, totalPageViews), uniqueVisitorsChange: this.calculatePercentageChange(previousUniqueVisitors, uniqueVisitors), engagementChange: 0, // Would need historical engagement data }, period: { start: since, end: now, label: this.formatPeriodLabel(since, now), }, }; this.logger.info(`Successfully retrieved analytics for entity ${input.entityId}.`); return result; } private getPeriodStartDate(now: Date, period: SnapshotPeriod): Date { this.logger.debug(`Calculating period start date for "${period}" from ${now.toISOString()}`); const start = new Date(now); switch (period) { case 'daily': start.setDate(start.getDate() - 1); break; case 'weekly': start.setDate(start.getDate() - 7); break; case 'monthly': start.setMonth(start.getMonth() - 1); break; } this.logger.debug(`Period start date calculated: ${start.toISOString()}`); return start; } private getPreviousPeriodStart(currentStart: Date, period: SnapshotPeriod): Date { this.logger.debug(`Calculating previous period start date for "${period}" from ${currentStart.toISOString()}`); const start = new Date(currentStart); switch (period) { case 'daily': start.setDate(start.getDate() - 1); break; case 'weekly': start.setDate(start.getDate() - 7); break; case 'monthly': start.setMonth(start.getMonth() - 1); break; } this.logger.debug(`Previous period start date calculated: ${start.toISOString()}`); return start; } private async calculateEngagementScore(entityId: string, since: Date): Promise { this.logger.debug(`Calculating engagement score for entity ${entityId} since ${since.toISOString()}`); let sponsorClicks = 0; try { sponsorClicks = await this.engagementRepository.getSponsorClicksForEntity(entityId, since); this.logger.debug(`Sponsor clicks for engagement score for entity ${entityId}: ${sponsorClicks}`); } catch (error) { this.logger.error(`Error getting sponsor clicks for engagement score for entity ${entityId}: ${error.message}`); throw error; } const score = sponsorClicks * 10; // Weighted score this.logger.debug(`Calculated engagement score for entity ${entityId}: ${score}`); return score; } private determineTrustIndicator( pageViews: number, uniqueVisitors: number, engagementScore: number ): 'high' | 'medium' | 'low' { this.logger.debug(`Determining trust indicator with pageViews: ${pageViews}, uniqueVisitors: ${uniqueVisitors}, engagementScore: ${engagementScore}`); const engagementRate = pageViews > 0 ? engagementScore / pageViews : 0; const returningVisitorRate = pageViews > 0 ? (pageViews - uniqueVisitors) / pageViews : 0; this.logger.debug(`Engagement rate: ${engagementRate}, Returning visitor rate: ${returningVisitorRate}`); if (engagementRate > 0.1 && returningVisitorRate > 0.3) { return 'high'; } if (engagementRate > 0.05 || returningVisitorRate > 0.1) { return 'medium'; } return 'low'; } private calculateExposureValue( pageViews: number, uniqueVisitors: number, sponsorClicks: number ): number { this.logger.debug(`Calculating exposure value with pageViews: ${pageViews}, uniqueVisitors: ${uniqueVisitors}, sponsorClicks: ${sponsorClicks}`); // Simple exposure value calculation (could be monetized) const exposure = (pageViews * 0.01) + (uniqueVisitors * 0.05) + (sponsorClicks * 0.50); this.logger.debug(`Calculated exposure value: ${exposure}`); return exposure; } private calculatePercentageChange(previous: number, current: number): number { this.logger.debug(`Calculating percentage change from previous: ${previous} to current: ${current}`); if (previous === 0) { const change = current > 0 ? 100 : 0; this.logger.debug(`Percentage change (previous was 0): ${change}%`); return change; } const change = Math.round(((current - previous) / previous) * 100); this.logger.debug(`Percentage change: ${change}%`); return change; } private formatPeriodLabel(start: Date, end: Date): string { this.logger.debug(`Formatting period label from ${start.toISOString()} to ${end.toISOString()}`); const formatter = new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric', }); const label = `${formatter.format(start)} - ${formatter.format(end)}`; this.logger.debug(`Formatted period label: "${label}"`); return label; } }