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
text/typescript
/**
* 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);
}
}
}