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

4
data/.gitignore vendored
View File

@@ -1 +1,3 @@
data.db*
*.db
*.db-wal
*.db-shm

Binary file not shown.

9
go.mod
View File

@@ -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
)

44
go.sum
View File

@@ -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=

View File

@@ -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'

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
}

161
views/tracking.html Normal file
View File

@@ -0,0 +1,161 @@
{{block "tracking" .}}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Tracking Dashboard</title>
<link rel="stylesheet" href="/css/main.css?v={{.BuildNumber}}" integrity="{{index .Integrity.CSS "main.css" }}" crossorigin="anonymous" />
<link rel="icon" type="image/svg+xml" href="/images/favicon.svg" />
<link rel="icon" type="image/x-icon" href="/images/favicon.ico" />
<style>
.tracking-dashboard {
max-width: 1200px;
margin: 2rem auto;
padding: 0 1rem;
}
.tracking-table {
width: 100%;
border-collapse: collapse;
margin: 2rem 0;
background: var(--bg-secondary);
border-radius: 8px;
overflow: hidden;
}
.tracking-table th,
.tracking-table td {
padding: 1rem;
text-align: left;
border-bottom: 1px solid var(--border);
}
.tracking-table th {
background: var(--bg-tertiary);
font-weight: 600;
}
.tracking-table tr:hover {
background: var(--bg-hover);
}
.metric-card {
background: var(--bg-secondary);
border-radius: 8px;
padding: 1.5rem;
margin: 1rem 0;
border: 1px solid var(--border);
}
.metric-value {
font-size: 2rem;
font-weight: bold;
color: var(--color-primary);
}
.metric-label {
color: var(--color-secondary);
font-size: 0.9rem;
margin-top: 0.5rem;
}
.metrics-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin: 2rem 0;
}
</style>
</head>
<body>
<header>
<nav class="logo">
<a href="/">
<svg width="64" height="64" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="16" cy="16" r="15" fill="#1e1e1e" stroke="var(--color)" stroke-width="2"/>
<line x1="10" y1="8" x2="10" y2="24" stroke="var(--color)" stroke-width="2" stroke-linecap="round"/>
<line x1="10" y1="16" x2="22" y2="8" stroke="var(--color)" stroke-width="2" stroke-linecap="round"/>
<line x1="10" y1="16" x2="22" y2="24" stroke="var(--color)" stroke-width="2" stroke-linecap="round"/>
</svg>
</a>
</nav>
</header>
<main>
<div class="tracking-dashboard">
<h1>Tracking Dashboard</h1>
<div class="metrics-grid">
<div class="metric-card">
<div class="metric-value">{{.TotalVisits}}</div>
<div class="metric-label">Total Visits</div>
</div>
<div class="metric-card">
<div class="metric-value">{{.UniqueVisitors}}</div>
<div class="metric-label">Unique Visitors</div>
</div>
<div class="metric-card">
<div class="metric-value">{{.TodayVisits}}</div>
<div class="metric-label">Today's Visits</div>
</div>
<div class="metric-card">
<div class="metric-value">{{.ActiveSessions}}</div>
<div class="metric-label">Active Sessions</div>
</div>
</div>
<h2>Recent Visits</h2>
<table class="tracking-table">
<thead>
<tr>
<th>Timestamp</th>
<th>IP Address</th>
<th>Path</th>
<th>User Agent</th>
<th>Referrer</th>
</tr>
</thead>
<tbody>
{{range .RecentVisits}}
<tr>
<td>{{.Timestamp}}</td>
<td>{{.IPAddress}}</td>
<td>{{.Path}}</td>
<td>{{.UserAgent}}</td>
<td>{{.Referrer}}</td>
</tr>
{{else}}
<tr>
<td colspan="5" style="text-align: center; color: var(--color-secondary);">
No visits recorded yet
</td>
</tr>
{{end}}
</tbody>
</table>
<h2>Top Pages</h2>
<table class="tracking-table">
<thead>
<tr>
<th>Path</th>
<th>Visits</th>
<th>Percentage</th>
</tr>
</thead>
<tbody>
{{range .TopPages}}
<tr>
<td>{{.Path}}</td>
<td>{{.Count}}</td>
<td>{{printf "%.2f" .Percentage}}%</td>
</tr>
{{else}}
<tr>
<td colspan="3" style="text-align: center; color: var(--color-secondary);">
No page data available
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
</main>
<script src="/js/global.d.ts?v={{.BuildNumber}}" integrity="{{index .Integrity.JS "global.d.ts" }}" crossorigin="anonymous"></script>
</body>
</html>
{{end}}