own tracking

This commit is contained in:
JurajKubrican
2025-08-15 23:13:47 +02:00
parent ebfd1fe9f1
commit 3249df5802
10 changed files with 531 additions and 3 deletions

View File

@@ -16,6 +16,7 @@ import (
"github.com/prometheus/client_golang/prometheus/promhttp"
"knet.sk/src/boxes"
"knet.sk/src/draw"
"knet.sk/src/tracking"
"knet.sk/src/util"
)
@@ -100,6 +101,11 @@ func main() {
}))
}
// MY supper tracking
e.Use(tracking.Echo)
trackingGroup := e.Group("/tracking")
trackingGroup.GET("", tracking.BasicTrackingHandler)
// Prometheus metrics endpoint (standard /metrics)
metricsGroup := e.Group("/metrics")
if util.IsProd() {
@@ -148,7 +154,9 @@ func main() {
return draw.Page(c)
})
e.GET("/draw/ws", draw.InitWs)
e.GET("/draw/ws", draw.InitWs)
// Tracking dashboard
e.GET("/tracking", tracking.BasicTrackingHandler)
go boxes.RegisterTicker()

View File

@@ -0,0 +1,61 @@
package tracking
import (
"fmt"
"github.com/labstack/echo/v4"
"knet.sk/src/util"
)
type TrackingData struct {
BuildNumber string
Integrity *util.AssetIntegrity
TotalVisits int
UniqueVisitors int
TodayVisits int
ActiveSessions int
RecentVisits []VisitDisplay
TopPages []PageStat
}
type VisitDisplay struct {
Timestamp string
IPAddress string
Path string
UserAgent string
Referrer string
}
type PageStat struct {
Path string
Count int
Percentage float64
}
var ts *TrackingService
func Echo(next echo.HandlerFunc) echo.HandlerFunc {
ts = StartTrackingService()
return func(c echo.Context) error {
fmt.Printf("Tracking visit: %s %s\n", c.RealIP(), c.Request().URL.Path)
go ts.AddVisit(c)
return next(c)
}
}
func BasicTrackingHandler(c echo.Context) error {
// Generate mock data
mockData := TrackingData{
BuildNumber: util.GetBuildNumber(),
Integrity: util.CalculateAssetIntegrities(),
TotalVisits: ts.GetTotalVisits(),
UniqueVisitors: ts.GetUniqueVisitors(),
TodayVisits: ts.GetTodayVisits(),
ActiveSessions: ts.GetActiveSessions(),
RecentVisits: ts.GetRecentVisits(20),
TopPages: ts.GetTopPages(5),
}
return c.Render(200, "tracking", mockData)
}

View File

@@ -0,0 +1,225 @@
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
}

15
src/tracking/util.go Normal file
View File

@@ -0,0 +1,15 @@
package tracking
import (
"github.com/labstack/echo/v4"
)
func getFingerprint(c echo.Context) string {
// ip, userAgent acceptLangueage Header
ip := c.RealIP()
userAgent := c.Request().Header.Get("User-Agent")
acceptLanguage := c.Request().Header.Get("Accept-Language")
// Combine them into a fingerprint string
return ip + userAgent + acceptLanguage
}