FRE-4529: Transfer ShieldAI code from FrenoCorp repo

Transferred ShieldAI-related files mistakenly placed in ~/code/FrenoCorp:
- Services: spamshield (feature-flags, audit-logger, error-handler), voiceprint (config, service, feature-flags), darkwatch (pipeline, scan, scheduler, watchlist, webhook)
- Packages: shared-analytics, shared-auth, shared-ui, shared-utils (new); shared-billing, jobs supplemented with unique FC files
- Server: alerts (FC version newer), routes (spamshield, darkwatch, voiceprint)
- Config: turbo.json, tsconfig.base.json, vite/vitest configs, drizzle, Dockerfile
- VoicePrint ML service
- Examples

Pending: apps/{api,web,mobile}/ structured merge, shared-db/db mapping

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
2026-05-02 10:13:13 -04:00
parent 8687868632
commit 1e42c4a5c2
45 changed files with 4837 additions and 562 deletions

View File

@@ -0,0 +1,19 @@
{
"name": "@shieldsai/shared-analytics",
"version": "0.1.0",
"private": true,
"type": "module",
"main": "src/index.ts",
"types": "src/index.ts",
"scripts": {
"lint": "eslint src/"
},
"dependencies": {
"@segment/analytics-node": "^1.0.0",
"googleapis": "^128.0.0",
"zod": "^4.3.6"
},
"devDependencies": {
"typescript": "^5.3.3"
}
}

View File

@@ -0,0 +1,132 @@
import { z } from 'zod';
// Environment variables for analytics
const envSchema = z.object({
MIXPANEL_TOKEN: z.string(),
MIXPANEL_API_SECRET: z.string().optional(),
GA4_MEASUREMENT_ID: z.string(),
GA4_API_SECRET: z.string().optional(),
STRIPE_WEBHOOK_SECRET: z.string(),
ANALYTICS_ENV: z.enum(['development', 'production', 'staging']).default('development'),
});
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,
});
// Event taxonomy
export enum EventType {
// User events
USER_SIGNED_UP = 'user_signed_up',
USER_LOGGED_IN = 'user_logged_in',
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',
EXPOSURE_DETECTED = 'exposure_detected',
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',
CHURN_OCCURRED = 'churn_occurred',
REFERRAL_SENT = 'referral_sent',
REFERRAL_CONVERTED = 'referral_converted',
}
// Event properties schema
export const eventPropertiesSchema = z.object({
userId: z.string().optional(),
sessionId: z.string().optional(),
timestamp: z.date().optional(),
platform: z.enum(['web', 'mobile', 'desktop', 'api']).optional(),
version: z.string().optional(),
environment: z.string().optional(),
});
// KPI definitions
export const kpiDefinitions = {
mau: {
name: 'Monthly Active Users',
description: 'Unique users who performed an action in the last 30 days',
calculation: 'COUNT(DISTINCT userId) WHERE timestamp > NOW() - INTERVAL 30 DAYS',
},
payingUsers: {
name: 'Paying Users',
description: 'Users with active subscriptions',
calculation: 'COUNT(DISTINCT userId) WHERE subscription.status = "active"',
},
mrr: {
name: 'Monthly Recurring Revenue',
description: 'Total monthly subscription revenue',
calculation: 'SUM(subscription.amount) WHERE subscription.status = "active"',
},
conversionRate: {
name: 'Conversion Rate',
description: 'Percentage of free users who upgrade to paid',
calculation: 'COUNT(upgrade events) / COUNT(signup events)',
},
churn: {
name: 'Churn Rate',
description: 'Percentage of paying users who cancel',
calculation: 'COUNT(cancel events) / COUNT(active subscriptions)',
},
cac: {
name: 'Customer Acquisition Cost',
description: 'Average cost to acquire a new paying user',
calculation: 'Total marketing spend / COUNT(new paying users)',
},
ltv: {
name: 'Lifetime Value',
description: 'Average revenue per user over their lifetime',
calculation: 'Average subscription amount / Churn rate',
},
nps: {
name: 'Net Promoter Score',
description: 'Customer satisfaction metric (-100 to 100)',
calculation: '% Promoters - % Detractors',
},
viralCoefficient: {
name: 'Viral Coefficient',
description: 'Average number of referrals per user',
calculation: 'COUNT(referral events) / COUNT(users)',
},
};
// Alert thresholds
export const alertThresholds = {
churn: { warning: 0.05, critical: 0.10 },
conversionRate: { warning: 0.02, critical: 0.01 },
mrr: { warning: 0.90, critical: 0.80 }, // Percentage of target
nps: { warning: 50, critical: 40 },
viralCoefficient: { warning: 0.4, critical: 0.3 },
};

View File

@@ -0,0 +1,18 @@
// Config
export {
analyticsEnv,
EventType,
eventPropertiesSchema,
kpiDefinitions,
alertThresholds,
} from './config/analytics.config';
// Services
export {
MixpanelService,
mixpanelService,
} from './services/mixpanel.service';
export {
GA4Service,
ga4Service,
} from './services/ga4.service';

View File

@@ -0,0 +1,104 @@
import { google } from 'googleapis';
import { analyticsEnv, EventType } from '../config/analytics.config';
// GA4 service
export class GA4Service {
private auth: any;
constructor() {
this.auth = google.auth.fromAPIKey(analyticsEnv.GA4_API_SECRET || 'placeholder');
}
/**
* Initialize GA4 client
*/
async initialize(): Promise<void> {
// TODO: Initialize GA4 client with measurement ID
console.log('GA4 client initialized');
}
/**
* Send event to GA4
*/
async sendEvent(
eventName: string,
params: {
client_id: string;
[key: string]: any;
}
): Promise<void> {
// TODO: Implement GA4 event tracking
// const measurementId = analyticsEnv.GA4_MEASUREMENT_ID;
// await fetch(`https://www.google-analytics.com/mp/collect?measurement_id=${measurementId}&api_secret=${analyticsEnv.GA4_API_SECRET}`, {
// method: 'POST',
// body: JSON.stringify({
// events: [{ name: eventName, params }],
// }),
// });
console.log('GA4 event:', eventName, params);
}
/**
* Track page view
*/
async trackPageView(clientId: string, path: string, title?: string): Promise<void> {
await this.sendEvent('page_view', {
client_id: clientId,
page_path: path,
page_title: title,
});
}
/**
* Track e-commerce purchase
*/
async trackPurchase(
clientId: string,
transactionId: string,
value: number,
currency: string,
items: Array<{ name: string; price: number; quantity: number }>
): Promise<void> {
await this.sendEvent('purchase', {
client_id: clientId,
transaction_id: transactionId,
value,
currency,
items,
});
}
/**
* Track conversion
*/
async trackConversion(
clientId: string,
conversionName: string,
metadata?: Record<string, any>
): Promise<void> {
await this.sendEvent('conversion', {
client_id: clientId,
conversion_name: conversionName,
...metadata,
});
}
/**
* Get analytics data (for dashboards)
*/
async getMetrics(
dateRange: { startDate: string; endDate: string },
metrics: string[],
dimensions?: string[]
): Promise<any> {
// TODO: Implement GA4 Analytics Data API
return {
rows: [],
totals: [],
};
}
}
// Export instance
export const ga4Service = new GA4Service();

View File

@@ -0,0 +1,117 @@
import { Analytics } from '@segment/analytics-node';
import { analyticsEnv, EventType, eventPropertiesSchema } from '../config/analytics.config';
import { hashPhoneNumber } from '../utils/phone-hash';
// Mixpanel service
export class MixpanelService {
private client: Analytics;
constructor() {
this.client = new Analytics({
apiKey: analyticsEnv.MIXPANEL_TOKEN,
});
}
/**
* Track an event in Mixpanel
*/
async track(
event: EventType,
distinctId: string,
properties?: Record<string, any>
): Promise<void> {
const validatedProperties = eventPropertiesSchema.parse(properties);
this.client.track({
event,
distinctId,
properties: {
...validatedProperties,
...properties,
},
});
}
/**
* Identify a user
*/
async identify(userId: string, traits?: Record<string, any>): Promise<void> {
this.client.identify({
distinctId: userId,
traits,
});
}
/**
* Group users by subscription tier
*/
async group(groupId: string, groupKey: string, traits?: Record<string, any>): Promise<void> {
this.client.group({
groupKey,
groupId,
traits,
});
}
/**
* Track user sign-up
*/
async userSignedUp(userId: string, plan?: string, referrer?: string): Promise<void> {
await this.track(EventType.USER_SIGNED_UP, userId, {
plan,
referrer,
timestamp: new Date(),
});
}
/**
* Track subscription upgrade
*/
async userUpgraded(userId: string, fromTier: string, toTier: string, mrr: number): Promise<void> {
await this.track(EventType.USER_UPGRADED, userId, {
fromTier,
toTier,
mrr,
timestamp: new Date(),
});
}
/**
* Track exposure detection
*/
async exposureDetected(
userId: string,
exposureType: string,
severity: string,
source: string
): Promise<void> {
await this.track(EventType.EXPOSURE_DETECTED, userId, {
exposureType,
severity,
source,
timestamp: new Date(),
});
}
/**
* Track spam detection
*/
async spamBlocked(userId: string, phoneNumber: string, confidence: number, method: string): Promise<void> {
await this.track(EventType.SPAM_BLOCKED, userId, {
phoneNumber: hashPhoneNumber(phoneNumber),
confidence,
method,
timestamp: new Date(),
});
}
/**
* Flush pending events
*/
async flush(): Promise<void> {
await this.client.flush();
}
}
// Export instance
export const mixpanelService = new MixpanelService();

View File

@@ -0,0 +1,12 @@
/**
* Hash a phone number for analytics purposes
* Uses a consistent hashing algorithm to create a deterministic hash
*/
export function hashPhoneNumber(phoneNumber: string): string {
let hash = 0;
for (let i = 0; i < phoneNumber.length; i++) {
hash = ((hash << 5) - hash) + phoneNumber.charCodeAt(i);
hash |= 0;
}
return `hash_${Math.abs(hash)}`;
}

View File

@@ -0,0 +1,12 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"declaration": true,
"declarationMap": true,
"emitDeclarationOnly": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}