Files
analytics/database.go

182 lines
4.6 KiB
Go

package main
import (
"context"
"database/sql"
"fmt"
"os"
"time"
_ "github.com/glebarez/go-sqlite"
)
type InstancesStats struct {
Total int `json:"total"`
History []InstancesHistory `json:"history"`
}
type InstancesHistory struct {
Date string `json:"date"`
Count int `json:"count"`
}
func DoesInstanceExist(parentCtx context.Context, db *sql.DB, instanceID string) (bool, error) {
const query = `
SELECT EXISTS(SELECT 1 FROM instances WHERE id = ?)
`
ctx, cancel := context.WithTimeout(parentCtx, 10*time.Second)
defer cancel()
var exists bool
err := db.QueryRowContext(ctx, query, instanceID).Scan(&exists)
if err != nil {
return false, fmt.Errorf("failed to check instance existence: %w", err)
}
return exists, nil
}
func UpsertInstance(parentCtx context.Context, db *sql.DB, instanceID, version string) error {
now := time.Now()
// Upsert the instance
const query = `
INSERT INTO instances (id, first_seen, last_seen, latest_version)
VALUES (?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
last_seen = excluded.last_seen,
latest_version = excluded.latest_version
`
ctx, cancel := context.WithTimeout(parentCtx, 10*time.Second)
defer cancel()
_, err := db.ExecContext(
ctx,
query,
instanceID, now, now, version,
)
return err
}
func GetTotalInstances(parentCtx context.Context, db *sql.DB) (int, error) {
// Only count instances that:
// 1. Are older than 1 day
// 2. Have been active in the last 2 days
const query = `
SELECT COUNT(*)
FROM instances
WHERE first_seen < datetime('now', '-1 day')
AND last_seen >= datetime('now', '-2 days')
`
ctx, cancel := context.WithTimeout(parentCtx, 10*time.Second)
defer cancel()
var count int
err := db.QueryRowContext(ctx, query).Scan(&count)
return count, err
}
func GetInstancesOverTime(parentCtx context.Context, db *sql.DB, timeframe string) ([]InstancesHistory, error) {
var query string
switch timeframe {
case "daily":
// Get daily instance counts for the last 30 days
// Only include instances that are older than 1 day and were active in the last 2 days
query = `
SELECT
DATE(first_seen) as date,
COUNT(*) as daily_new,
(SELECT COUNT(*)
FROM instances i2
WHERE DATE(i2.first_seen) <= DATE(i1.first_seen)
AND i2.first_seen < datetime('now', '-1 day')
AND i2.last_seen >= datetime('now', '-2 days')) as cumulative_count
FROM instances i1
WHERE first_seen >= datetime('now', '-30 days')
AND first_seen < datetime('now', '-1 day')
AND last_seen >= datetime('now', '-2 days')
GROUP BY DATE(first_seen)
ORDER BY date
`
case "monthly":
// Get monthly instance counts for all time
// Only include instances that are older than 1 day and were active in the last 2 days
query = `
SELECT
strftime('%Y-%m', first_seen) as date,
COUNT(*) as monthly_new,
(SELECT COUNT(*)
FROM instances i2
WHERE strftime('%Y-%m', i2.first_seen) <= strftime('%Y-%m', i1.first_seen)
AND i2.first_seen < datetime('now', '-1 day')
AND i2.last_seen >= datetime('now', '-2 days')) as cumulative_count
FROM instances i1
WHERE first_seen < datetime('now', '-1 day')
AND last_seen >= datetime('now', '-2 days')
GROUP BY strftime('%Y-%m', first_seen)
ORDER BY date
`
default:
return nil, fmt.Errorf("invalid timeframe: %s. Use 'daily' or 'monthly'", timeframe)
}
ctx, cancel := context.WithTimeout(parentCtx, 10*time.Second)
defer cancel()
rows, err := db.QueryContext(ctx, query)
if err != nil {
return nil, err
}
defer rows.Close()
chartData := make([]InstancesHistory, 0, 36)
for rows.Next() {
var date string
var newCount, cumulativeCount int
err := rows.Scan(&date, &newCount, &cumulativeCount)
if err != nil {
return nil, err
}
chartData = append(chartData, InstancesHistory{
Date: date,
Count: cumulativeCount,
})
}
return chartData, nil
}
func initDB() (*sql.DB, error) {
if err := os.MkdirAll("./data", 0755); err != nil {
return nil, fmt.Errorf("failed to create data directory: %w", err)
}
db, err := sql.Open("sqlite", "./data/pocket-id-analytics.db?_pragma=journal_mode(WAL)&_pragma=busy_timeout(5000)&_txlock=immediate")
if err != nil {
return nil, err
}
// Create instances table
createTableSQL := `
CREATE TABLE IF NOT EXISTS instances (
id TEXT PRIMARY KEY,
first_seen DATETIME NOT NULL,
last_seen DATETIME NOT NULL,
latest_version TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_first_seen ON instances(first_seen);
CREATE INDEX IF NOT EXISTS idx_last_seen ON instances(last_seen);
`
_, err = db.Exec(createTableSQL)
if err != nil {
return nil, err
}
return db, nil
}