684 lines
21 KiB
TypeScript
684 lines
21 KiB
TypeScript
import { Context } from 'hono';
|
|
import { analyticsService } from '../services/analyticsService';
|
|
import { logger } from '../utils/logger';
|
|
|
|
/**
|
|
* Controller for analytics endpoints
|
|
*/
|
|
export class AnalyticsController {
|
|
/**
|
|
* Get KOL performance overview
|
|
* Returns card-style layout showing key performance metrics for each KOL
|
|
*
|
|
* @param c Hono Context
|
|
* @returns Response with KOL performance data
|
|
*/
|
|
async getKolOverview(c: Context) {
|
|
const requestId = `req_${Date.now()}_${Math.random().toString(36).substring(2, 10)}`;
|
|
const startTime = Date.now();
|
|
|
|
try {
|
|
// Get query parameters for time range filtering
|
|
const timeRange = c.req.query('timeRange') || '30'; // Default to 30 days
|
|
const projectId = c.req.query('projectId'); // Optional project filter
|
|
const sortBy = c.req.query('sortBy') || 'followers_change'; // Default sort by followers change
|
|
const sortOrder = c.req.query('sortOrder') || 'desc'; // Default to descending order
|
|
const limit = parseInt(c.req.query('limit') || '20', 10); // Default limit to 20 KOLs
|
|
const offset = parseInt(c.req.query('offset') || '0', 10); // Default offset to 0
|
|
const debug = c.req.query('debug') || 'false'; // Default debug to false
|
|
|
|
logger.info(`[${requestId}] KOL overview request received`, {
|
|
timeRange,
|
|
projectId,
|
|
sortBy,
|
|
sortOrder,
|
|
limit,
|
|
offset,
|
|
debug,
|
|
userAgent: c.req.header('user-agent'),
|
|
ip: c.req.header('x-forwarded-for') || 'unknown'
|
|
});
|
|
|
|
// Validate time range
|
|
if (!['7', '30', '90'].includes(timeRange)) {
|
|
logger.warn(`[${requestId}] Invalid timeRange: ${timeRange}`);
|
|
return c.json({
|
|
success: false,
|
|
error: 'Invalid timeRange. Must be 7, 30, or 90.'
|
|
}, 400);
|
|
}
|
|
|
|
// Validate sort order
|
|
if (!['asc', 'desc'].includes(sortOrder)) {
|
|
logger.warn(`[${requestId}] Invalid sortOrder: ${sortOrder}`);
|
|
return c.json({
|
|
success: false,
|
|
error: 'Invalid sortOrder. Must be asc or desc.'
|
|
}, 400);
|
|
}
|
|
|
|
// Validate sort field
|
|
const validSortFields = ['followers_change', 'likes_change', 'follows_change', 'followers_count'];
|
|
if (!validSortFields.includes(sortBy)) {
|
|
logger.warn(`[${requestId}] Invalid sortBy: ${sortBy}`);
|
|
return c.json({
|
|
success: false,
|
|
error: `Invalid sortBy. Must be one of: ${validSortFields.join(', ')}`
|
|
}, 400);
|
|
}
|
|
|
|
// Get KOL overview data from the service
|
|
const data = await analyticsService.getKolPerformanceOverview(
|
|
parseInt(timeRange, 10),
|
|
projectId,
|
|
sortBy,
|
|
sortOrder,
|
|
limit,
|
|
offset
|
|
);
|
|
|
|
// Debug mode - log additional event data
|
|
if (debug.toLowerCase() === 'true' && process.env.NODE_ENV !== 'production') {
|
|
await analyticsService.debugEventData();
|
|
}
|
|
|
|
// Log successful response
|
|
const duration = Date.now() - startTime;
|
|
logger.info(`[${requestId}] KOL overview response sent successfully`, {
|
|
duration,
|
|
resultCount: data.kols.length,
|
|
totalRecords: data.total
|
|
});
|
|
|
|
// Return the data
|
|
return c.json({
|
|
success: true,
|
|
data: data.kols,
|
|
pagination: {
|
|
limit,
|
|
offset,
|
|
total: data.total
|
|
}
|
|
});
|
|
} catch (error) {
|
|
// Log error
|
|
const duration = Date.now() - startTime;
|
|
logger.error(`[${requestId}] Error fetching KOL overview (${duration}ms)`, error);
|
|
|
|
// Return error response
|
|
return c.json({
|
|
success: false,
|
|
error: 'Failed to fetch KOL overview data',
|
|
message: error instanceof Error ? error.message : 'Unknown error'
|
|
}, 500);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get KOL conversion funnel data
|
|
* Returns user counts and conversion rates for each funnel stage
|
|
*
|
|
* @param c Hono Context
|
|
* @returns Response with funnel data
|
|
*/
|
|
async getKolFunnel(c: Context) {
|
|
const requestId = `req_${Date.now()}_${Math.random().toString(36).substring(2, 10)}`;
|
|
const startTime = Date.now();
|
|
|
|
try {
|
|
// Get query parameters
|
|
const timeRange = c.req.query('timeRange') || '30'; // Default to 30 days
|
|
const projectId = c.req.query('projectId'); // Optional project filter
|
|
const debug = c.req.query('debug') || 'false'; // Debug mode
|
|
|
|
logger.info(`[${requestId}] KOL funnel request received`, {
|
|
timeRange,
|
|
projectId,
|
|
debug,
|
|
userAgent: c.req.header('user-agent'),
|
|
ip: c.req.header('x-forwarded-for') || 'unknown'
|
|
});
|
|
|
|
// Validate time range
|
|
if (!['7', '30', '90'].includes(timeRange)) {
|
|
logger.warn(`[${requestId}] Invalid timeRange: ${timeRange}`);
|
|
return c.json({
|
|
success: false,
|
|
error: 'Invalid timeRange. Must be 7, 30, or 90.'
|
|
}, 400);
|
|
}
|
|
|
|
// Get funnel data from service
|
|
const data = await analyticsService.getKolFunnel(
|
|
parseInt(timeRange, 10),
|
|
projectId
|
|
);
|
|
|
|
// Debug mode - log additional data
|
|
if (debug.toLowerCase() === 'true' && process.env.NODE_ENV !== 'production') {
|
|
await analyticsService.debugEventData();
|
|
}
|
|
|
|
// Log successful response
|
|
const duration = Date.now() - startTime;
|
|
logger.info(`[${requestId}] KOL funnel response sent successfully`, {
|
|
duration,
|
|
stageCount: data.stages.length
|
|
});
|
|
|
|
// Return the data
|
|
return c.json({
|
|
success: true,
|
|
data
|
|
});
|
|
} catch (error) {
|
|
// Log error
|
|
const duration = Date.now() - startTime;
|
|
logger.error(`[${requestId}] Error fetching KOL funnel (${duration}ms)`, error);
|
|
|
|
// Return error response
|
|
return c.json({
|
|
success: false,
|
|
error: 'Failed to fetch KOL funnel data',
|
|
message: error instanceof Error ? error.message : 'Unknown error'
|
|
}, 500);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get KOL post performance data
|
|
* Returns table data of posts with key metrics and sentiment scores
|
|
*
|
|
* @param c Hono Context
|
|
* @returns Response with post performance data
|
|
*/
|
|
async getPostPerformance(c: Context): Promise<Response> {
|
|
const startTime = Date.now();
|
|
// 解析查询参数
|
|
const kolId = c.req.query('kolId');
|
|
const platform = c.req.query('platform');
|
|
const startDate = c.req.query('startDate');
|
|
const endDate = c.req.query('endDate');
|
|
const sortBy = c.req.query('sortBy');
|
|
const sortOrder = c.req.query('sortOrder');
|
|
const limit = c.req.query('limit');
|
|
const offset = c.req.query('offset');
|
|
|
|
// 记录请求参数
|
|
logger.info('Post performance data requested', {
|
|
kolId, platform, startDate, endDate,
|
|
sortBy, sortOrder, limit, offset
|
|
});
|
|
|
|
try {
|
|
// 验证时间范围
|
|
if (startDate && !this.isValidDateFormat(startDate)) {
|
|
return c.json({
|
|
success: false,
|
|
error: 'Invalid startDate format. Expected YYYY-MM-DD'
|
|
}, 400);
|
|
}
|
|
|
|
if (endDate && !this.isValidDateFormat(endDate)) {
|
|
return c.json({
|
|
success: false,
|
|
error: 'Invalid endDate format. Expected YYYY-MM-DD'
|
|
}, 400);
|
|
}
|
|
|
|
// 验证排序字段
|
|
const validSortFields = ['publish_date', 'views', 'likes', 'comments', 'shares', 'sentiment_score'];
|
|
if (sortBy && !validSortFields.includes(sortBy)) {
|
|
return c.json({
|
|
success: false,
|
|
error: `Invalid sortBy field. Expected one of: ${validSortFields.join(', ')}`
|
|
}, 400);
|
|
}
|
|
|
|
// 验证排序顺序
|
|
if (sortOrder && !['asc', 'desc', 'ASC', 'DESC'].includes(sortOrder)) {
|
|
return c.json({
|
|
success: false,
|
|
error: 'Invalid sortOrder. Expected: asc or desc'
|
|
}, 400);
|
|
}
|
|
|
|
// 验证限制数量和偏移量
|
|
const parsedLimit = limit ? parseInt(limit, 10) : 10;
|
|
if (isNaN(parsedLimit) || parsedLimit < 1 || parsedLimit > 100) {
|
|
return c.json({
|
|
success: false,
|
|
error: 'Invalid limit. Expected a number between 1 and 100'
|
|
}, 400);
|
|
}
|
|
|
|
const parsedOffset = offset ? parseInt(offset, 10) : 0;
|
|
if (isNaN(parsedOffset) || parsedOffset < 0) {
|
|
return c.json({
|
|
success: false,
|
|
error: 'Invalid offset. Expected a non-negative number'
|
|
}, 400);
|
|
}
|
|
|
|
// 获取帖文表现数据
|
|
const { posts, total } = await analyticsService.getPostPerformance(
|
|
kolId || undefined,
|
|
platform || undefined,
|
|
startDate || undefined,
|
|
endDate || undefined,
|
|
sortBy || 'publish_date',
|
|
sortOrder || 'desc',
|
|
parsedLimit,
|
|
parsedOffset
|
|
);
|
|
|
|
// 返回结果
|
|
const duration = Date.now() - startTime;
|
|
logger.info(`Post performance data returned (${duration}ms)`, {
|
|
totalPosts: total,
|
|
returnedPosts: posts.length
|
|
});
|
|
|
|
return c.json({
|
|
success: true,
|
|
data: posts,
|
|
meta: {
|
|
total,
|
|
returned: posts.length,
|
|
limit: parsedLimit,
|
|
offset: parsedOffset
|
|
}
|
|
});
|
|
} catch (error) {
|
|
const duration = Date.now() - startTime;
|
|
logger.error(`Error getting post performance data (${duration}ms)`, error);
|
|
|
|
return c.json({
|
|
success: false,
|
|
error: 'Failed to retrieve post performance data'
|
|
}, 500);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate date string format (YYYY-MM-DD)
|
|
* @param dateString Date string to validate
|
|
* @returns True if valid date format
|
|
*/
|
|
private isValidDateFormat(dateString: string): boolean {
|
|
const regex = /^\d{4}-\d{2}-\d{2}$/;
|
|
if (!regex.test(dateString)) return false;
|
|
|
|
const date = new Date(dateString);
|
|
return date instanceof Date && !isNaN(date.getTime());
|
|
}
|
|
|
|
/**
|
|
* 获取概览卡片数据
|
|
* 返回包含留言总数、平均互动率和情感分析三个卡片数据
|
|
*
|
|
* @param c Hono Context
|
|
* @returns Response with dashboard card data
|
|
*/
|
|
async getDashboardCards(c: Context) {
|
|
const requestId = `req_${Date.now()}_${Math.random().toString(36).substring(2, 10)}`;
|
|
const startTime = Date.now();
|
|
|
|
try {
|
|
// 获取查询参数
|
|
const timeRange = c.req.query('timeRange') || '30'; // 默认30天
|
|
const projectId = c.req.query('projectId'); // 可选项目过滤
|
|
|
|
logger.info(`[${requestId}] Dashboard cards request received`, {
|
|
timeRange,
|
|
projectId,
|
|
userAgent: c.req.header('user-agent'),
|
|
ip: c.req.header('x-forwarded-for') || 'unknown'
|
|
});
|
|
|
|
// 验证时间范围
|
|
if (!['7', '30', '90'].includes(timeRange)) {
|
|
logger.warn(`[${requestId}] Invalid timeRange: ${timeRange}`);
|
|
return c.json({
|
|
success: false,
|
|
error: 'Invalid timeRange. Must be 7, 30, or 90.'
|
|
}, 400);
|
|
}
|
|
|
|
// 获取概览卡片数据
|
|
const data = await analyticsService.getDashboardCardData(
|
|
parseInt(timeRange, 10),
|
|
projectId
|
|
);
|
|
|
|
// 返回成功响应
|
|
const duration = Date.now() - startTime;
|
|
logger.info(`[${requestId}] Dashboard cards response sent successfully`, {
|
|
duration
|
|
});
|
|
|
|
return c.json({
|
|
success: true,
|
|
data
|
|
});
|
|
} catch (error) {
|
|
// 记录错误
|
|
const duration = Date.now() - startTime;
|
|
logger.error(`[${requestId}] Error fetching dashboard cards (${duration}ms)`, error);
|
|
|
|
// 返回错误响应
|
|
return c.json({
|
|
success: false,
|
|
error: 'Failed to fetch dashboard card data',
|
|
message: error instanceof Error ? error.message : 'Unknown error'
|
|
}, 500);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 获取留言趋势数据
|
|
* 返回一段时间内留言数量的变化趋势
|
|
*
|
|
* @param c Hono Context
|
|
* @returns Response with comment trend data
|
|
*/
|
|
async getCommentTrend(c: Context) {
|
|
const requestId = `req_${Date.now()}_${Math.random().toString(36).substring(2, 10)}`;
|
|
const startTime = Date.now();
|
|
|
|
try {
|
|
// 获取查询参数
|
|
const timeRange = c.req.query('timeRange') || '30'; // 默认30天
|
|
const projectId = c.req.query('projectId'); // 可选项目过滤
|
|
const platform = c.req.query('platform'); // 可选平台过滤
|
|
|
|
logger.info(`[${requestId}] Comment trend request received`, {
|
|
timeRange,
|
|
projectId,
|
|
platform,
|
|
userAgent: c.req.header('user-agent'),
|
|
ip: c.req.header('x-forwarded-for') || 'unknown'
|
|
});
|
|
|
|
// 验证时间范围
|
|
if (!['7', '30', '90'].includes(timeRange)) {
|
|
logger.warn(`[${requestId}] Invalid timeRange: ${timeRange}`);
|
|
return c.json({
|
|
success: false,
|
|
error: 'Invalid timeRange. Must be 7, 30, or 90.'
|
|
}, 400);
|
|
}
|
|
|
|
// 获取留言趋势数据
|
|
const data = await analyticsService.getCommentTrend(
|
|
parseInt(timeRange, 10),
|
|
projectId,
|
|
platform
|
|
);
|
|
|
|
// 返回成功响应
|
|
const duration = Date.now() - startTime;
|
|
logger.info(`[${requestId}] Comment trend response sent successfully`, {
|
|
duration,
|
|
dataPoints: data.data.length,
|
|
totalComments: data.total_count
|
|
});
|
|
|
|
return c.json({
|
|
success: true,
|
|
data: data.data,
|
|
metadata: {
|
|
max_count: data.max_count,
|
|
total_count: data.total_count
|
|
}
|
|
});
|
|
} catch (error) {
|
|
// 记录错误
|
|
const duration = Date.now() - startTime;
|
|
logger.error(`[${requestId}] Error fetching comment trend (${duration}ms)`, error);
|
|
|
|
// 返回错误响应
|
|
return c.json({
|
|
success: false,
|
|
error: 'Failed to fetch comment trend data',
|
|
message: error instanceof Error ? error.message : 'Unknown error'
|
|
}, 500);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 获取平台分布数据
|
|
* 返回不同社交平台上的事件分布情况
|
|
*
|
|
* @param c Hono Context
|
|
* @returns Response with platform distribution data
|
|
*/
|
|
async getPlatformDistribution(c: Context) {
|
|
const requestId = `req_${Date.now()}_${Math.random().toString(36).substring(2, 10)}`;
|
|
const startTime = Date.now();
|
|
|
|
try {
|
|
// 获取查询参数
|
|
const timeRange = c.req.query('timeRange') || '30'; // 默认30天
|
|
const projectId = c.req.query('projectId'); // 可选项目过滤
|
|
const eventType = c.req.query('eventType') || 'comment'; // 默认分析评论事件
|
|
|
|
logger.info(`[${requestId}] Platform distribution request received`, {
|
|
timeRange,
|
|
projectId,
|
|
eventType,
|
|
userAgent: c.req.header('user-agent'),
|
|
ip: c.req.header('x-forwarded-for') || 'unknown'
|
|
});
|
|
|
|
// 验证时间范围
|
|
if (!['7', '30', '90'].includes(timeRange)) {
|
|
logger.warn(`[${requestId}] Invalid timeRange: ${timeRange}`);
|
|
return c.json({
|
|
success: false,
|
|
error: 'Invalid timeRange. Must be 7, 30, or 90.'
|
|
}, 400);
|
|
}
|
|
|
|
// 验证事件类型
|
|
const validEventTypes = ['comment', 'like', 'view', 'share'];
|
|
if (!validEventTypes.includes(eventType)) {
|
|
logger.warn(`[${requestId}] Invalid eventType: ${eventType}`);
|
|
return c.json({
|
|
success: false,
|
|
error: `Invalid eventType. Must be one of: ${validEventTypes.join(', ')}`
|
|
}, 400);
|
|
}
|
|
|
|
// 获取平台分布数据
|
|
const data = await analyticsService.getPlatformDistribution(
|
|
parseInt(timeRange, 10),
|
|
projectId,
|
|
eventType
|
|
);
|
|
|
|
// 返回成功响应
|
|
const duration = Date.now() - startTime;
|
|
logger.info(`[${requestId}] Platform distribution response sent successfully`, {
|
|
duration,
|
|
platformCount: data.data.length,
|
|
total: data.total
|
|
});
|
|
|
|
return c.json({
|
|
success: true,
|
|
data: data.data,
|
|
metadata: {
|
|
total: data.total,
|
|
event_type: eventType
|
|
}
|
|
});
|
|
} catch (error) {
|
|
// 记录错误
|
|
const duration = Date.now() - startTime;
|
|
logger.error(`[${requestId}] Error fetching platform distribution (${duration}ms)`, error);
|
|
|
|
// 返回错误响应
|
|
return c.json({
|
|
success: false,
|
|
error: 'Failed to fetch platform distribution data',
|
|
message: error instanceof Error ? error.message : 'Unknown error'
|
|
}, 500);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 获取情感分析详情数据
|
|
* 返回正面、中性、负面评论的比例和分析
|
|
*
|
|
* @param c Hono Context
|
|
* @returns Response with sentiment analysis data
|
|
*/
|
|
async getSentimentAnalysis(c: Context) {
|
|
const requestId = `req_${Date.now()}_${Math.random().toString(36).substring(2, 10)}`;
|
|
const startTime = Date.now();
|
|
|
|
try {
|
|
// 获取查询参数
|
|
const timeRange = c.req.query('timeRange') || '30'; // 默认30天
|
|
const projectId = c.req.query('projectId'); // 可选项目过滤
|
|
const platform = c.req.query('platform'); // 可选平台过滤
|
|
|
|
logger.info(`[${requestId}] Sentiment analysis request received`, {
|
|
timeRange,
|
|
projectId,
|
|
platform,
|
|
userAgent: c.req.header('user-agent'),
|
|
ip: c.req.header('x-forwarded-for') || 'unknown'
|
|
});
|
|
|
|
// 验证时间范围
|
|
if (!['7', '30', '90'].includes(timeRange)) {
|
|
logger.warn(`[${requestId}] Invalid timeRange: ${timeRange}`);
|
|
return c.json({
|
|
success: false,
|
|
error: 'Invalid timeRange. Must be 7, 30, or 90.'
|
|
}, 400);
|
|
}
|
|
|
|
// 获取情感分析数据
|
|
const data = await analyticsService.getSentimentAnalysis(
|
|
parseInt(timeRange, 10),
|
|
projectId,
|
|
platform
|
|
);
|
|
|
|
// 返回成功响应
|
|
const duration = Date.now() - startTime;
|
|
logger.info(`[${requestId}] Sentiment analysis response sent successfully`, {
|
|
duration,
|
|
total: data.total
|
|
});
|
|
|
|
return c.json({
|
|
success: true,
|
|
data
|
|
});
|
|
} catch (error) {
|
|
// 记录错误
|
|
const duration = Date.now() - startTime;
|
|
logger.error(`[${requestId}] Error fetching sentiment analysis (${duration}ms)`, error);
|
|
|
|
// 返回错误响应
|
|
return c.json({
|
|
success: false,
|
|
error: 'Failed to fetch sentiment analysis data',
|
|
message: error instanceof Error ? error.message : 'Unknown error'
|
|
}, 500);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 获取热门文章数据
|
|
* 返回按互动数量或互动率排序的热门帖文
|
|
*
|
|
* @param c Hono Context
|
|
* @returns Response with popular posts data
|
|
*/
|
|
async getPopularPosts(c: Context) {
|
|
const requestId = `req_${Date.now()}_${Math.random().toString(36).substring(2, 10)}`;
|
|
const startTime = Date.now();
|
|
|
|
try {
|
|
// 获取查询参数
|
|
const timeRange = c.req.query('timeRange') || '30'; // 默认30天
|
|
const projectId = c.req.query('projectId'); // 可选项目过滤
|
|
const platform = c.req.query('platform'); // 可选平台过滤
|
|
const sortBy = c.req.query('sortBy') || 'engagement_count'; // 默认按互动数量排序
|
|
const limit = parseInt(c.req.query('limit') || '10', 10); // 默认返回10个
|
|
|
|
logger.info(`[${requestId}] Popular posts request received`, {
|
|
timeRange,
|
|
projectId,
|
|
platform,
|
|
sortBy,
|
|
limit,
|
|
userAgent: c.req.header('user-agent'),
|
|
ip: c.req.header('x-forwarded-for') || 'unknown'
|
|
});
|
|
|
|
// 验证时间范围
|
|
if (!['7', '30', '90'].includes(timeRange)) {
|
|
logger.warn(`[${requestId}] Invalid timeRange: ${timeRange}`);
|
|
return c.json({
|
|
success: false,
|
|
error: 'Invalid timeRange. Must be 7, 30, or 90.'
|
|
}, 400);
|
|
}
|
|
|
|
// 验证排序字段
|
|
if (!['engagement_count', 'engagement_rate'].includes(sortBy)) {
|
|
logger.warn(`[${requestId}] Invalid sortBy: ${sortBy}`);
|
|
return c.json({
|
|
success: false,
|
|
error: 'Invalid sortBy. Must be engagement_count or engagement_rate.'
|
|
}, 400);
|
|
}
|
|
|
|
// 获取热门文章数据
|
|
const data = await analyticsService.getPopularPosts(
|
|
parseInt(timeRange, 10),
|
|
projectId,
|
|
platform,
|
|
sortBy,
|
|
Math.min(limit, 50) // 最多返回50条
|
|
);
|
|
|
|
// 返回成功响应
|
|
const duration = Date.now() - startTime;
|
|
logger.info(`[${requestId}] Popular posts response sent successfully`, {
|
|
duration,
|
|
resultCount: data.posts.length,
|
|
total: data.total
|
|
});
|
|
|
|
return c.json({
|
|
success: true,
|
|
data: data.posts,
|
|
metadata: {
|
|
total: data.total,
|
|
high_engagement_count: data.posts.filter(post => post.is_high_engagement).length
|
|
}
|
|
});
|
|
} catch (error) {
|
|
// 记录错误
|
|
const duration = Date.now() - startTime;
|
|
logger.error(`[${requestId}] Error fetching popular posts (${duration}ms)`, error);
|
|
|
|
// 返回错误响应
|
|
return c.json({
|
|
success: false,
|
|
error: 'Failed to fetch popular posts data',
|
|
message: error instanceof Error ? error.message : 'Unknown error'
|
|
}, 500);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Export singleton instance
|
|
export const analyticsController = new AnalyticsController();
|