226 lines
5.3 KiB
Go
226 lines
5.3 KiB
Go
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
|
|
}
|