Files
Emailsorter/server/routes/analytics.mjs
ANDJ 6da8ce1cbd huhuih
hzgjuigik
2026-01-27 21:06:48 +01:00

132 lines
3.2 KiB
JavaScript

/**
* 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<string, any>,
* 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