CREATE TABLE app_event_engagement_stats ( id SERIAL PRIMARY KEY, event_key VARCHAR(255) NOT NULL, action_type VARCHAR(50) NOT NULL, -- 'click' | 'outbound_click' | 'calendar_sync' session_id VARCHAR(255), source VARCHAR(255), path VARCHAR(500), is_authenticated BOOLEAN DEFAULT FALSE, recorded_at TIMESTAMP DEFAULT NOW())CREATE INDEX idx_event_engagement_event_key ON app_event_engagement_stats(event_key)CREATE INDEX idx_event_engagement_action_type ON app_event_engagement_stats(action_type)CREATE INDEX idx_event_engagement_recorded_at ON app_event_engagement_stats(recorded_at)
// features/events/engagement/client-tracking.tsimport { trackGenrePreference } from '@/features/events/engagement/client-tracking'// User clicks genre tag or expresses preferencetrackGenrePreference('Jazz')
Preferences are stored per user email:
CREATE TABLE app_user_genre_preferences ( id SERIAL PRIMARY KEY, user_email VARCHAR(255) NOT NULL, genre VARCHAR(100) NOT NULL, preference_count INTEGER DEFAULT 1, last_updated TIMESTAMP DEFAULT NOW(), UNIQUE(user_email, genre))
Genre preference tracking requires user authentication. Anonymous visitors cannot save preferences.
interface EngagementSummary { clickCount: number // Total event opens dedupedViewCount: number // Unique views (deduped by session) outboundClickCount: number // External link clicks calendarSyncCount: number // Calendar downloads uniqueSessionCount: number // Distinct sessions uniqueViewSessionCount: number // Sessions with views uniqueOutboundSessionCount: number // Sessions with outbound clicks uniqueCalendarSessionCount: number // Sessions with calendar syncs outboundSessionRate: number // % sessions with outbound click calendarSessionRate: number // % sessions with calendar sync outboundInteractionRate: number // Outbound / views calendarInteractionRate: number // Calendar / views}
interface EventEngagementMetrics { eventKey: string eventName: string clickCount: number dedupedViewCount: number outboundClickCount: number calendarSyncCount: number uniqueSessionCount: number conversionRate: number // Calendar / views}
View engagement projection query
// lib/platform/postgres/event-engagement-repository.tsexport async function getCalendarSyncCountsByEventKeys( eventKeys: string[]): Promise<Map<string, number>> { const query = ` SELECT event_key, COUNT(*) as sync_count FROM app_event_engagement_stats WHERE action_type = 'calendar_sync' AND event_key = ANY($1) GROUP BY event_key ` const result = await db.query(query, [eventKeys]) return new Map( result.rows.map(row => [row.event_key, parseInt(row.sync_count)]) )}
Partner stats are derived from engagement tracking:
// GET /api/partner-stats/[activationId]?token=xxxinterface PartnerStatsResponse { activation: { id: string eventKey: string eventName: string placementType: 'spotlight' | 'promoted' startedAt: string endedAt: string | null } metrics: { viewCount: number uniqueViewers: number outboundClickCount: number calendarSyncCount: number outboundRate: number // % of views that clicked outbound saveRate: number // % of views that saved to calendar }}
Partner stats can be exported as CSV using the format=csv query parameter.