/** * Analytics Routes * Track events and conversions for marketing analytics */ import express from 'express' import { asyncHandler, ValidationError } from '../middleware/errorHandler.mjs' import { respond } from '../utils/response.mjs' import { db, Collections } from '../services/database.mjs' import { log } from '../middleware/logger.mjs' const router = express.Router() // Whitelist of allowed event types const ALLOWED_EVENT_TYPES = [ 'page_view', 'signup', 'trial_start', 'purchase', 'email_connected', 'onboarding_step', 'provider_connected', 'demo_used', 'suggested_rules_generated', 'rule_created', 'rules_applied', 'limit_reached', 'upgrade_clicked', 'referral_shared', 'sort_completed', 'account_deleted', ] // Fields that should never be stored (PII) const PII_FIELDS = ['email', 'password', 'emailContent', 'emailBody', 'subject', 'from', 'to', 'snippet', 'content'] function stripPII(metadata) { if (!metadata || typeof metadata !== 'object') return {} const cleaned = {} for (const [key, value] of Object.entries(metadata)) { if (PII_FIELDS.includes(key.toLowerCase())) { continue // Skip PII fields } if (typeof value === 'string' && value.includes('@')) { // Skip if looks like email continue } if (typeof value === 'object' && value !== null) { cleaned[key] = stripPII(value) } else { cleaned[key] = value } } return cleaned } /** * POST /api/analytics/track * Track analytics events (page views, conversions, etc.) * * Body: * { * type: 'page_view' | 'signup' | 'trial_start' | 'purchase' | 'email_connected', * userId?: string, * tracking?: { * utm_source?: string, * utm_medium?: string, * utm_campaign?: string, * utm_term?: string, * utm_content?: string, * }, * metadata?: Record, * timestamp?: string, * page?: string, * referrer?: string, * } */ router.post('/track', asyncHandler(async (req, res) => { const { type, userId, tracking, metadata, timestamp, page, referrer, sessionId, } = req.body // Validate event type if (!type || !ALLOWED_EVENT_TYPES.includes(type)) { throw new ValidationError(`Invalid event type. Allowed: ${ALLOWED_EVENT_TYPES.join(', ')}`) } // Strip PII from metadata const cleanedMetadata = stripPII(metadata || {}) // Prepare event data const eventData = { userId: userId || null, eventType: type, metadataJson: JSON.stringify(cleanedMetadata), timestamp: timestamp || new Date().toISOString(), sessionId: sessionId || null, } // Store in database try { await db.create(Collections.ANALYTICS_EVENTS, eventData) log.info(`Analytics event tracked: ${type}`, { userId, sessionId }) } catch (err) { log.warn('Failed to store analytics event', { error: err.message, type }) // Don't fail the request if analytics storage fails } // Log in development if (process.env.NODE_ENV === 'development') { console.log('📊 Analytics Event:', { type, userId, sessionId, metadata: cleanedMetadata, }) } // Return success (client doesn't need to wait) respond.success(res, { received: true }) })) export default router