feat: implement analytics service with PostHog integration
- Create AnalyticsService singleton for centralized analytics tracking - Implement ConsentManager for privacy-first analytics consent - Add comprehensive event builders and sanitizers - Define analytics event types and interfaces - Support opt-in analytics with local storage persistence - Include event queue and batch processing - Add privacy-focused configuration (no session recording, no autocapture)
This commit is contained in:
137
src/lib/analytics/consent.ts
Normal file
137
src/lib/analytics/consent.ts
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
import type { AnalyticsSettings } from './types';
|
||||||
|
|
||||||
|
const ANALYTICS_STORAGE_KEY = 'claudia-analytics-settings';
|
||||||
|
|
||||||
|
export class ConsentManager {
|
||||||
|
private static instance: ConsentManager;
|
||||||
|
private settings: AnalyticsSettings | null = null;
|
||||||
|
|
||||||
|
private constructor() {}
|
||||||
|
|
||||||
|
static getInstance(): ConsentManager {
|
||||||
|
if (!ConsentManager.instance) {
|
||||||
|
ConsentManager.instance = new ConsentManager();
|
||||||
|
}
|
||||||
|
return ConsentManager.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
async initialize(): Promise<AnalyticsSettings> {
|
||||||
|
try {
|
||||||
|
// Try to load from localStorage first
|
||||||
|
const stored = localStorage.getItem(ANALYTICS_STORAGE_KEY);
|
||||||
|
if (stored) {
|
||||||
|
this.settings = JSON.parse(stored);
|
||||||
|
} else {
|
||||||
|
// Initialize with default settings
|
||||||
|
this.settings = {
|
||||||
|
enabled: false,
|
||||||
|
hasConsented: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate anonymous user ID if not exists
|
||||||
|
if (this.settings && !this.settings.userId) {
|
||||||
|
this.settings.userId = this.generateAnonymousId();
|
||||||
|
await this.saveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate session ID
|
||||||
|
if (this.settings) {
|
||||||
|
this.settings.sessionId = this.generateSessionId();
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.settings || {
|
||||||
|
enabled: false,
|
||||||
|
hasConsented: false,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to initialize consent manager:', error);
|
||||||
|
// Return default settings on error
|
||||||
|
return {
|
||||||
|
enabled: false,
|
||||||
|
hasConsented: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async grantConsent(): Promise<void> {
|
||||||
|
if (!this.settings) {
|
||||||
|
await this.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.settings!.enabled = true;
|
||||||
|
this.settings!.hasConsented = true;
|
||||||
|
this.settings!.consentDate = new Date().toISOString();
|
||||||
|
|
||||||
|
await this.saveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
async revokeConsent(): Promise<void> {
|
||||||
|
if (!this.settings) {
|
||||||
|
await this.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.settings!.enabled = false;
|
||||||
|
|
||||||
|
await this.saveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteAllData(): Promise<void> {
|
||||||
|
// Clear local storage
|
||||||
|
localStorage.removeItem(ANALYTICS_STORAGE_KEY);
|
||||||
|
|
||||||
|
// Reset settings with new anonymous ID
|
||||||
|
this.settings = {
|
||||||
|
enabled: false,
|
||||||
|
hasConsented: false,
|
||||||
|
userId: this.generateAnonymousId(),
|
||||||
|
sessionId: this.generateSessionId(),
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.saveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
getSettings(): AnalyticsSettings | null {
|
||||||
|
return this.settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasConsented(): boolean {
|
||||||
|
return this.settings?.hasConsented || false;
|
||||||
|
}
|
||||||
|
|
||||||
|
isEnabled(): boolean {
|
||||||
|
return this.settings?.enabled || false;
|
||||||
|
}
|
||||||
|
|
||||||
|
getUserId(): string {
|
||||||
|
return this.settings?.userId || this.generateAnonymousId();
|
||||||
|
}
|
||||||
|
|
||||||
|
getSessionId(): string {
|
||||||
|
return this.settings?.sessionId || this.generateSessionId();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async saveSettings(): Promise<void> {
|
||||||
|
if (!this.settings) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
localStorage.setItem(ANALYTICS_STORAGE_KEY, JSON.stringify(this.settings));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to save analytics settings:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateAnonymousId(): string {
|
||||||
|
// Generate a UUID v4
|
||||||
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
||||||
|
const r = (Math.random() * 16) | 0;
|
||||||
|
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
||||||
|
return v.toString(16);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateSessionId(): string {
|
||||||
|
// Simple session ID based on timestamp and random value
|
||||||
|
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||||
|
}
|
||||||
|
}
|
150
src/lib/analytics/events.ts
Normal file
150
src/lib/analytics/events.ts
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
import type {
|
||||||
|
EventName,
|
||||||
|
FeatureUsageProperties,
|
||||||
|
ErrorProperties,
|
||||||
|
SessionProperties,
|
||||||
|
ModelProperties,
|
||||||
|
AgentProperties,
|
||||||
|
MCPProperties,
|
||||||
|
SlashCommandProperties,
|
||||||
|
PerformanceMetrics
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
export const ANALYTICS_EVENTS = {
|
||||||
|
// Session events
|
||||||
|
SESSION_CREATED: 'session_created' as EventName,
|
||||||
|
SESSION_COMPLETED: 'session_completed' as EventName,
|
||||||
|
SESSION_RESUMED: 'session_resumed' as EventName,
|
||||||
|
|
||||||
|
// Feature usage events
|
||||||
|
FEATURE_USED: 'feature_used' as EventName,
|
||||||
|
MODEL_SELECTED: 'model_selected' as EventName,
|
||||||
|
TAB_CREATED: 'tab_created' as EventName,
|
||||||
|
TAB_CLOSED: 'tab_closed' as EventName,
|
||||||
|
FILE_OPENED: 'file_opened' as EventName,
|
||||||
|
FILE_EDITED: 'file_edited' as EventName,
|
||||||
|
FILE_SAVED: 'file_saved' as EventName,
|
||||||
|
AGENT_EXECUTED: 'agent_executed' as EventName,
|
||||||
|
MCP_SERVER_CONNECTED: 'mcp_server_connected' as EventName,
|
||||||
|
MCP_SERVER_DISCONNECTED: 'mcp_server_disconnected' as EventName,
|
||||||
|
SLASH_COMMAND_USED: 'slash_command_used' as EventName,
|
||||||
|
|
||||||
|
// Settings and system events
|
||||||
|
SETTINGS_CHANGED: 'settings_changed' as EventName,
|
||||||
|
APP_STARTED: 'app_started' as EventName,
|
||||||
|
APP_CLOSED: 'app_closed' as EventName,
|
||||||
|
|
||||||
|
// Error events
|
||||||
|
ERROR_OCCURRED: 'error_occurred' as EventName,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// Event property builders - help ensure consistent event structure
|
||||||
|
export const eventBuilders = {
|
||||||
|
session: (props: SessionProperties) => ({
|
||||||
|
event: ANALYTICS_EVENTS.SESSION_CREATED,
|
||||||
|
properties: {
|
||||||
|
category: 'session',
|
||||||
|
...props,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
feature: (feature: string, subfeature?: string, metadata?: Record<string, any>) => ({
|
||||||
|
event: ANALYTICS_EVENTS.FEATURE_USED,
|
||||||
|
properties: {
|
||||||
|
category: 'feature',
|
||||||
|
feature,
|
||||||
|
subfeature,
|
||||||
|
...metadata,
|
||||||
|
} as FeatureUsageProperties,
|
||||||
|
}),
|
||||||
|
|
||||||
|
error: (errorType: string, errorCode?: string, context?: string) => ({
|
||||||
|
event: ANALYTICS_EVENTS.ERROR_OCCURRED,
|
||||||
|
properties: {
|
||||||
|
category: 'error',
|
||||||
|
error_type: errorType,
|
||||||
|
error_code: errorCode,
|
||||||
|
context,
|
||||||
|
} as ErrorProperties,
|
||||||
|
}),
|
||||||
|
|
||||||
|
model: (newModel: string, previousModel?: string, source?: string) => ({
|
||||||
|
event: ANALYTICS_EVENTS.MODEL_SELECTED,
|
||||||
|
properties: {
|
||||||
|
category: 'model',
|
||||||
|
new_model: newModel,
|
||||||
|
previous_model: previousModel,
|
||||||
|
source,
|
||||||
|
} as ModelProperties,
|
||||||
|
}),
|
||||||
|
|
||||||
|
agent: (agentType: string, success: boolean, agentName?: string, durationMs?: number) => ({
|
||||||
|
event: ANALYTICS_EVENTS.AGENT_EXECUTED,
|
||||||
|
properties: {
|
||||||
|
category: 'agent',
|
||||||
|
agent_type: agentType,
|
||||||
|
agent_name: agentName,
|
||||||
|
success,
|
||||||
|
duration_ms: durationMs,
|
||||||
|
} as AgentProperties,
|
||||||
|
}),
|
||||||
|
|
||||||
|
mcp: (serverName: string, success: boolean, serverType?: string) => ({
|
||||||
|
event: ANALYTICS_EVENTS.MCP_SERVER_CONNECTED,
|
||||||
|
properties: {
|
||||||
|
category: 'mcp',
|
||||||
|
server_name: serverName,
|
||||||
|
server_type: serverType,
|
||||||
|
success,
|
||||||
|
} as MCPProperties,
|
||||||
|
}),
|
||||||
|
|
||||||
|
slashCommand: (command: string, success: boolean) => ({
|
||||||
|
event: ANALYTICS_EVENTS.SLASH_COMMAND_USED,
|
||||||
|
properties: {
|
||||||
|
category: 'slash_command',
|
||||||
|
command,
|
||||||
|
success,
|
||||||
|
} as SlashCommandProperties,
|
||||||
|
}),
|
||||||
|
|
||||||
|
performance: (metrics: PerformanceMetrics) => ({
|
||||||
|
event: ANALYTICS_EVENTS.FEATURE_USED,
|
||||||
|
properties: {
|
||||||
|
category: 'performance',
|
||||||
|
feature: 'system_metrics',
|
||||||
|
...metrics,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sanitization helpers to remove PII
|
||||||
|
export const sanitizers = {
|
||||||
|
// Remove file paths, keeping only extension
|
||||||
|
sanitizeFilePath: (path: string): string => {
|
||||||
|
const ext = path.split('.').pop();
|
||||||
|
return ext ? `*.${ext}` : 'unknown';
|
||||||
|
},
|
||||||
|
|
||||||
|
// Remove project names and paths
|
||||||
|
sanitizeProjectPath: (_path: string): string => {
|
||||||
|
return 'project';
|
||||||
|
},
|
||||||
|
|
||||||
|
// Sanitize error messages that might contain sensitive info
|
||||||
|
sanitizeErrorMessage: (message: string): string => {
|
||||||
|
// Remove file paths
|
||||||
|
message = message.replace(/\/[\w\-\/\.]+/g, '/***');
|
||||||
|
// Remove potential API keys or tokens
|
||||||
|
message = message.replace(/[a-zA-Z0-9]{20,}/g, '***');
|
||||||
|
// Remove email addresses
|
||||||
|
message = message.replace(/[\w\.-]+@[\w\.-]+\.\w+/g, '***@***.***');
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Sanitize agent names that might contain user info
|
||||||
|
sanitizeAgentName: (name: string): string => {
|
||||||
|
// Only keep the type, remove custom names
|
||||||
|
return name.split('-')[0] || 'custom';
|
||||||
|
},
|
||||||
|
};
|
250
src/lib/analytics/index.ts
Normal file
250
src/lib/analytics/index.ts
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
import posthog from 'posthog-js';
|
||||||
|
import { ConsentManager } from './consent';
|
||||||
|
import { sanitizers } from './events';
|
||||||
|
import type {
|
||||||
|
AnalyticsConfig,
|
||||||
|
AnalyticsEvent,
|
||||||
|
EventName,
|
||||||
|
AnalyticsSettings
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
export * from './types';
|
||||||
|
export * from './events';
|
||||||
|
export { ConsentManager } from './consent';
|
||||||
|
|
||||||
|
class AnalyticsService {
|
||||||
|
private static instance: AnalyticsService;
|
||||||
|
private initialized = false;
|
||||||
|
private consentManager: ConsentManager;
|
||||||
|
private config: AnalyticsConfig;
|
||||||
|
private eventQueue: AnalyticsEvent[] = [];
|
||||||
|
private flushInterval: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
this.consentManager = ConsentManager.getInstance();
|
||||||
|
|
||||||
|
// Default configuration - pulled from Vite environment variables
|
||||||
|
this.config = {
|
||||||
|
apiKey: import.meta.env.VITE_PUBLIC_POSTHOG_KEY || 'phc_YOUR_PROJECT_API_KEY',
|
||||||
|
apiHost: import.meta.env.VITE_PUBLIC_POSTHOG_HOST || 'https://app.posthog.com',
|
||||||
|
persistence: 'localStorage',
|
||||||
|
autocapture: false, // We'll manually track events
|
||||||
|
disable_session_recording: true, // Privacy first
|
||||||
|
opt_out_capturing_by_default: true, // Require explicit opt-in
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static getInstance(): AnalyticsService {
|
||||||
|
if (!AnalyticsService.instance) {
|
||||||
|
AnalyticsService.instance = new AnalyticsService();
|
||||||
|
}
|
||||||
|
return AnalyticsService.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
async initialize(): Promise<void> {
|
||||||
|
if (this.initialized) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Initialize consent manager
|
||||||
|
const settings = await this.consentManager.initialize();
|
||||||
|
|
||||||
|
// Only initialize PostHog if user has consented
|
||||||
|
if (settings.hasConsented && settings.enabled) {
|
||||||
|
this.initializePostHog(settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start event queue flush interval
|
||||||
|
this.startFlushInterval();
|
||||||
|
|
||||||
|
this.initialized = true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to initialize analytics:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private initializePostHog(settings: AnalyticsSettings): void {
|
||||||
|
try {
|
||||||
|
posthog.init(this.config.apiKey, {
|
||||||
|
api_host: this.config.apiHost,
|
||||||
|
defaults: '2025-05-24',
|
||||||
|
capture_exceptions: true,
|
||||||
|
debug: import.meta.env.MODE === 'development',
|
||||||
|
persistence: this.config.persistence,
|
||||||
|
autocapture: this.config.autocapture,
|
||||||
|
disable_session_recording: this.config.disable_session_recording,
|
||||||
|
opt_out_capturing_by_default: this.config.opt_out_capturing_by_default,
|
||||||
|
loaded: (ph) => {
|
||||||
|
// Set user properties
|
||||||
|
ph.identify(settings.userId, {
|
||||||
|
anonymous: true,
|
||||||
|
consent_date: settings.consentDate,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Opt in since user has consented
|
||||||
|
ph.opt_in_capturing();
|
||||||
|
|
||||||
|
if (this.config.loaded) {
|
||||||
|
this.config.loaded(ph);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to initialize PostHog:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async enable(): Promise<void> {
|
||||||
|
await this.consentManager.grantConsent();
|
||||||
|
const settings = this.consentManager.getSettings();
|
||||||
|
if (settings) {
|
||||||
|
this.initializePostHog(settings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async disable(): Promise<void> {
|
||||||
|
await this.consentManager.revokeConsent();
|
||||||
|
if (typeof posthog !== 'undefined' && posthog.opt_out_capturing) {
|
||||||
|
posthog.opt_out_capturing();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteAllData(): Promise<void> {
|
||||||
|
await this.consentManager.deleteAllData();
|
||||||
|
if (typeof posthog !== 'undefined' && posthog.reset) {
|
||||||
|
posthog.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
track(eventName: EventName | string, properties?: Record<string, any>): void {
|
||||||
|
// Check if analytics is enabled
|
||||||
|
if (!this.consentManager.isEnabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanitize properties to remove PII
|
||||||
|
const sanitizedProperties = this.sanitizeProperties(properties || {});
|
||||||
|
|
||||||
|
// Create event
|
||||||
|
const event: AnalyticsEvent = {
|
||||||
|
event: eventName,
|
||||||
|
properties: sanitizedProperties,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
sessionId: this.consentManager.getSessionId(),
|
||||||
|
userId: this.consentManager.getUserId(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add to queue
|
||||||
|
this.eventQueue.push(event);
|
||||||
|
|
||||||
|
// Send immediately if PostHog is initialized
|
||||||
|
if (typeof posthog !== 'undefined' && typeof posthog.capture === 'function') {
|
||||||
|
this.flushEvents();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
identify(traits?: Record<string, any>): void {
|
||||||
|
if (!this.consentManager.isEnabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = this.consentManager.getUserId();
|
||||||
|
const sanitizedTraits = this.sanitizeProperties(traits || {});
|
||||||
|
|
||||||
|
if (typeof posthog !== 'undefined' && posthog.identify) {
|
||||||
|
posthog.identify(userId, {
|
||||||
|
...sanitizedTraits,
|
||||||
|
anonymous: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sanitizeProperties(properties: Record<string, any>): Record<string, any> {
|
||||||
|
const sanitized: Record<string, any> = {};
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(properties)) {
|
||||||
|
// Skip null/undefined values
|
||||||
|
if (value == null) continue;
|
||||||
|
|
||||||
|
// Apply specific sanitizers based on key
|
||||||
|
if (key.includes('path') || key.includes('file')) {
|
||||||
|
sanitized[key] = typeof value === 'string' ? sanitizers.sanitizeFilePath(value) : value;
|
||||||
|
} else if (key.includes('project')) {
|
||||||
|
sanitized[key] = typeof value === 'string' ? sanitizers.sanitizeProjectPath(value) : value;
|
||||||
|
} else if (key.includes('error') || key.includes('message')) {
|
||||||
|
sanitized[key] = typeof value === 'string' ? sanitizers.sanitizeErrorMessage(value) : value;
|
||||||
|
} else if (key.includes('agent_name')) {
|
||||||
|
sanitized[key] = typeof value === 'string' ? sanitizers.sanitizeAgentName(value) : value;
|
||||||
|
} else {
|
||||||
|
// For other properties, ensure no PII
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
// Remove potential file paths
|
||||||
|
let cleanValue = value.replace(/\/[\w\-\/\.]+/g, '/***');
|
||||||
|
// Remove potential API keys
|
||||||
|
cleanValue = cleanValue.replace(/[a-zA-Z0-9]{32,}/g, '***');
|
||||||
|
// Remove emails
|
||||||
|
cleanValue = cleanValue.replace(/[\w\.-]+@[\w\.-]+\.\w+/g, '***@***.***');
|
||||||
|
sanitized[key] = cleanValue;
|
||||||
|
} else {
|
||||||
|
sanitized[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sanitized;
|
||||||
|
}
|
||||||
|
|
||||||
|
private flushEvents(): void {
|
||||||
|
if (this.eventQueue.length === 0) return;
|
||||||
|
|
||||||
|
const events = [...this.eventQueue];
|
||||||
|
this.eventQueue = [];
|
||||||
|
|
||||||
|
events.forEach(event => {
|
||||||
|
if (typeof posthog !== 'undefined' && posthog.capture) {
|
||||||
|
posthog.capture(event.event, {
|
||||||
|
...event.properties,
|
||||||
|
$session_id: event.sessionId,
|
||||||
|
timestamp: event.timestamp,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private startFlushInterval(): void {
|
||||||
|
// Flush events every 5 seconds
|
||||||
|
this.flushInterval = setInterval(() => {
|
||||||
|
if (this.consentManager.isEnabled()) {
|
||||||
|
this.flushEvents();
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
shutdown(): void {
|
||||||
|
if (this.flushInterval) {
|
||||||
|
clearInterval(this.flushInterval);
|
||||||
|
this.flushInterval = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flush any remaining events
|
||||||
|
this.flushEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convenience methods
|
||||||
|
isEnabled(): boolean {
|
||||||
|
return this.consentManager.isEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
hasConsented(): boolean {
|
||||||
|
return this.consentManager.hasConsented();
|
||||||
|
}
|
||||||
|
|
||||||
|
getSettings(): AnalyticsSettings | null {
|
||||||
|
return this.consentManager.getSettings();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export singleton instance
|
||||||
|
export const analytics = AnalyticsService.getInstance();
|
||||||
|
|
||||||
|
// Export for direct usage
|
||||||
|
export default analytics;
|
102
src/lib/analytics/types.ts
Normal file
102
src/lib/analytics/types.ts
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
export interface AnalyticsEvent {
|
||||||
|
event: string;
|
||||||
|
properties?: {
|
||||||
|
category?: string;
|
||||||
|
action?: string;
|
||||||
|
label?: string;
|
||||||
|
value?: number;
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
timestamp: number;
|
||||||
|
sessionId: string;
|
||||||
|
userId: string; // anonymous UUID
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AnalyticsSettings {
|
||||||
|
enabled: boolean;
|
||||||
|
hasConsented: boolean;
|
||||||
|
consentDate?: string;
|
||||||
|
userId?: string;
|
||||||
|
sessionId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AnalyticsConfig {
|
||||||
|
apiKey: string;
|
||||||
|
apiHost?: string;
|
||||||
|
persistence?: 'localStorage' | 'memory';
|
||||||
|
autocapture?: boolean;
|
||||||
|
disable_session_recording?: boolean;
|
||||||
|
opt_out_capturing_by_default?: boolean;
|
||||||
|
loaded?: (posthog: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EventName =
|
||||||
|
| 'session_created'
|
||||||
|
| 'session_completed'
|
||||||
|
| 'session_resumed'
|
||||||
|
| 'feature_used'
|
||||||
|
| 'error_occurred'
|
||||||
|
| 'model_selected'
|
||||||
|
| 'tab_created'
|
||||||
|
| 'tab_closed'
|
||||||
|
| 'file_opened'
|
||||||
|
| 'file_edited'
|
||||||
|
| 'file_saved'
|
||||||
|
| 'agent_executed'
|
||||||
|
| 'mcp_server_connected'
|
||||||
|
| 'mcp_server_disconnected'
|
||||||
|
| 'slash_command_used'
|
||||||
|
| 'settings_changed'
|
||||||
|
| 'app_started'
|
||||||
|
| 'app_closed';
|
||||||
|
|
||||||
|
export interface FeatureUsageProperties {
|
||||||
|
feature: string;
|
||||||
|
subfeature?: string;
|
||||||
|
metadata?: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ErrorProperties {
|
||||||
|
error_type: string;
|
||||||
|
error_code?: string;
|
||||||
|
error_message?: string;
|
||||||
|
context?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SessionProperties {
|
||||||
|
model?: string;
|
||||||
|
source?: string;
|
||||||
|
resumed?: boolean;
|
||||||
|
checkpoint_id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ModelProperties {
|
||||||
|
previous_model?: string;
|
||||||
|
new_model: string;
|
||||||
|
source?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AgentProperties {
|
||||||
|
agent_type: string;
|
||||||
|
agent_name?: string;
|
||||||
|
success: boolean;
|
||||||
|
duration_ms?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MCPProperties {
|
||||||
|
server_name: string;
|
||||||
|
server_type?: string;
|
||||||
|
success: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SlashCommandProperties {
|
||||||
|
command: string;
|
||||||
|
success: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PerformanceMetrics {
|
||||||
|
startup_time_ms?: number;
|
||||||
|
memory_usage_mb?: number;
|
||||||
|
api_response_time_ms?: number;
|
||||||
|
render_time_ms?: number;
|
||||||
|
}
|
Reference in New Issue
Block a user