own tracking
This commit is contained in:
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
|
||||
}
|
||||
Reference in New Issue
Block a user