an
This commit is contained in:
@@ -4,6 +4,12 @@ import clickhouse from '../utils/clickhouse';
|
||||
import { addAnalyticsJob } from '../utils/queue';
|
||||
import { getRedisClient } from '../utils/redis';
|
||||
import supabase from '../utils/supabase';
|
||||
import {
|
||||
scheduleInfluencerCollection,
|
||||
schedulePostCollection,
|
||||
removeScheduledJob,
|
||||
getScheduledJobs
|
||||
} from '../utils/scheduledTasks';
|
||||
|
||||
// Define user type
|
||||
interface User {
|
||||
@@ -519,4 +525,357 @@ analyticsRouter.get('/project/:id/interaction-types', async (c) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ===== Scheduled Collection Endpoints =====
|
||||
|
||||
// Schedule automated data collection for an influencer
|
||||
analyticsRouter.post('/schedule/influencer', async (c) => {
|
||||
try {
|
||||
const { influencer_id, cron_expression } = await c.req.json();
|
||||
|
||||
if (!influencer_id) {
|
||||
return c.json({ error: 'Influencer ID is required' }, 400);
|
||||
}
|
||||
|
||||
// Validate that the influencer exists
|
||||
const { data, error } = await supabase
|
||||
.from('influencers')
|
||||
.select('influencer_id')
|
||||
.eq('influencer_id', influencer_id)
|
||||
.single();
|
||||
|
||||
if (error || !data) {
|
||||
return c.json({ error: 'Influencer not found' }, 404);
|
||||
}
|
||||
|
||||
// Schedule the collection job
|
||||
await scheduleInfluencerCollection(
|
||||
influencer_id,
|
||||
cron_expression || '0 0 * * *' // Default: Every day at midnight
|
||||
);
|
||||
|
||||
return c.json({
|
||||
message: 'Influencer metrics collection scheduled successfully',
|
||||
influencer_id,
|
||||
cron_expression: cron_expression || '0 0 * * *'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error scheduling influencer collection:', error);
|
||||
return c.json({ error: 'Internal server error' }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// Schedule automated data collection for a post
|
||||
analyticsRouter.post('/schedule/post', async (c) => {
|
||||
try {
|
||||
const { post_id, cron_expression } = await c.req.json();
|
||||
|
||||
if (!post_id) {
|
||||
return c.json({ error: 'Post ID is required' }, 400);
|
||||
}
|
||||
|
||||
// Validate that the post exists
|
||||
const { data, error } = await supabase
|
||||
.from('posts')
|
||||
.select('post_id')
|
||||
.eq('post_id', post_id)
|
||||
.single();
|
||||
|
||||
if (error || !data) {
|
||||
return c.json({ error: 'Post not found' }, 404);
|
||||
}
|
||||
|
||||
// Schedule the collection job
|
||||
await schedulePostCollection(
|
||||
post_id,
|
||||
cron_expression || '0 0 * * *' // Default: Every day at midnight
|
||||
);
|
||||
|
||||
return c.json({
|
||||
message: 'Post metrics collection scheduled successfully',
|
||||
post_id,
|
||||
cron_expression: cron_expression || '0 0 * * *'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error scheduling post collection:', error);
|
||||
return c.json({ error: 'Internal server error' }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// Get all scheduled collection jobs
|
||||
analyticsRouter.get('/schedule', async (c) => {
|
||||
try {
|
||||
const scheduledJobs = await getScheduledJobs();
|
||||
|
||||
return c.json({
|
||||
scheduled_jobs: scheduledJobs
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching scheduled jobs:', error);
|
||||
return c.json({ error: 'Internal server error' }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// Delete a scheduled collection job
|
||||
analyticsRouter.delete('/schedule/:job_id', async (c) => {
|
||||
try {
|
||||
const jobId = c.req.param('job_id');
|
||||
|
||||
await removeScheduledJob(jobId);
|
||||
|
||||
return c.json({
|
||||
message: 'Scheduled job removed successfully',
|
||||
job_id: jobId
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error removing scheduled job:', error);
|
||||
return c.json({ error: 'Internal server error' }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// ===== Data Export Endpoints =====
|
||||
|
||||
// Export influencer growth data (CSV format)
|
||||
analyticsRouter.get('/export/influencer/:id/growth', async (c) => {
|
||||
try {
|
||||
const influencerId = c.req.param('id');
|
||||
const {
|
||||
metric = 'followers_count',
|
||||
timeframe = '6months',
|
||||
interval = 'month'
|
||||
} = c.req.query();
|
||||
|
||||
// The same logic as the influencer growth endpoint, but return CSV format
|
||||
|
||||
// Validate parameters
|
||||
const validMetrics = ['followers_count', 'video_count', 'views_count', 'likes_count'];
|
||||
if (!validMetrics.includes(metric)) {
|
||||
return c.json({ error: 'Invalid metric specified' }, 400);
|
||||
}
|
||||
|
||||
// Determine time range and interval function
|
||||
let timeRangeSql: string;
|
||||
let intervalFunction: string;
|
||||
|
||||
switch (timeframe) {
|
||||
case '30days':
|
||||
timeRangeSql = 'timestamp >= subtractDays(now(), 30)';
|
||||
break;
|
||||
case '90days':
|
||||
timeRangeSql = 'timestamp >= subtractDays(now(), 90)';
|
||||
break;
|
||||
case '6months':
|
||||
default:
|
||||
timeRangeSql = 'timestamp >= subtractMonths(now(), 6)';
|
||||
break;
|
||||
case '1year':
|
||||
timeRangeSql = 'timestamp >= subtractYears(now(), 1)';
|
||||
break;
|
||||
}
|
||||
|
||||
switch (interval) {
|
||||
case 'day':
|
||||
intervalFunction = 'toDate(timestamp)';
|
||||
break;
|
||||
case 'week':
|
||||
intervalFunction = 'toStartOfWeek(timestamp)';
|
||||
break;
|
||||
case 'month':
|
||||
default:
|
||||
intervalFunction = 'toStartOfMonth(timestamp)';
|
||||
break;
|
||||
}
|
||||
|
||||
// Query ClickHouse for data
|
||||
const result = await clickhouse.query({
|
||||
query: `
|
||||
SELECT
|
||||
${intervalFunction} AS time_period,
|
||||
sumIf(metric_value, metric_name = ?) AS change,
|
||||
maxIf(metric_total, metric_name = ?) AS total_value
|
||||
FROM promote.events
|
||||
WHERE
|
||||
influencer_id = ? AND
|
||||
event_type = ? AND
|
||||
${timeRangeSql}
|
||||
GROUP BY time_period
|
||||
ORDER BY time_period ASC
|
||||
`,
|
||||
values: [
|
||||
metric,
|
||||
metric,
|
||||
influencerId,
|
||||
`${metric}_change`
|
||||
]
|
||||
});
|
||||
|
||||
// Get influencer info
|
||||
const { data: influencer } = await supabase
|
||||
.from('influencers')
|
||||
.select('name, platform')
|
||||
.eq('influencer_id', influencerId)
|
||||
.single();
|
||||
|
||||
// Extract trend data
|
||||
const trendData = 'rows' in result ? result.rows : [];
|
||||
|
||||
// Format as CSV
|
||||
const csvHeader = `Time Period,Change,Total Value\n`;
|
||||
const csvRows = trendData.map((row: any) =>
|
||||
`${row.time_period},${row.change},${row.total_value}`
|
||||
).join('\n');
|
||||
|
||||
const influencerInfo = influencer
|
||||
? `Influencer: ${influencer.name} (${influencer.platform})\nMetric: ${metric}\nTimeframe: ${timeframe}\nInterval: ${interval}\n\n`
|
||||
: '';
|
||||
|
||||
const csvContent = influencerInfo + csvHeader + csvRows;
|
||||
|
||||
return c.body(csvContent, {
|
||||
headers: {
|
||||
'Content-Type': 'text/csv',
|
||||
'Content-Disposition': `attachment; filename="influencer_growth_${influencerId}.csv"`
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error exporting influencer growth data:', error);
|
||||
return c.json({ error: 'Internal server error' }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// Export project performance data (CSV format)
|
||||
analyticsRouter.get('/export/project/:id/performance', async (c) => {
|
||||
try {
|
||||
const projectId = c.req.param('id');
|
||||
const { timeframe = '30days' } = c.req.query();
|
||||
|
||||
// Get project information
|
||||
const { data: project, error: projectError } = await supabase
|
||||
.from('projects')
|
||||
.select('id, name, description')
|
||||
.eq('id', projectId)
|
||||
.single();
|
||||
|
||||
if (projectError) {
|
||||
return c.json({ error: 'Project not found' }, 404);
|
||||
}
|
||||
|
||||
// Get project influencers
|
||||
const { data: projectInfluencers, error: influencersError } = await supabase
|
||||
.from('project_influencers')
|
||||
.select('influencer_id')
|
||||
.eq('project_id', projectId);
|
||||
|
||||
if (influencersError) {
|
||||
console.error('Error fetching project influencers:', influencersError);
|
||||
return c.json({ error: 'Failed to fetch project data' }, 500);
|
||||
}
|
||||
|
||||
const influencerIds = projectInfluencers.map(pi => pi.influencer_id);
|
||||
|
||||
if (influencerIds.length === 0) {
|
||||
const emptyCSV = `Project: ${project.name}\nNo influencers found in this project.`;
|
||||
return c.body(emptyCSV, {
|
||||
headers: {
|
||||
'Content-Type': 'text/csv',
|
||||
'Content-Disposition': `attachment; filename="project_performance_${projectId}.csv"`
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Determine time range
|
||||
let startDate: Date;
|
||||
const endDate = new Date();
|
||||
|
||||
switch (timeframe) {
|
||||
case '7days':
|
||||
startDate = new Date(endDate);
|
||||
startDate.setDate(endDate.getDate() - 7);
|
||||
break;
|
||||
case '30days':
|
||||
default:
|
||||
startDate = new Date(endDate);
|
||||
startDate.setDate(endDate.getDate() - 30);
|
||||
break;
|
||||
case '90days':
|
||||
startDate = new Date(endDate);
|
||||
startDate.setDate(endDate.getDate() - 90);
|
||||
break;
|
||||
case '6months':
|
||||
startDate = new Date(endDate);
|
||||
startDate.setMonth(endDate.getMonth() - 6);
|
||||
break;
|
||||
}
|
||||
|
||||
// Get influencer details
|
||||
const { data: influencersData } = await supabase
|
||||
.from('influencers')
|
||||
.select('influencer_id, name, platform, followers_count')
|
||||
.in('influencer_id', influencerIds);
|
||||
|
||||
// Get metrics from ClickHouse
|
||||
const metricsResult = await clickhouse.query({
|
||||
query: `
|
||||
SELECT
|
||||
influencer_id,
|
||||
sumIf(metric_value, event_type = 'followers_count_change') AS followers_change,
|
||||
sumIf(metric_value, event_type = 'post_views_count_change') AS views_change,
|
||||
sumIf(metric_value, event_type = 'post_likes_count_change') AS likes_change
|
||||
FROM promote.events
|
||||
WHERE
|
||||
influencer_id IN (?) AND
|
||||
timestamp >= ? AND
|
||||
timestamp <= ?
|
||||
GROUP BY influencer_id
|
||||
`,
|
||||
values: [
|
||||
influencerIds,
|
||||
startDate.toISOString(),
|
||||
endDate.toISOString()
|
||||
]
|
||||
});
|
||||
|
||||
// Extract metrics data
|
||||
const metricsData = 'rows' in metricsResult ? metricsResult.rows : [];
|
||||
|
||||
// Combine data
|
||||
const reportData = (influencersData || []).map(influencer => {
|
||||
const metrics = metricsData.find((m: any) => m.influencer_id === influencer.influencer_id) || {
|
||||
followers_change: 0,
|
||||
views_change: 0,
|
||||
likes_change: 0
|
||||
};
|
||||
|
||||
return {
|
||||
influencer_id: influencer.influencer_id,
|
||||
name: influencer.name,
|
||||
platform: influencer.platform,
|
||||
followers_count: influencer.followers_count,
|
||||
followers_change: metrics.followers_change || 0,
|
||||
views_change: metrics.views_change || 0,
|
||||
likes_change: metrics.likes_change || 0
|
||||
};
|
||||
});
|
||||
|
||||
// Format as CSV
|
||||
const csvHeader = `Influencer Name,Platform,Followers Count,Followers Change,Views Change,Likes Change\n`;
|
||||
const csvRows = reportData.map(row =>
|
||||
`${row.name},${row.platform},${row.followers_count},${row.followers_change},${row.views_change},${row.likes_change}`
|
||||
).join('\n');
|
||||
|
||||
const projectInfo = `Project: ${project.name}\nDescription: ${project.description || 'N/A'}\nTimeframe: ${timeframe}\nExport Date: ${new Date().toISOString()}\n\n`;
|
||||
|
||||
const csvContent = projectInfo + csvHeader + csvRows;
|
||||
|
||||
return c.body(csvContent, {
|
||||
headers: {
|
||||
'Content-Type': 'text/csv',
|
||||
'Content-Disposition': `attachment; filename="project_performance_${projectId}.csv"`
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error exporting project performance data:', error);
|
||||
return c.json({ error: 'Internal server error' }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
export default analyticsRouter;
|
||||
Reference in New Issue
Block a user