package tracking import ( "database/sql" "sync" "github.com/labstack/echo/v4" _ "modernc.org/sqlite" ) type TrackingService struct { db *sql.DB mu sync.RWMutex // Add mutex for thread safety } var trackingService *TrackingService func StartTrackingService() *TrackingService { if trackingService == nil { db, err := sql.Open("sqlite", "./data/tracking.db") if err != nil { panic(err) } // Apply SQLite settings explicitly with PRAGMA statements pragmas := []string{ "PRAGMA busy_timeout = 10000", // 10 second busy timeout "PRAGMA journal_mode = WAL", // Write-Ahead Logging mode "PRAGMA synchronous = NORMAL", // Normal synchronization "PRAGMA foreign_keys = ON", // Enable foreign key constraints "PRAGMA temp_store = MEMORY", // Store temp tables in memory } for _, pragma := range pragmas { if _, err := db.Exec(pragma); err != nil { println("Warning: Failed to set pragma:", pragma, "Error:", err.Error()) } } // With mutex protection, we can use a single connection db.SetMaxOpenConns(1) db.SetMaxIdleConns(1) trackingService = &TrackingService{db: db} err = trackingService.initDB() if err != nil { panic(err) } } return trackingService } func (ts *TrackingService) AddVisit(c echo.Context) { ts.mu.Lock() defer ts.mu.Unlock() query := `INSERT INTO user_visits (ip, path, method, fingerprint, user_agent, referer, accept_language) VALUES (?, ?, ?, ?, ?, ?, ?)` fingerprint := getFingerprint(c) referer := c.Request().Header.Get("Referer") userAgent := c.Request().Header.Get("User-Agent") acceptLanguage := c.Request().Header.Get("Accept-Language") ip := c.RealIP() path := c.Request().URL.Path method := c.Request().Method _, err := ts.db.Exec(query, ip, path, method, fingerprint, userAgent, referer, acceptLanguage) if err != nil { // Log error instead of panic to prevent crashes println("Error adding visit:", err.Error()) } } func (ts *TrackingService) initDB() error { query := ` CREATE TABLE IF NOT EXISTS user_visits ( id INTEGER PRIMARY KEY AUTOINCREMENT, ip TEXT NOT NULL, path TEXT NOT NULL, method TEXT NOT NULL, fingerprint TEXT, user_agent TEXT, referer TEXT, accept_language TEXT, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP ); ` _, err := ts.db.Exec(query) return err } func (ts *TrackingService) GetTotalVisits() int { ts.mu.RLock() defer ts.mu.RUnlock() var count int query := `SELECT COUNT(*) FROM user_visits` err := ts.db.QueryRow(query).Scan(&count) if err != nil { println("Error getting total visits:", err.Error()) return 0 } return count } func (ts *TrackingService) GetUniqueVisitors() int { ts.mu.RLock() defer ts.mu.RUnlock() var count int query := `SELECT COUNT(DISTINCT fingerprint) FROM user_visits` err := ts.db.QueryRow(query).Scan(&count) if err != nil { println("Error getting unique visitors:", err.Error()) return 0 } return count } func (ts *TrackingService) GetTodayVisits() int { ts.mu.RLock() defer ts.mu.RUnlock() var count int query := `SELECT COUNT(*) FROM user_visits WHERE DATE(timestamp) = DATE('now')` err := ts.db.QueryRow(query).Scan(&count) if err != nil { println("Error getting today visits:", err.Error()) return 0 } return count } func (ts *TrackingService) GetActiveSessions() int { ts.mu.RLock() defer ts.mu.RUnlock() var count int query := `SELECT COUNT(DISTINCT fingerprint) FROM user_visits WHERE timestamp >= datetime('now', '-30 minutes')` err := ts.db.QueryRow(query).Scan(&count) if err != nil { println("Error getting active sessions:", err.Error()) return 0 } return count } func (ts *TrackingService) GetRecentVisits(limit int) []VisitDisplay { ts.mu.RLock() defer ts.mu.RUnlock() query := `SELECT timestamp, ip, path, user_agent, referer FROM user_visits ORDER BY timestamp DESC LIMIT ?` rows, err := ts.db.Query(query, limit) if err != nil { println("Error getting recent visits:", err.Error()) return []VisitDisplay{} } defer rows.Close() var visits []VisitDisplay for rows.Next() { var visit VisitDisplay if err := rows.Scan(&visit.Timestamp, &visit.IPAddress, &visit.Path, &visit.UserAgent, &visit.Referrer); err != nil { println("Error scanning visit:", err.Error()) continue } visits = append(visits, visit) } return visits } func (ts *TrackingService) GetTopPages(limit int) []PageStat { ts.mu.RLock() defer ts.mu.RUnlock() // First get total visits count var totalVisits int totalQuery := `SELECT COUNT(*) FROM user_visits` err := ts.db.QueryRow(totalQuery).Scan(&totalVisits) if err != nil { println("Error getting total visits for top pages:", err.Error()) return []PageStat{} } // Then get the top pages query := `SELECT path, COUNT(*) as count FROM user_visits GROUP BY path ORDER BY count DESC LIMIT ?` rows, err := ts.db.Query(query, limit) if err != nil { println("Error getting top pages:", err.Error()) return []PageStat{} } defer rows.Close() var pages []PageStat for rows.Next() { var page PageStat if err := rows.Scan(&page.Path, &page.Count); err != nil { println("Error scanning page:", err.Error()) continue } // Calculate percentage using the totalVisits we got earlier if totalVisits > 0 { page.Percentage = float64(page.Count) / float64(totalVisits) * 100 } pages = append(pages, page) } return pages }