main pico / cmd / scripts / analytics-aggregate / main.go
Eric Bower  ·  2026-05-03
  1package main
  2
  3import (
  4	"flag"
  5	"fmt"
  6	"log/slog"
  7	"os"
  8	"time"
  9
 10	"github.com/picosh/pico/pkg/apps/auth"
 11	"github.com/picosh/pico/pkg/db/postgres"
 12)
 13
 14func main() {
 15	monthPtr := flag.String("month", "", "target month in YYYY-MM format (default: previous month)")
 16	backfill := flag.Bool("backfill", false, "aggregate all historical months up to the month before last")
 17	dryRun := flag.Bool("dry-run", false, "print months that would be processed without running aggregation")
 18	flag.Parse()
 19
 20	logger := slog.Default()
 21	dbURL := os.Getenv("DATABASE_URL")
 22	if dbURL == "" {
 23		fmt.Fprintln(os.Stderr, "DATABASE_URL must be set")
 24		os.Exit(1)
 25	}
 26	dbpool := postgres.NewDB(dbURL, logger)
 27	defer func() { _ = dbpool.Close() }()
 28
 29	if *backfill {
 30		runBackfill(dbpool, logger, *dryRun)
 31		return
 32	}
 33
 34	targetMonth := parseMonth(*monthPtr, logger)
 35	if err := auth.RunAnalyticsAggregation(dbpool, logger, targetMonth); err != nil {
 36		logger.Error("aggregation failed", "err", err)
 37		os.Exit(1)
 38	}
 39}
 40
 41func runBackfill(dbpool *postgres.PsqlDB, logger *slog.Logger, dryRun bool) {
 42	months, err := fetchHistoricalMonths(dbpool)
 43	if err != nil {
 44		logger.Error("failed to fetch historical months", "err", err)
 45		os.Exit(1)
 46	}
 47	if dryRun {
 48		fmt.Println("Months to backfill:")
 49		for _, m := range months {
 50			fmt.Println(" ", m.Format("2006-01"))
 51		}
 52		return
 53	}
 54	for _, m := range months {
 55		if err := auth.RunAnalyticsAggregation(dbpool, logger, m); err != nil {
 56			logger.Error("aggregation failed for month", "month", m.Format("2006-01"), "err", err)
 57		}
 58	}
 59	logger.Info("backfill complete", "months", len(months))
 60}
 61
 62// fetchHistoricalMonths returns all distinct months that have data in analytics_visits,
 63// excluding the current month and the previous month (handled by the auth service cron).
 64func fetchHistoricalMonths(dbpool *postgres.PsqlDB) ([]time.Time, error) {
 65	cutoff := time.Now().AddDate(0, -1, 0)
 66	cutoffMonth := time.Date(cutoff.Year(), cutoff.Month(), 1, 0, 0, 0, 0, time.UTC)
 67
 68	rows, err := dbpool.Db.Queryx(`
 69		SELECT DISTINCT date_trunc('month', created_at)::date AS month_start
 70		FROM analytics_visits
 71		WHERE created_at < $1
 72		ORDER BY month_start ASC
 73	`, cutoffMonth)
 74	if err != nil {
 75		return nil, err
 76	}
 77	defer func() { _ = rows.Close() }()
 78
 79	var months []time.Time
 80	for rows.Next() {
 81		var monthDate time.Time
 82		if err := rows.Scan(&monthDate); err != nil {
 83			return nil, err
 84		}
 85		months = append(months, monthDate)
 86	}
 87	return months, rows.Err()
 88}
 89
 90func parseMonth(arg string, logger *slog.Logger) time.Time {
 91	now := time.Now()
 92	if arg != "" {
 93		t, err := time.Parse("2006-01", arg)
 94		if err != nil {
 95			logger.Error("invalid month format, use YYYY-MM", "err", err, "input", arg)
 96			os.Exit(1)
 97		}
 98		return t
 99	}
100	return now.AddDate(0, -1, 0)
101}