UNPKG

recoder-code

Version:

🚀 AI-powered development platform - Chat with 32+ models, build projects, automate workflows. Free models included!

474 lines (422 loc) • 14.2 kB
/** * Analytics Service - Handles tracking and analytics for packages */ import { Repository } from 'typeorm'; import { Package, PackageStatus } from '../entities/Package'; import { PackageVersion } from '../entities/PackageVersion'; import { Download } from '../entities/Download'; import { User } from '../entities/User'; import { AppDataSource } from '../database'; export interface AnalyticsData { downloads: { total: number; daily: number; weekly: number; monthly: number; yearly: number; }; packages: { total: number; active: number; deprecated: number; }; users: { total: number; active: number; }; trends: { popularPackages: Package[]; trendingPackages: Package[]; recentlyUpdated: Package[]; }; } export interface PackageAnalytics { packageId: string; packageName: string; downloads: { total: number; daily: Record<string, number>; weekly: Record<string, number>; monthly: Record<string, number>; }; versions: { total: number; latest: string; popular: Array<{ version: string; downloads: number }>; }; dependencies: { count: number; dependents: number; }; quality: { score: number; metrics: Record<string, number>; }; } export class AnalyticsService { private packageRepo: Repository<Package>; private downloadRepo: Repository<Download>; private userRepo: Repository<User>; private logger = { info: (msg: string, ...args: any[]) => console.log(`[INFO] ${msg}`, ...args), error: (msg: string, ...args: any[]) => console.error(`[ERROR] ${msg}`, ...args), warn: (msg: string, ...args: any[]) => console.warn(`[WARN] ${msg}`, ...args) }; constructor() { this.packageRepo = AppDataSource.getRepository(Package); this.downloadRepo = AppDataSource.getRepository(Download); this.userRepo = AppDataSource.getRepository(User); } async getGlobalAnalytics(): Promise<AnalyticsData> { try { const [ totalPackages, activePackages, deprecatedPackages, totalUsers, activeUsers, totalDownloads, popularPackages, trendingPackages, recentlyUpdated ] = await Promise.all([ this.packageRepo.count(), this.packageRepo.count({ where: { status: PackageStatus.ACTIVE } }), this.packageRepo.count({ where: { status: PackageStatus.DEPRECATED } }), this.userRepo.count(), this.userRepo.count({ where: { is_active: true } }), this.getTotalDownloads(), this.getPopularPackages(10), this.getTrendingPackages(10), this.getRecentlyUpdatedPackages(10) ]); return { downloads: { total: totalDownloads, daily: await this.getDailyDownloads(), weekly: await this.getWeeklyDownloads(), monthly: await this.getMonthlyDownloads(), yearly: await this.getYearlyDownloads() }, packages: { total: totalPackages, active: activePackages, deprecated: deprecatedPackages }, users: { total: totalUsers, active: activeUsers }, trends: { popularPackages, trendingPackages, recentlyUpdated } }; } catch (error) { this.logger.error('Failed to get global analytics:', error); throw error; } } async getPackageAnalytics(packageName: string): Promise<PackageAnalytics | null> { try { const pkg = await this.packageRepo.findOne({ where: { name: packageName }, relations: ['versions'] }); if (!pkg) { return null; } const [ totalDownloads, dailyDownloads, weeklyDownloads, monthlyDownloads, dependentsCount, qualityScore ] = await Promise.all([ this.getPackageDownloads(pkg.id), this.getPackageDailyDownloads(pkg.id), this.getPackageWeeklyDownloads(pkg.id), this.getPackageMonthlyDownloads(pkg.id), this.getPackageDependents(pkg.id), this.getPackageQualityScore(pkg.id) ]); const popularVersions = await this.getPopularVersions(pkg.id); return { packageId: pkg.id, packageName: pkg.name, downloads: { total: totalDownloads, daily: dailyDownloads, weekly: weeklyDownloads, monthly: monthlyDownloads }, versions: { total: pkg.versions.length, latest: pkg.latest_version || '', popular: popularVersions }, dependencies: { count: pkg.stats?.dependencies || 0, dependents: dependentsCount }, quality: { score: qualityScore, metrics: pkg.quality_metrics || {} } }; } catch (error) { this.logger.error(`Failed to get analytics for package ${packageName}:`, error); throw error; } } async recordDownload(packageId: string, version: string, userId?: string, ip?: string): Promise<void> { try { // Record download event const download = this.downloadRepo.create({ package_id: packageId, version_id: packageId, // This should be the version ID, simplified for now user_id: userId, ip_address: ip || 'unknown', user_agent: '', // Would get from request headers date: new Date(), date_only: new Date().toISOString().split('T')[0] }); await this.downloadRepo.save(download); // Update package download count await this.packageRepo.increment({ id: packageId }, 'download_count', 1); this.logger.info(`Recorded download: ${packageId}@${version}`); } catch (error) { this.logger.error('Failed to record download:', error); throw error; } } private async getTotalDownloads(): Promise<number> { const result = await this.downloadRepo.count(); return result; } private async getDailyDownloads(): Promise<number> { const today = new Date(); today.setHours(0, 0, 0, 0); return this.downloadRepo.createQueryBuilder('download') .where('download.date >= :today', { today }) .getCount(); } private async getWeeklyDownloads(): Promise<number> { const weekAgo = new Date(); weekAgo.setDate(weekAgo.getDate() - 7); return this.downloadRepo.createQueryBuilder('download') .where('download.date >= :weekAgo', { weekAgo }) .getCount(); } private async getMonthlyDownloads(): Promise<number> { const monthAgo = new Date(); monthAgo.setMonth(monthAgo.getMonth() - 1); return this.downloadRepo.createQueryBuilder('download') .where('download.date >= :monthAgo', { monthAgo }) .getCount(); } private async getYearlyDownloads(): Promise<number> { const yearAgo = new Date(); yearAgo.setFullYear(yearAgo.getFullYear() - 1); return this.downloadRepo.createQueryBuilder('download') .where('download.date >= :yearAgo', { yearAgo }) .getCount(); } private async getPopularPackages(limit: number): Promise<Package[]> { return this.packageRepo.find({ order: { download_count: 'DESC' }, take: limit, relations: ['owner'] }); } private async getTrendingPackages(limit: number): Promise<Package[]> { // Get packages with highest download growth in the last week return this.packageRepo.createQueryBuilder('package') .leftJoinAndSelect('package.owner', 'owner') .orderBy('package.download_count', 'DESC') .take(limit) .getMany(); } private async getRecentlyUpdatedPackages(limit: number): Promise<Package[]> { return this.packageRepo.find({ order: { updated_at: 'DESC' }, take: limit, relations: ['owner'] }); } private async getPackageDownloads(packageId: string): Promise<number> { return this.downloadRepo.count({ where: { package_id: packageId } }); } private async getPackageDailyDownloads(packageId: string): Promise<Record<string, number>> { // Implementation would aggregate downloads by day return {}; } private async getPackageWeeklyDownloads(packageId: string): Promise<Record<string, number>> { // Implementation would aggregate downloads by week return {}; } private async getPackageMonthlyDownloads(packageId: string): Promise<Record<string, number>> { // Implementation would aggregate downloads by month return {}; } private async getPackageDependents(packageId: string): Promise<number> { // Implementation would count packages that depend on this package return 0; } private async getPackageQualityScore(packageId: string): Promise<number> { const pkg = await this.packageRepo.findOne({ where: { id: packageId } }); if (!pkg?.quality_metrics) { return 0; } // Calculate overall quality score from metrics const metrics = pkg.quality_metrics; return ( (metrics.code_quality || 0) + (metrics.documentation || 0) + (metrics.testing || 0) + (metrics.popularity || 0) + (metrics.maintenance || 0) ) / 5; } private async getPopularVersions(packageId: string): Promise<Array<{ version: string; downloads: number }>> { // Implementation would aggregate downloads by version return []; } async updatePackageQuality(packageId: string, metrics: Record<string, number>): Promise<void> { try { await this.packageRepo.update(packageId, { quality_metrics: metrics }); this.logger.info(`Updated quality metrics for package ${packageId}`); } catch (error) { this.logger.error(`Failed to update quality metrics for package ${packageId}:`, error); throw error; } } async getDownloadTrends(packageId: string, period: 'day' | 'week' | 'month' | 'year'): Promise<Record<string, number>> { try { // Implementation would calculate download trends for the specified period return {}; } catch (error) { this.logger.error(`Failed to get download trends for package ${packageId}:`, error); throw error; } } async trackPackageView(packageId: string, userId?: string): Promise<void> { try { // Track package view for analytics this.logger.info(`Package viewed: ${packageId} by user ${userId || 'anonymous'}`); // Implementation would: // - Record the view in analytics database // - Update view counts // - Track user engagement } catch (error) { this.logger.error(`Failed to track package view for ${packageId}:`, error); // Don't throw error for analytics failures } } async trackVersionView(packageId: string, version: string, userId?: string): Promise<void> { try { // Track package version view for analytics this.logger.info(`Package version viewed: ${packageId}@${version} by user ${userId || 'anonymous'}`); // Implementation would: // - Record the version view in analytics database // - Update version-specific view counts // - Track version popularity } catch (error) { this.logger.error(`Failed to track version view for ${packageId}@${version}:`, error); // Don't throw error for analytics failures } } async trackDownload(packageId: string, version: string, userId?: string): Promise<void> { try { // Track package download for analytics this.logger.info(`Package downloaded: ${packageId}@${version} by user ${userId || 'anonymous'}`); // Implementation would: // - Record the download in analytics database // - Update download counts // - Track download trends } catch (error) { this.logger.error(`Failed to track download for ${packageId}@${version}:`, error); // Don't throw error for analytics failures } } async getPackageStats(packageId: string): Promise<any> { try { const pkg = await this.packageRepo.findOne({ where: { id: packageId }, relations: ['versions'] }); if (!pkg) { return null; } return { packageId: pkg.id, packageName: pkg.name, downloads: { total: pkg.download_count || 0, daily: {}, weekly: {}, monthly: {} }, versions: { total: pkg.versions?.length || 0, latest: pkg.latest_version || '', popular: [] }, dependencies: { count: pkg.stats?.dependencies || 0, dependents: 0 }, quality: { score: await this.getPackageQualityScore(pkg.id), metrics: pkg.quality_metrics || {} } }; } catch (error) { this.logger.error(`Failed to get package stats for ${packageId}:`, error); return null; } } async getDownloadStats(packageId: string, period: string): Promise<any> { try { // Get download statistics for a package over a period const downloads = await this.downloadRepo.createQueryBuilder('download') .where('download.package_id = :packageId', { packageId }) .andWhere('download.date >= :startDate', { startDate: this.getStartDateForPeriod(period) }) .getCount(); return { packageId, period, downloads, breakdown: {} }; } catch (error) { this.logger.error(`Failed to get download stats for ${packageId}:`, error); return { packageId, period, downloads: 0, breakdown: {} }; } } private getStartDateForPeriod(period: string): Date { const now = new Date(); switch (period) { case 'day': return new Date(now.getTime() - 24 * 60 * 60 * 1000); case 'week': return new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); case 'month': return new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); case 'year': return new Date(now.getTime() - 365 * 24 * 60 * 60 * 1000); default: return new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); } } }