import type { AnalyticsEvent, PrivacyConfig, ScreenEvent, UserTraits } from '../types'; import { deepClone, setNestedValue } from '../utils'; const DEFAULT_MASK = '***'; // Common PII field names to auto-detect const KNOWN_PII_FIELDS = ['email', 'phone', 'password', 'ssn', 'credit_card', 'card_number']; /** * Handles GDPR/privacy concerns: * - opt-out / opt-in toggle * - masks configured PII fields before events leave the device */ export class PrivacyManager { private config: PrivacyConfig; private optedOut: boolean; private maskFields: string[]; constructor(config: PrivacyConfig = {}) { this.config = config; this.optedOut = config.defaultOptOut ?? false; this.maskFields = [ ...KNOWN_PII_FIELDS, ...(config.maskFields ?? []), ]; } optOut(): void { this.optedOut = true; } optIn(): void { this.optedOut = false; } isOptedIn(): boolean { return !this.optedOut; } sanitizeEvent(event: AnalyticsEvent): AnalyticsEvent { const cloned = deepClone(event); if (cloned.properties) { cloned.properties = this.maskObject(cloned.properties); } return cloned; } sanitizeScreenEvent(event: ScreenEvent): ScreenEvent { const cloned = deepClone(event); if (cloned.properties) { cloned.properties = this.maskObject(cloned.properties); } return cloned; } sanitizeTraits(traits: UserTraits): UserTraits { return this.maskObject(deepClone(traits)) as UserTraits; } private maskObject(obj: Record): Record { const maskValue = this.config.maskValue ?? DEFAULT_MASK; const walk = (target: Record, path = ''): Record => { const result: Record = {}; for (const key of Object.keys(target)) { const fullPath = path ? `${path}.${key}` : key; const value = target[key]; const shouldMask = this.maskFields.some( (field) => key.toLowerCase() === field.toLowerCase() || fullPath.toLowerCase() === field.toLowerCase() ); if (shouldMask) { result[key] = maskValue; } else if (value !== null && typeof value === 'object' && !Array.isArray(value)) { result[key] = walk(value, fullPath); } else { result[key] = value; } } return result; }; return walk(obj); } }