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:
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user