own tracking
This commit is contained in:
4
data/.gitignore
vendored
4
data/.gitignore
vendored
@@ -1 +1,3 @@
|
||||
data.db*
|
||||
*.db
|
||||
*.db-wal
|
||||
*.db-shm
|
||||
BIN
data/tracking.db
BIN
data/tracking.db
Binary file not shown.
9
go.mod
9
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
|
||||
)
|
||||
|
||||
44
go.sum
44
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=
|
||||
|
||||
@@ -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'
|
||||
|
||||
10
src/main.go
10
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()
|
||||
|
||||
|
||||
61
src/tracking/middleware.go
Normal file
61
src/tracking/middleware.go
Normal 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)
|
||||
}
|
||||
225
src/tracking/tracking-store.go
Normal file
225
src/tracking/tracking-store.go
Normal 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
15
src/tracking/util.go
Normal 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
161
views/tracking.html
Normal 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}}
|
||||
Reference in New Issue
Block a user