From f5b14bf936575af6436df023f9b8e84d0b2085af Mon Sep 17 00:00:00 2001 From: William Tso Date: Wed, 26 Mar 2025 18:19:37 +0800 Subject: [PATCH] event track api --- app/(swagger)/swagger/page.tsx | 376 +++++++++++++++++++++++++++++++++ app/api/events/track/route.ts | 106 ++++++++++ 2 files changed, 482 insertions(+) create mode 100644 app/api/events/track/route.ts diff --git a/app/(swagger)/swagger/page.tsx b/app/(swagger)/swagger/page.tsx index 4a70351..0d227ee 100644 --- a/app/(swagger)/swagger/page.tsx +++ b/app/(swagger)/swagger/page.tsx @@ -39,6 +39,174 @@ export default function SwaggerPage() { }, ], paths: { + '/events/track': { + post: { + tags: ['events'], + summary: 'Track new event', + description: 'Record a new event in the analytics system', + requestBody: { + required: true, + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/EventInput', + }, + examples: { + clickEvent: { + summary: 'Basic click event', + value: { + event_type: 'click', + link_id: 'link_123', + link_slug: 'promo2023', + link_original_url: 'https://example.com/promotion', + visitor_id: '6ec0bd7f-11c0-43da-975e-2a8ad9ebae0b' + } + }, + conversionEvent: { + summary: 'Conversion event', + value: { + event_type: 'conversion', + link_id: 'link_123', + link_slug: 'promo2023', + visitor_id: '6ec0bd7f-11c0-43da-975e-2a8ad9ebae0b', + conversion_type: 'purchase', + conversion_value: 99.99 + } + }, + completeEvent: { + summary: 'Complete event with all fields', + value: { + // Core event fields + event_id: '123e4567-e89b-12d3-a456-426614174000', + event_time: '2025-03-26T10:30:00.000Z', + event_type: 'click', + event_attributes: '{"source":"email_campaign","campaign_id":"spring_sale_2025"}', + + // Link information + link_id: 'link_abc123', + link_slug: 'summer-promo', + link_label: 'Summer Promotion 2025', + link_title: 'Summer Sale 50% Off', + link_original_url: 'https://example.com/summer-sale-2025', + link_attributes: '{"utm_campaign":"summer_2025","discount_code":"SUMMER50"}', + link_created_at: '2025-03-20T08:00:00.000Z', + link_expires_at: '2025-09-30T23:59:59.000Z', + link_tags: '["promotion","summer","sale"]', + + // User information + user_id: 'user_12345', + user_name: 'John Doe', + user_email: 'john.doe@example.com', + user_attributes: '{"subscription_tier":"premium","account_created":"2024-01-15"}', + + // Team information + team_id: 'team_67890', + team_name: 'Marketing Team', + team_attributes: '{"department":"marketing","region":"APAC"}', + + // Project information + project_id: 'proj_54321', + project_name: 'Summer Campaign 2025', + project_attributes: '{"goals":"increase_sales","budget":"10000"}', + + // QR code information + qr_code_id: 'qr_98765', + qr_code_name: 'Summer Flyer QR', + qr_code_attributes: '{"size":"large","color":"#FF5500","logo":true}', + + // Visitor information + visitor_id: '6ec0bd7f-11c0-43da-975e-2a8ad9ebae0b', + session_id: '7fc1bd8f-22d1-54eb-986f-3b9be5ecaf1c', + ip_address: '203.0.113.42', + country: 'United States', + city: 'San Francisco', + device_type: 'mobile', + browser: 'Chrome', + os: 'iOS', + user_agent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5 Mobile/15E148 Safari/604.1', + + // Referrer information + referrer: 'https://www.google.com/search?q=summer+sale', + utm_source: 'google', + utm_medium: 'organic', + utm_campaign: 'summer_promotion', + + // Interaction information + time_spent_sec: 145, + is_bounce: false, + is_qr_scan: true, + conversion_type: 'signup', + conversion_value: 0 + } + } + } + } + } + }, + responses: { + '201': { + description: 'Event successfully tracked', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + success: { + type: 'boolean', + example: true + }, + message: { + type: 'string', + example: 'Event tracked successfully' + }, + event_id: { + type: 'string', + format: 'uuid', + example: '123e4567-e89b-12d3-a456-426614174000' + } + } + } + } + } + }, + '400': { + description: 'Bad request', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/Error' + }, + example: { + error: 'Missing required field: event_type' + } + } + } + }, + '500': { + description: 'Server error', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + error: { + type: 'string' + }, + details: { + type: 'string' + } + } + }, + example: { + error: 'Failed to track event', + details: 'Database connection error' + } + } + } + } + } + } + }, '/events': { get: { tags: ['events'], @@ -350,6 +518,214 @@ export default function SwaggerPage() { }, components: { schemas: { + EventInput: { + type: 'object', + required: ['event_type'], + properties: { + // Core event fields + event_id: { + type: 'string', + format: 'uuid', + description: 'Unique identifier for the event (auto-generated if not provided)' + }, + event_time: { + type: 'string', + format: 'date-time', + description: 'Time when the event occurred (defaults to current time if not provided)' + }, + event_type: { + type: 'string', + enum: ['click', 'conversion', 'redirect', 'error'], + description: 'Type of the event' + }, + event_attributes: { + type: 'string', + description: 'JSON string with additional event attributes' + }, + + // Link information + link_id: { + type: 'string', + description: 'ID of the associated short link' + }, + link_slug: { + type: 'string', + description: 'Slug of the short link' + }, + link_label: { + type: 'string', + description: 'Label of the short link' + }, + link_title: { + type: 'string', + description: 'Title of the short link' + }, + link_original_url: { + type: 'string', + format: 'uri', + description: 'Original URL of the short link' + }, + link_attributes: { + type: 'string', + description: 'JSON string with additional link attributes' + }, + link_created_at: { + type: 'string', + format: 'date-time', + description: 'Creation time of the link' + }, + link_expires_at: { + type: 'string', + format: 'date-time', + nullable: true, + description: 'Expiration time of the link' + }, + link_tags: { + type: 'string', + description: 'JSON array with link tags' + }, + + // User information + user_id: { + type: 'string', + description: 'ID of the user who created the link' + }, + user_name: { + type: 'string', + description: 'Name of the user' + }, + user_email: { + type: 'string', + format: 'email', + description: 'Email of the user' + }, + user_attributes: { + type: 'string', + description: 'JSON string with additional user attributes' + }, + + // Team information + team_id: { + type: 'string', + description: 'ID of the team' + }, + team_name: { + type: 'string', + description: 'Name of the team' + }, + team_attributes: { + type: 'string', + description: 'JSON string with additional team attributes' + }, + + // Project information + project_id: { + type: 'string', + description: 'ID of the project' + }, + project_name: { + type: 'string', + description: 'Name of the project' + }, + project_attributes: { + type: 'string', + description: 'JSON string with additional project attributes' + }, + + // QR code information + qr_code_id: { + type: 'string', + description: 'ID of the QR code' + }, + qr_code_name: { + type: 'string', + description: 'Name of the QR code' + }, + qr_code_attributes: { + type: 'string', + description: 'JSON string with additional QR code attributes' + }, + + // Visitor information + visitor_id: { + type: 'string', + format: 'uuid', + description: 'Unique identifier for the visitor' + }, + session_id: { + type: 'string', + description: 'Session identifier' + }, + ip_address: { + type: 'string', + description: 'IP address of the visitor' + }, + country: { + type: 'string', + description: 'Country of the visitor' + }, + city: { + type: 'string', + description: 'City of the visitor' + }, + device_type: { + type: 'string', + description: 'Type of device used' + }, + browser: { + type: 'string', + description: 'Browser used' + }, + os: { + type: 'string', + description: 'Operating system used' + }, + user_agent: { + type: 'string', + description: 'User agent string' + }, + + // Referrer information + referrer: { + type: 'string', + description: 'Referrer URL' + }, + utm_source: { + type: 'string', + description: 'UTM source parameter' + }, + utm_medium: { + type: 'string', + description: 'UTM medium parameter' + }, + utm_campaign: { + type: 'string', + description: 'UTM campaign parameter' + }, + + // Interaction information + time_spent_sec: { + type: 'number', + description: 'Time spent in seconds' + }, + is_bounce: { + type: 'boolean', + description: 'Whether this was a bounce visit' + }, + is_qr_scan: { + type: 'boolean', + description: 'Whether this event came from a QR code scan' + }, + conversion_type: { + type: 'string', + description: 'Type of conversion' + }, + conversion_value: { + type: 'number', + description: 'Value of the conversion' + } + } + }, Event: { type: 'object', required: ['event_id', 'event_type', 'event_time', 'visitor_id'], diff --git a/app/api/events/track/route.ts b/app/api/events/track/route.ts new file mode 100644 index 0000000..45918e8 --- /dev/null +++ b/app/api/events/track/route.ts @@ -0,0 +1,106 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { Event } from '../../types'; +import { v4 as uuid } from 'uuid'; +import clickhouse from '@/lib/clickhouse'; + +// Handler for POST request to track events +export async function POST(req: NextRequest) { + try { + // Parse request body + const eventData = await req.json(); + + // Validate required fields + if (!eventData.event_type) { + return NextResponse.json( + { error: 'Missing required field: event_type' }, + { status: 400 } + ); + } + + // Set default values for required fields if missing + const event: Event = { + // Core event fields + event_id: eventData.event_id || uuid(), + event_time: eventData.event_time || new Date().toISOString(), + event_type: eventData.event_type, + event_attributes: eventData.event_attributes || '{}', + + // Link information + link_id: eventData.link_id || '', + link_slug: eventData.link_slug || '', + link_label: eventData.link_label || '', + link_title: eventData.link_title || '', + link_original_url: eventData.link_original_url || '', + link_attributes: eventData.link_attributes || '{}', + link_created_at: eventData.link_created_at || new Date().toISOString(), + link_expires_at: eventData.link_expires_at || null, + link_tags: eventData.link_tags || '[]', + + // User information + user_id: eventData.user_id || '', + user_name: eventData.user_name || '', + user_email: eventData.user_email || '', + user_attributes: eventData.user_attributes || '{}', + + // Team information + team_id: eventData.team_id || '', + team_name: eventData.team_name || '', + team_attributes: eventData.team_attributes || '{}', + + // Project information + project_id: eventData.project_id || '', + project_name: eventData.project_name || '', + project_attributes: eventData.project_attributes || '{}', + + // QR code information + qr_code_id: eventData.qr_code_id || '', + qr_code_name: eventData.qr_code_name || '', + qr_code_attributes: eventData.qr_code_attributes || '{}', + + // Visitor information + visitor_id: eventData.visitor_id || uuid(), + session_id: eventData.session_id || uuid(), + ip_address: eventData.ip_address || req.headers.get('x-forwarded-for')?.toString() || '', + country: eventData.country || '', + city: eventData.city || '', + device_type: eventData.device_type || '', + browser: eventData.browser || '', + os: eventData.os || '', + user_agent: eventData.user_agent || req.headers.get('user-agent')?.toString() || '', + + // Referrer information + referrer: eventData.referrer || req.headers.get('referer')?.toString() || '', + utm_source: eventData.utm_source || '', + utm_medium: eventData.utm_medium || '', + utm_campaign: eventData.utm_campaign || '', + + // Interaction information + time_spent_sec: eventData.time_spent_sec || 0, + is_bounce: eventData.is_bounce !== undefined ? eventData.is_bounce : true, + is_qr_scan: eventData.is_qr_scan !== undefined ? eventData.is_qr_scan : false, + conversion_type: eventData.conversion_type || '', + conversion_value: eventData.conversion_value || 0, + }; + + // Insert event into ClickHouse + await clickhouse.insert({ + table: 'events', + values: [event], + format: 'JSONEachRow', + }); + + // Return success response + return NextResponse.json({ + success: true, + message: 'Event tracked successfully', + event_id: event.event_id + }, { status: 201 }); + + } catch (error) { + console.error('Error tracking event:', error); + return NextResponse.json( + { error: 'Failed to track event', details: (error as Error).message }, + { status: 500 } + ); + } +} \ No newline at end of file