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

@@ -12,6 +12,7 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@shieldsai/shared-analytics": "workspace:*",
"@shieldai/db": "workspace:*",
"@shieldai/types": "workspace:*",
"@shieldai/correlation": "workspace:*",

View File

@@ -1,3 +1,4 @@
import { mixpanelService, EventType } from '@shieldsai/shared-analytics';
import { FieldEncryptionService } from '@shieldai/db';
export interface SpamBlockedEvent {
@@ -30,6 +31,14 @@ const DEFAULT_CONFIG: Required<MixpanelConfig> = {
enableLogging: true,
};
/**
* SpamShield analytics adapter.
* Delegates to the shared MixpanelService for consistent event tracking
* across the ShieldAI platform, while maintaining spam-specific interfaces.
*
* @deprecated Use {@link @shieldsai/shared-analytics#MixpanelService} directly
* for new analytics code. This wrapper maintains backward compatibility.
*/
export class MixpanelService {
private readonly config: Required<MixpanelConfig>;
private readonly events: MixpanelEventProperties[] = [];
@@ -58,15 +67,24 @@ export class MixpanelService {
);
}
const response = await this.track('spam_blocked', properties);
await mixpanelService.track(EventType.SPAM_BLOCKED, properties.phoneNumberHash, {
decision: event.decision,
confidence: event.confidence,
ruleMatches: event.ruleMatches,
timestamp: event.timestamp,
});
return {
...properties,
...response,
};
return properties;
}
async track(eventName: string, properties: Record<string, any>): Promise<Record<string, any>> {
const mpEvent = Object.values(EventType).find(e => e === eventName) as EventType | undefined;
if (mpEvent) {
await mixpanelService.track(mpEvent, properties.phoneNumberHash || 'anonymous', properties);
return { status: 200 };
}
const url = `https://${this.config.apiHost}/track`;
const payload = {