Fix Mixpanel analytics review findings FRE-5281

P0: Fix validation bypass - validated properties now override raw properties
P1: Add unit tests for shared-analytics package (3 test files)
P1: Refactor spamshield to use shared-analytics, deprecate duplicate
P2: Normalize phone numbers to E.164 before hashing
P2: Add graceful error handling for missing env vars in config
P3: Add singleton pattern to MixpanelService
P3: Include timestamp in validated properties schema
This commit is contained in:
2026-05-17 15:37:21 -04:00
parent 986941e201
commit 06ca3ec0cf
10 changed files with 494 additions and 32 deletions

View File

@@ -2,23 +2,53 @@ import { z } from 'zod';
// Environment variables for analytics
const envSchema = z.object({
MIXPANEL_TOKEN: z.string(),
MIXPANEL_TOKEN: z.string().min(1, 'MIXPANEL_TOKEN is required for analytics'),
MIXPANEL_API_SECRET: z.string().optional(),
GA4_MEASUREMENT_ID: z.string(),
GA4_MEASUREMENT_ID: z.string().min(1, 'GA4_MEASUREMENT_ID is required for analytics'),
GA4_API_SECRET: z.string().optional(),
STRIPE_WEBHOOK_SECRET: z.string(),
STRIPE_WEBHOOK_SECRET: z.string().optional(),
ANALYTICS_ENV: z.enum(['development', 'production', 'staging']).default('development'),
});
function getEnvValue(key: string): string | undefined {
const value = process.env[key];
if (!value) {
return undefined;
}
return value;
}
const rawEnv = {
MIXPANEL_TOKEN: getEnvValue('MIXPANEL_TOKEN'),
MIXPANEL_API_SECRET: getEnvValue('MIXPANEL_API_SECRET'),
GA4_MEASUREMENT_ID: getEnvValue('GA4_MEASUREMENT_ID'),
GA4_API_SECRET: getEnvValue('GA4_API_SECRET'),
STRIPE_WEBHOOK_SECRET: getEnvValue('STRIPE_WEBHOOK_SECRET'),
ANALYTICS_ENV: getEnvValue('ANALYTICS_ENV'),
};
const missingRequired: string[] = [];
if (!rawEnv.MIXPANEL_TOKEN) missingRequired.push('MIXPANEL_TOKEN');
if (!rawEnv.GA4_MEASUREMENT_ID) missingRequired.push('GA4_MEASUREMENT_ID');
if (missingRequired.length > 0) {
console.warn(
`[Analytics] Missing required environment variables: ${missingRequired.join(', ')}. ` +
`Analytics will operate in degraded mode. Set these in your .env file.`
);
}
export const analyticsEnv = envSchema.parse({
MIXPANEL_TOKEN: process.env.MIXPANEL_TOKEN,
MIXPANEL_API_SECRET: process.env.MIXPANEL_API_SECRET,
GA4_MEASUREMENT_ID: process.env.GA4_MEASUREMENT_ID,
GA4_API_SECRET: process.env.GA4_API_SECRET,
STRIPE_WEBHOOK_SECRET: process.env.STRIPE_WEBHOOK_SECRET,
ANALYTICS_ENV: process.env.ANALYTICS_ENV,
MIXPANEL_TOKEN: rawEnv.MIXPANEL_TOKEN || '__MISSING__',
MIXPANEL_API_SECRET: rawEnv.MIXPANEL_API_SECRET,
GA4_MEASUREMENT_ID: rawEnv.GA4_MEASUREMENT_ID || '__MISSING__',
GA4_API_SECRET: rawEnv.GA4_API_SECRET,
STRIPE_WEBHOOK_SECRET: rawEnv.STRIPE_WEBHOOK_SECRET,
ANALYTICS_ENV: rawEnv.ANALYTICS_ENV,
});
export const isAnalyticsConfigured = !missingRequired.length;
// Event taxonomy
export enum EventType {
// User events
@@ -27,13 +57,13 @@ export enum EventType {
USER_LOGGED_OUT = 'user_logged_out',
USER_UPGRADED = 'user_upgraded',
USER_DOWNGRADED = 'user_downgraded',
// Subscription events
SUBSCRIPTION_CREATED = 'subscription_created',
SUBSCRIPTION_UPDATED = 'subscription_updated',
SUBSCRIPTION_CANCELLED = 'subscription_cancelled',
SUBSCRIPTION_RENEWED = 'subscription_renewed',
// DarkWatch events
DARK_WEB_SCAN_STARTED = 'dark_web_scan_started',
DARK_WEB_SCAN_COMPLETED = 'dark_web_scan_completed',
@@ -41,20 +71,20 @@ export enum EventType {
EXPOSURE_RESOLVED = 'exposure_resolved',
WATCHLIST_ITEM_ADDED = 'watchlist_item_added',
WATCHLIST_ITEM_REMOVED = 'watchlist_item_removed',
// VoicePrint events
VOICE_ENROLLED = 'voice_enrolled',
VOICE_ANALYZED = 'voice_analyzed',
VOICE_MATCH_FOUND = 'voice_match_found',
SYNTHETIC_VOICE_DETECTED = 'synthetic_voice_detected',
// SpamShield events
CALL_ANALYZED = 'call_analyzed',
SMS_ANALYZED = 'sms_analyzed',
SPAM_BLOCKED = 'spam_blocked',
SPAM_FLAGGED = 'spam_flagged',
SPAM_FEEDBACK_SUBMITTED = 'spam_feedback_submitted',
// KPI events
MRR_UPDATED = 'mrr_updated',
CONVERSION_OCCURRED = 'conversion_occurred',
@@ -63,15 +93,15 @@ export enum EventType {
REFERRAL_CONVERTED = 'referral_converted',
}
// Event properties schema
// Event properties schema - accepts common properties and allows extension
export const eventPropertiesSchema = z.object({
userId: z.string().optional(),
sessionId: z.string().optional(),
timestamp: z.date().optional(),
timestamp: z.union([z.date(), z.string().datetime()]).optional(),
platform: z.enum(['web', 'mobile', 'desktop', 'api']).optional(),
version: z.string().optional(),
environment: z.string().optional(),
});
}).passthrough();
// KPI definitions
export const kpiDefinitions = {