Files
shorturl-analytics/app/api/activities/route.ts

230 lines
7.3 KiB
TypeScript

import { NextRequest, NextResponse } from 'next/server';
import { getEvents } from '@/lib/analytics';
import { ApiResponse } from '@/lib/types';
// 扩展Event类型以包含所需字段
interface EventWithFullPath extends Record<string, any> {
event_id?: string;
event_time?: string;
event_type?: string;
visitor_id?: string;
ip_address?: string;
req_full_path?: string;
referrer?: string;
// 其他可能的字段
}
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
// Get parameters
const slug = searchParams.get('slug');
const domain = searchParams.get('domain');
const format = searchParams.get('format');
// Optional date range parameters
const startTime = searchParams.get('startTime') || undefined;
const endTime = searchParams.get('endTime') || undefined;
// 修改验证逻辑,允许只使用时间范围
// 现在只需要确保有足够的过滤条件
if ((!slug && !domain) && (!startTime && !endTime)) {
return NextResponse.json({
success: false,
error: 'Missing filter parameters: provide either slug/domain or date range'
}, { status: 400 });
}
// Construct the shortUrl from domain and slug if both are provided
let shortUrl = undefined;
if (slug && domain) {
shortUrl = `https://${domain}/${slug}`;
// Log the request for debugging
console.log('Activities API received parameters:', {
slug,
domain,
shortUrl,
startTime,
endTime
});
} else {
console.log('Activities API using time range filter:', {
startTime,
endTime
});
}
// Set default page size and page
const page = parseInt(searchParams.get('page') || '1');
const pageSize = parseInt(searchParams.get('pageSize') || '50');
// Get events for the specified filters
const { events, total } = await getEvents({
linkSlug: slug || undefined,
page,
pageSize,
startTime,
endTime,
sortBy: 'event_time',
sortOrder: 'desc'
});
// If format=csv, return CSV format data
if (format === 'csv') {
// CSV header line
let csvContent = 'time,activity,campaign,clientId,originPath\n';
// Helper function to extract utm_campaign from URL
const extractUtmCampaign = (url: string | null | undefined): string => {
if (!url) return 'demo';
try {
// Try to parse URL and extract utm_campaign parameter
const urlObj = new URL(url.startsWith('http') ? url : `https://example.com${url}`);
const campaign = urlObj.searchParams.get('utm_campaign');
if (campaign) return campaign;
// If utm_campaign is not found or URL parsing fails, use regex as fallback
const campaignMatch = url.match(/[?&]utm_campaign=([^&]+)/i);
if (campaignMatch && campaignMatch[1]) return campaignMatch[1];
} catch (_) {
// If URL parsing fails, try regex directly
const campaignMatch = url.match(/[?&]utm_campaign=([^&]+)/i);
if (campaignMatch && campaignMatch[1]) return campaignMatch[1];
}
return 'demo'; // Default value
};
// Process each event record
events.forEach(event => {
// 使用类型断言处理扩展字段
const eventWithFullPath = event as unknown as EventWithFullPath;
// Get the full URL from appropriate field
// Try different possible fields that might contain the URL
const fullUrl = eventWithFullPath.req_full_path || eventWithFullPath.referrer || '';
// Extract campaign from URL
const campaign = extractUtmCampaign(fullUrl);
// Format time
const time = eventWithFullPath.event_time ?
new Date(eventWithFullPath.event_time).toISOString().replace('T', ' ').slice(0, 19) :
'';
// Determine activity (event_type)
const activity = eventWithFullPath.event_type || '';
// Client ID (possibly part of visitor_id)
const clientId = eventWithFullPath.visitor_id?.split('-')[0] || 'undefined';
// Original path (use full URL field)
const originPath = fullUrl || 'undefined';
// Add to CSV content
csvContent += `${time},${activity},${campaign},${clientId},${originPath}\n`;
});
// Return CSV response
return new NextResponse(csvContent, {
headers: {
'Content-Type': 'text/plain'
}
});
}
// Process the events to extract useful information
const processedEvents = events.map(event => {
// Parse JSON strings to objects safely
let eventAttributes: Record<string, unknown> = {};
try {
if (typeof event.event_attributes === 'string') {
eventAttributes = JSON.parse(event.event_attributes);
} else if (typeof event.event_attributes === 'object') {
eventAttributes = event.event_attributes;
}
} catch {
// Keep default empty object if parsing fails
}
// Extract tags
let tags: string[] = [];
try {
if (typeof event.link_tags === 'string') {
const parsedTags = JSON.parse(event.link_tags);
if (Array.isArray(parsedTags)) {
tags = parsedTags;
}
} else if (Array.isArray(event.link_tags)) {
tags = event.link_tags;
}
} catch {
// If parsing fails, keep tags as empty array
}
// Return a simplified event object
return {
id: event.event_id,
type: event.event_type,
time: event.event_time,
visitor: {
id: event.visitor_id,
ipAddress: event.ip_address,
userAgent: eventAttributes.user_agent as string || null,
referrer: eventAttributes.referrer as string || null
},
device: {
type: event.device_type,
browser: event.browser,
os: event.os
},
location: {
country: event.country,
city: event.city
},
link: {
id: event.link_id,
slug: event.link_slug,
originalUrl: event.link_original_url,
label: event.link_label,
tags
},
utm: {
source: eventAttributes.utm_source as string || null,
medium: eventAttributes.utm_medium as string || null,
campaign: eventAttributes.utm_campaign as string || null,
term: eventAttributes.utm_term as string || null,
content: eventAttributes.utm_content as string || null
}
};
});
// Return processed events
const response: ApiResponse<typeof processedEvents> = {
success: true,
data: processedEvents,
meta: {
total,
page,
pageSize
}
};
return NextResponse.json(response);
} catch (error) {
console.error('Error retrieving activities:', error);
const response: ApiResponse<null> = {
success: false,
data: null,
error: error instanceof Error ? error.message : 'An error occurred while retrieving activities'
};
return NextResponse.json(response, { status: 500 });
}
}