init
This commit is contained in:
commit
d69342b2e9
160 changed files with 28681 additions and 0 deletions
279
internal/tenant/analytics.go
Normal file
279
internal/tenant/analytics.go
Normal 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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue