This commit is contained in:
Josh 2026-01-09 00:16:46 +02:00
commit d69342b2e9
160 changed files with 28681 additions and 0 deletions

View file

@ -0,0 +1,279 @@
package tenant
import (
"context"
"database/sql"
"encoding/json"
"time"
)
type PageView struct {
ID int64
Path string
PostSlug string
Referrer string
UserAgent string
CreatedAt time.Time
}
type AnalyticsSummary struct {
TotalViews int64 `json:"total_views"`
TotalPageViews int64 `json:"total_page_views"`
UniqueVisitors int64 `json:"unique_visitors"`
TotalBandwidth int64 `json:"total_bandwidth"`
ViewsChange float64 `json:"views_change"`
TopPages []PageStats `json:"top_pages"`
TopReferrers []ReferrerStats `json:"top_referrers"`
ViewsByDay []DailyStats `json:"views_by_day"`
Browsers []NamedStat `json:"browsers"`
OS []NamedStat `json:"os"`
Devices []NamedStat `json:"devices"`
Countries []NamedStat `json:"countries"`
}
type PageStats struct {
Path string `json:"path"`
Views int64 `json:"views"`
}
type ReferrerStats struct {
Referrer string `json:"referrer"`
Views int64 `json:"views"`
}
type DailyStats struct {
Date string `json:"date"`
Views int64 `json:"views"`
Visitors int64 `json:"visitors"`
}
type NamedStat struct {
Name string `json:"name"`
Count int64 `json:"count"`
}
type ArchivedDay struct {
Date string `json:"date"`
Requests int64 `json:"requests"`
PageViews int64 `json:"page_views"`
UniqueVisitors int64 `json:"unique_visitors"`
Bandwidth int64 `json:"bandwidth"`
Browsers []NamedStat `json:"browsers"`
OS []NamedStat `json:"os"`
Devices []NamedStat `json:"devices"`
Countries []NamedStat `json:"countries"`
Paths []PageStats `json:"paths"`
}
func (q *Queries) RecordPageView(ctx context.Context, path, postSlug, referrer, userAgent string) error {
_, err := q.db.ExecContext(ctx, `INSERT INTO page_views (path, post_slug, referrer, user_agent, visitor_hash, utm_source, utm_medium, utm_campaign, device_type, browser, os, country)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
path, nullStr(postSlug), nullStr(referrer), nullStr(userAgent), sql.NullString{}, sql.NullString{}, sql.NullString{}, sql.NullString{}, sql.NullString{}, sql.NullString{}, sql.NullString{}, sql.NullString{})
return err
}
func (q *Queries) GetAnalytics(ctx context.Context, days int) (*AnalyticsSummary, error) {
if days <= 0 {
days = 30
}
since := time.Now().AddDate(0, 0, -days).Format("2006-01-02")
var totalCount, uniqueCount int64
err := q.db.QueryRowContext(ctx, `SELECT COUNT(*), COUNT(DISTINCT visitor_hash) FROM page_views WHERE created_at >= ?`, since).
Scan(&totalCount, &uniqueCount)
if err != nil {
return nil, err
}
topPagesRows, err := q.db.QueryContext(ctx, `SELECT path, COUNT(*) as views FROM page_views WHERE created_at >= ? GROUP BY path ORDER BY views DESC LIMIT ?`, since, 10)
if err != nil {
return nil, err
}
defer topPagesRows.Close()
var topPages []PageStats
for topPagesRows.Next() {
var p PageStats
if err := topPagesRows.Scan(&p.Path, &p.Views); err != nil {
return nil, err
}
topPages = append(topPages, p)
}
topRefRows, err := q.db.QueryContext(ctx, `SELECT COALESCE(referrer, 'Direct') as referrer, COUNT(*) as views FROM page_views WHERE created_at >= ? AND referrer != '' GROUP BY referrer ORDER BY views DESC LIMIT ?`, since, 10)
if err != nil {
return nil, err
}
defer topRefRows.Close()
var topReferrers []ReferrerStats
for topRefRows.Next() {
var r ReferrerStats
if err := topRefRows.Scan(&r.Referrer, &r.Views); err != nil {
return nil, err
}
topReferrers = append(topReferrers, r)
}
viewsByDayRows, err := q.db.QueryContext(ctx, `SELECT DATE(created_at) as date, COUNT(*) as views FROM page_views WHERE created_at >= ? GROUP BY date ORDER BY date ASC`, since)
if err != nil {
return nil, err
}
defer viewsByDayRows.Close()
var viewsByDay []DailyStats
for viewsByDayRows.Next() {
var d DailyStats
var date any
if err := viewsByDayRows.Scan(&date, &d.Views); err != nil {
return nil, err
}
if s, ok := date.(string); ok {
d.Date = s
}
viewsByDay = append(viewsByDay, d)
}
return &AnalyticsSummary{
TotalViews: totalCount,
TotalPageViews: totalCount,
UniqueVisitors: uniqueCount,
TopPages: topPages,
TopReferrers: topReferrers,
ViewsByDay: viewsByDay,
}, nil
}
func (q *Queries) GetPostAnalytics(ctx context.Context, slug string, days int) (*AnalyticsSummary, error) {
if days <= 0 {
days = 30
}
since := time.Now().AddDate(0, 0, -days).Format("2006-01-02")
var totalViews int64
err := q.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM page_views WHERE post_slug = ? AND created_at >= ?`, slug, since).Scan(&totalViews)
if err != nil {
return nil, err
}
viewsByDayRows, err := q.db.QueryContext(ctx, `SELECT DATE(created_at) as date, COUNT(*) as views FROM page_views WHERE post_slug = ? AND created_at >= ? GROUP BY date ORDER BY date ASC`, slug, since)
if err != nil {
return nil, err
}
defer viewsByDayRows.Close()
var viewsByDay []DailyStats
for viewsByDayRows.Next() {
var d DailyStats
var date any
if err := viewsByDayRows.Scan(&date, &d.Views); err != nil {
return nil, err
}
if s, ok := date.(string); ok {
d.Date = s
}
viewsByDay = append(viewsByDay, d)
}
refRows, err := q.db.QueryContext(ctx, `SELECT COALESCE(referrer, 'Direct') as referrer, COUNT(*) as views FROM page_views WHERE post_slug = ? AND created_at >= ? AND referrer != '' GROUP BY referrer ORDER BY views DESC LIMIT ?`, slug, since, 10)
if err != nil {
return nil, err
}
defer refRows.Close()
var topReferrers []ReferrerStats
for refRows.Next() {
var r ReferrerStats
if err := refRows.Scan(&r.Referrer, &r.Views); err != nil {
return nil, err
}
topReferrers = append(topReferrers, r)
}
return &AnalyticsSummary{
TotalViews: totalViews,
TopReferrers: topReferrers,
ViewsByDay: viewsByDay,
}, nil
}
func (q *Queries) SaveDailyAnalytics(ctx context.Context, day *ArchivedDay) error {
browsers, _ := json.Marshal(day.Browsers)
os, _ := json.Marshal(day.OS)
devices, _ := json.Marshal(day.Devices)
countries, _ := json.Marshal(day.Countries)
paths, _ := json.Marshal(day.Paths)
_, err := q.db.ExecContext(ctx, `INSERT INTO daily_analytics (date, requests, page_views, unique_visitors, bandwidth, browsers, os, devices, countries, paths)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(date) DO UPDATE SET
requests = excluded.requests, page_views = excluded.page_views, unique_visitors = excluded.unique_visitors,
bandwidth = excluded.bandwidth, browsers = excluded.browsers, os = excluded.os, devices = excluded.devices,
countries = excluded.countries, paths = excluded.paths`,
day.Date, day.Requests, day.PageViews, day.UniqueVisitors, day.Bandwidth,
nullStr(string(browsers)), nullStr(string(os)), nullStr(string(devices)), nullStr(string(countries)), nullStr(string(paths)))
return err
}
func (q *Queries) GetArchivedAnalytics(ctx context.Context, since, until string) ([]ArchivedDay, error) {
rows, err := q.db.QueryContext(ctx, `SELECT date, requests, page_views, unique_visitors, bandwidth, browsers, os, devices, countries, paths
FROM daily_analytics WHERE date >= ? AND date <= ? ORDER BY date ASC`, since, until)
if err != nil {
return nil, err
}
defer rows.Close()
var days []ArchivedDay
for rows.Next() {
var d ArchivedDay
var requests, pageViews, uniqueVisitors, bandwidth sql.NullInt64
var browsers, os, devices, countries, paths sql.NullString
if err := rows.Scan(&d.Date, &requests, &pageViews, &uniqueVisitors, &bandwidth, &browsers, &os, &devices, &countries, &paths); err != nil {
return nil, err
}
d.Requests = requests.Int64
d.PageViews = pageViews.Int64
d.UniqueVisitors = uniqueVisitors.Int64
d.Bandwidth = bandwidth.Int64
if browsers.Valid {
json.Unmarshal([]byte(browsers.String), &d.Browsers)
}
if os.Valid {
json.Unmarshal([]byte(os.String), &d.OS)
}
if devices.Valid {
json.Unmarshal([]byte(devices.String), &d.Devices)
}
if countries.Valid {
json.Unmarshal([]byte(countries.String), &d.Countries)
}
if paths.Valid {
json.Unmarshal([]byte(paths.String), &d.Paths)
}
days = append(days, d)
}
return days, rows.Err()
}
func (q *Queries) GetOldestArchivedDate(ctx context.Context) (string, error) {
var date any
err := q.db.QueryRowContext(ctx, `SELECT MIN(date) FROM daily_analytics`).Scan(&date)
if err != nil || date == nil {
return "", err
}
if s, ok := date.(string); ok {
return s, nil
}
return "", nil
}
func (q *Queries) HasArchivedDate(ctx context.Context, date string) (bool, error) {
var count int64
err := q.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM daily_analytics WHERE date = ?`, date).Scan(&count)
return count > 0, err
}