import { NextRequest, NextResponse } from 'next/server'; import { getEvents } from '@/lib/analytics'; import { ApiResponse } from '@/lib/types'; // Extended Event type with required fields interface EventWithFullPath { event_id?: string; event_time?: string; event_type?: string; visitor_id?: string; ip_address?: string; req_full_path?: string; referrer?: string; event_attributes?: string | Record; link_tags?: string | string[]; link_id?: string; link_slug?: string; link_original_url?: string; link_label?: string; device_type?: string; browser?: string; os?: string; country?: string; city?: string; [key: string]: unknown; } 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; // Check if either slug or domain is provided without the other if ((slug && !domain) || (!slug && domain)) { return NextResponse.json({ success: false, error: 'Both slug and domain parameters must be provided together' }, { status: 400 }); } // Ensure either slug+domain or date range is provided 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 - 修正:使用link_original_url作为原始URL来源 const originPath = eventWithFullPath.link_original_url || 'undefined'; // Add to CSV content csvContent += `${time},${activity},${campaign},${clientId},${originPath}\n`; }); // No need to generate filename since we're not using Content-Disposition header // Return CSV response without forcing download 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 = {}; 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 = { success: true, data: processedEvents, meta: { total, page, pageSize } }; return NextResponse.json(response); } catch (error) { console.error('Error retrieving activities:', error); const response: ApiResponse = { success: false, data: null, error: error instanceof Error ? error.message : 'An error occurred while retrieving activities' }; return NextResponse.json(response, { status: 500 }); } }