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:
19
packages/shared-analytics/package.json
Normal file
19
packages/shared-analytics/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
132
packages/shared-analytics/src/config/analytics.config.ts
Normal file
132
packages/shared-analytics/src/config/analytics.config.ts
Normal 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 },
|
||||
};
|
||||
18
packages/shared-analytics/src/index.ts
Normal file
18
packages/shared-analytics/src/index.ts
Normal 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';
|
||||
104
packages/shared-analytics/src/services/ga4.service.ts
Normal file
104
packages/shared-analytics/src/services/ga4.service.ts
Normal 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();
|
||||
117
packages/shared-analytics/src/services/mixpanel.service.ts
Normal file
117
packages/shared-analytics/src/services/mixpanel.service.ts
Normal 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();
|
||||
12
packages/shared-analytics/src/utils/phone-hash.ts
Normal file
12
packages/shared-analytics/src/utils/phone-hash.ts
Normal 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)}`;
|
||||
}
|
||||
12
packages/shared-analytics/tsconfig.json
Normal file
12
packages/shared-analytics/tsconfig.json
Normal 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"]
|
||||
}
|
||||
Reference in New Issue
Block a user