250 lines
7.9 KiB
TypeScript
250 lines
7.9 KiB
TypeScript
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<string, unknown>;
|
||
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<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 });
|
||
}
|
||
}
|