diff --git a/data/.gitignore b/data/.gitignore index 2c57205..fc5edfe 100644 --- a/data/.gitignore +++ b/data/.gitignore @@ -1 +1,3 @@ -data.db* \ No newline at end of file +*.db +*.db-wal +*.db-shm \ No newline at end of file diff --git a/data/tracking.db b/data/tracking.db deleted file mode 100644 index dd167d4..0000000 Binary files a/data/tracking.db and /dev/null differ diff --git a/go.mod b/go.mod index d428ee8..c7ec811 100644 --- a/go.mod +++ b/go.mod @@ -8,22 +8,31 @@ require ( github.com/labstack/gommon v0.4.2 github.com/prometheus/client_golang v1.23.0 golang.org/x/net v0.42.0 + modernc.org/sqlite v1.38.2 ) require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.65.0 // indirect github.com/prometheus/procfs v0.16.1 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect golang.org/x/crypto v0.40.0 // indirect + golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect golang.org/x/sys v0.34.0 // indirect golang.org/x/text v0.27.0 // indirect golang.org/x/time v0.12.0 // indirect google.golang.org/protobuf v1.36.6 // indirect + modernc.org/libc v1.66.3 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect ) diff --git a/go.sum b/go.sum index 03561b1..2833817 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,14 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= @@ -22,6 +28,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc= @@ -32,6 +40,8 @@ github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2 github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= @@ -42,8 +52,14 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= +golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= +golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= @@ -51,7 +67,35 @@ golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= +golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +modernc.org/cc/v4 v4.26.2 h1:991HMkLjJzYBIfha6ECZdjrIYz2/1ayr+FL8GN+CNzM= +modernc.org/cc/v4 v4.26.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU= +modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE= +modernc.org/fileutil v1.3.8 h1:qtzNm7ED75pd1C7WgAGcK4edm4fvhtBsEiI/0NQ54YM= +modernc.org/fileutil v1.3.8/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= +modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= +modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ= +modernc.org/libc v1.66.3/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= +modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.38.2 h1:Aclu7+tgjgcQVShZqim41Bbw9Cho0y/7WzYptXqkEek= +modernc.org/sqlite v1.38.2/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/prometheus.yml b/prometheus.yml index 651b6c6..38cde9d 100644 --- a/prometheus.yml +++ b/prometheus.yml @@ -34,13 +34,16 @@ scrape_configs: # Timeout for scraping scrape_timeout: 10s + # Use HTTPS to connect to the target + scheme: https + # Basic authentication for protected endpoints basic_auth: username: 'api' password: 'your-default-token' # This should match your API_TOKEN env var static_configs: - - targets: ['knet:54321'] # Your knet application + - targets: ['knet.sk'] # Your knet application labels: instance: 'knet-prod' environment: 'production' diff --git a/src/main.go b/src/main.go index 837b27f..4442060 100644 --- a/src/main.go +++ b/src/main.go @@ -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() diff --git a/src/tracking/middleware.go b/src/tracking/middleware.go new file mode 100644 index 0000000..7d72a0c --- /dev/null +++ b/src/tracking/middleware.go @@ -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) +} diff --git a/src/tracking/tracking-store.go b/src/tracking/tracking-store.go new file mode 100644 index 0000000..b3cddf1 --- /dev/null +++ b/src/tracking/tracking-store.go @@ -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 +} diff --git a/src/tracking/util.go b/src/tracking/util.go new file mode 100644 index 0000000..cafd90f --- /dev/null +++ b/src/tracking/util.go @@ -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 +} diff --git a/views/tracking.html b/views/tracking.html new file mode 100644 index 0000000..78188df --- /dev/null +++ b/views/tracking.html @@ -0,0 +1,161 @@ +{{block "tracking" .}} + + + + + + Tracking Dashboard + + + + + + +
+ +
+ +
+
+

Tracking Dashboard

+ +
+
+
{{.TotalVisits}}
+
Total Visits
+
+
+
{{.UniqueVisitors}}
+
Unique Visitors
+
+
+
{{.TodayVisits}}
+
Today's Visits
+
+
+
{{.ActiveSessions}}
+
Active Sessions
+
+
+ +

Recent Visits

+ + + + + + + + + + + + {{range .RecentVisits}} + + + + + + + + {{else}} + + + + {{end}} + +
TimestampIP AddressPathUser AgentReferrer
{{.Timestamp}}{{.IPAddress}}{{.Path}}{{.UserAgent}}{{.Referrer}}
+ No visits recorded yet +
+ +

Top Pages

+ + + + + + + + + + {{range .TopPages}} + + + + + + {{else}} + + + + {{end}} + +
PathVisitsPercentage
{{.Path}}{{.Count}}{{printf "%.2f" .Percentage}}%
+ No page data available +
+
+
+ + + + +{{end}}