Add 30-day send statistics and auto level-2 rate limits on the domain page. Close Unreleased; pin compose and docs to 1.6.0. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"time"
|
||||
)
|
||||
|
||||
// StatsWindowDays is the rolling window for send statistics on the domain page.
|
||||
const StatsWindowDays = 30
|
||||
|
||||
// SendStats holds message-volume metrics over a stats window (guide § Rate
|
||||
// limiting — same counting rules as CountMessages).
|
||||
type SendStats struct {
|
||||
Total int64
|
||||
PeakPerHour int64
|
||||
AvgPerHour float64
|
||||
WindowDays int // actual days queried (min(30, retention))
|
||||
WindowHours float64 // hours used for the average-rate denominator
|
||||
}
|
||||
|
||||
// StatsWindow computes the since timestamp and hours denominator for send
|
||||
// statistics. The query window is min(30, retention) days; hours_in_window is
|
||||
// min(720, entity age in hours, retention hours).
|
||||
func StatsWindow(retentionDays int, entityCreated time.Time) (since time.Time, hours float64, windowDays int) {
|
||||
if retentionDays <= 0 {
|
||||
retentionDays = SendLogRetentionDaysDefault
|
||||
}
|
||||
windowDays = StatsWindowDays
|
||||
if retentionDays < StatsWindowDays {
|
||||
windowDays = retentionDays
|
||||
}
|
||||
since = time.Now().UTC().AddDate(0, 0, -windowDays)
|
||||
|
||||
retentionHours := float64(retentionDays) * 24
|
||||
ageHours := time.Since(entityCreated).Hours()
|
||||
if ageHours < 0 {
|
||||
ageHours = 0
|
||||
}
|
||||
|
||||
hours = math.Min(720, math.Min(ageHours, retentionHours))
|
||||
if hours < 1 {
|
||||
hours = 1
|
||||
}
|
||||
return since, hours, windowDays
|
||||
}
|
||||
|
||||
// DomainSendStats returns 30-day (or shorter when retention is lower) send
|
||||
// statistics for a domain name.
|
||||
func (s *Store) DomainSendStats(name string, retentionDays int, createdAt time.Time) (SendStats, error) {
|
||||
since, hours, windowDays := StatsWindow(retentionDays, createdAt)
|
||||
stats, err := s.sendStats("domain", name, since, hours)
|
||||
if err != nil {
|
||||
return SendStats{}, err
|
||||
}
|
||||
stats.WindowDays = windowDays
|
||||
stats.WindowHours = hours
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
// AppSendStats returns send statistics for an application login.
|
||||
func (s *Store) AppSendStats(login string, retentionDays int, createdAt time.Time) (SendStats, error) {
|
||||
since, hours, windowDays := StatsWindow(retentionDays, createdAt)
|
||||
stats, err := s.sendStats("app_login", login, since, hours)
|
||||
if err != nil {
|
||||
return SendStats{}, err
|
||||
}
|
||||
stats.WindowDays = windowDays
|
||||
stats.WindowHours = hours
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
func (s *Store) sendStats(column, ref string, since time.Time, hours float64) (SendStats, error) {
|
||||
sinceStr := since.UTC().Format(time.RFC3339)
|
||||
|
||||
var total int64
|
||||
err := s.db.QueryRow(
|
||||
`SELECT COUNT(DISTINCT queue_id) FROM send_log
|
||||
WHERE `+column+` = ? AND status != ? AND created_at >= ?`,
|
||||
ref, StatusRejected, sinceStr,
|
||||
).Scan(&total)
|
||||
if err != nil {
|
||||
return SendStats{}, fmt.Errorf("send stats total %s %q: %w", column, ref, err)
|
||||
}
|
||||
|
||||
var peak int64
|
||||
err = s.db.QueryRow(
|
||||
`SELECT COALESCE(MAX(bucket_count), 0) FROM (
|
||||
SELECT COUNT(DISTINCT queue_id) AS bucket_count FROM send_log
|
||||
WHERE `+column+` = ? AND status != ? AND created_at >= ?
|
||||
GROUP BY substr(created_at, 1, 13)
|
||||
)`,
|
||||
ref, StatusRejected, sinceStr,
|
||||
).Scan(&peak)
|
||||
if err != nil {
|
||||
return SendStats{}, fmt.Errorf("send stats peak %s %q: %w", column, ref, err)
|
||||
}
|
||||
|
||||
avg := float64(total) / hours
|
||||
return SendStats{Total: total, PeakPerHour: peak, AvgPerHour: avg}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user