0.3.49
This commit is contained in:
@@ -59,6 +59,53 @@ func previousMonth(now time.Time, location *time.Location) (from, to time.Time,
|
||||
return from, to, from.Format("2006-01")
|
||||
}
|
||||
|
||||
// priorWeekToDate is last week measured to the same point this week has reached: the local
|
||||
// Monday a week ago, up to the same weekday and wall-clock time as now. AddDate keeps the
|
||||
// wall clock across a daylight-saving change, so "the same time last week" stays the same
|
||||
// time rather than drifting an hour — the reason weekStartIn works in dates too.
|
||||
func priorWeekToDate(now time.Time, location *time.Location) (from, to time.Time) {
|
||||
from = weekStartIn(now, location).AddDate(0, 0, -7)
|
||||
to = now.In(location).AddDate(0, 0, -7)
|
||||
return from, to
|
||||
}
|
||||
|
||||
// watchWeekTrendFloor is how far this week's figure can sit from last week's before the
|
||||
// difference is worth showing. The band is the larger of this and a tenth of last week —
|
||||
// an absolute floor so a light household is not told "down" over a few minutes, and a
|
||||
// proportion so a heavy one is not told "steady" over an hour.
|
||||
const watchWeekTrendFloor = 5 * time.Minute
|
||||
|
||||
// watchWeekTrend is this week-to-date against the same point last week: a direction the
|
||||
// console prints beside the figure, plus the numbers behind it. Direction is "up", "down",
|
||||
// "steady" or "none" — the last when there is nothing to compare, the runtimestats
|
||||
// DirectionUnknown stance.
|
||||
type watchWeekTrend struct {
|
||||
Direction string `json:"direction"`
|
||||
PriorMs int64 `json:"priorMs"`
|
||||
DeltaMs int64 `json:"deltaMs"`
|
||||
}
|
||||
|
||||
// weekOverWeek classifies this week's watch time against last week's to-date figure. Pure,
|
||||
// so the console and its tests agree on where "steady" ends.
|
||||
func weekOverWeek(currentMs, priorMs int64) watchWeekTrend {
|
||||
if currentMs <= 0 && priorMs <= 0 {
|
||||
return watchWeekTrend{Direction: "none"}
|
||||
}
|
||||
delta := currentMs - priorMs
|
||||
band := priorMs / 10
|
||||
if floor := watchWeekTrendFloor.Milliseconds(); band < floor {
|
||||
band = floor
|
||||
}
|
||||
direction := "steady"
|
||||
switch {
|
||||
case delta > band:
|
||||
direction = "up"
|
||||
case delta < -band:
|
||||
direction = "down"
|
||||
}
|
||||
return watchWeekTrend{Direction: direction, PriorMs: priorMs, DeltaMs: delta}
|
||||
}
|
||||
|
||||
// weekKey names a week the way the source key needs it — a stable string that changes exactly
|
||||
// once per week. ISO year-and-week, so the last days of December cannot collide with the
|
||||
// first days of January.
|
||||
@@ -143,14 +190,18 @@ func monthlyDigestMessage(month time.Duration, monthName, topTitle string) strin
|
||||
// different facts, and a console that showed both as a row of zeroes would leave an operator
|
||||
// investigating a viewer rather than an integration.
|
||||
type watchTimeSummary struct {
|
||||
Matched bool `json:"matched"`
|
||||
Username string `json:"tracearrUsername,omitempty"`
|
||||
WeekMs int64 `json:"weekMs"`
|
||||
MonthMs int64 `json:"monthMs"`
|
||||
TotalMs int64 `json:"totalMs"`
|
||||
WeekSessions int `json:"weekSessions"`
|
||||
MonthSessions int `json:"monthSessions"`
|
||||
LastWatchedAt *time.Time `json:"lastWatchedAt,omitempty"`
|
||||
Matched bool `json:"matched"`
|
||||
Username string `json:"tracearrUsername,omitempty"`
|
||||
WeekMs int64 `json:"weekMs"`
|
||||
// WeekTrend compares WeekMs with the same point last week, so the console can show
|
||||
// whether a person's viewing is up or down without the operator holding last week's
|
||||
// figure in their head.
|
||||
WeekTrend watchWeekTrend `json:"weekTrend"`
|
||||
MonthMs int64 `json:"monthMs"`
|
||||
TotalMs int64 `json:"totalMs"`
|
||||
WeekSessions int `json:"weekSessions"`
|
||||
MonthSessions int `json:"monthSessions"`
|
||||
LastWatchedAt *time.Time `json:"lastWatchedAt,omitempty"`
|
||||
}
|
||||
|
||||
// tracearrEnabled is whether there is anything to read at all. Every watch-time reader checks
|
||||
@@ -220,16 +271,16 @@ type watchTimeAccount struct {
|
||||
func (s *Server) watchTimeForAccounts(
|
||||
ctx context.Context,
|
||||
accounts []watchTimeAccount,
|
||||
) map[string]store.WatchTimeTotals {
|
||||
) (totals map[string]store.WatchTimeTotals, priorWeekMs map[string]int64) {
|
||||
if !s.tracearrEnabled() || len(accounts) == 0 {
|
||||
return nil
|
||||
return nil, nil
|
||||
}
|
||||
location := s.householdLocation()
|
||||
now := time.Now()
|
||||
totals, err := s.store.TracearrWatchTime(ctx, weekStartIn(now, location), monthStartIn(now, location))
|
||||
rows, err := s.store.TracearrWatchTime(ctx, weekStartIn(now, location), monthStartIn(now, location))
|
||||
if err != nil {
|
||||
s.loggerFor(ctx).Warn("watch time read failed", "error", err)
|
||||
return nil
|
||||
return nil, nil
|
||||
}
|
||||
identities, err := s.store.TracearrIdentities(ctx)
|
||||
if err != nil {
|
||||
@@ -238,18 +289,37 @@ func (s *Server) watchTimeForAccounts(
|
||||
s.loggerFor(ctx).Warn("Tracearr identity map unavailable", "error", err)
|
||||
identities = map[string]store.RecommendationIdentity{}
|
||||
}
|
||||
return attributeWatchTime(accounts, totals, identities)
|
||||
attributed := attributeWatchTime(accounts, rows, identities)
|
||||
|
||||
// Last week to the same point, attributed the same two ways, so the console can say
|
||||
// whether viewing is up or down. A failure here costs the arrow, never the figures.
|
||||
priorWeek := map[string]int64{}
|
||||
priorFrom, priorTo := priorWeekToDate(now, location)
|
||||
windows, err := s.store.TracearrWatchTimeRange(ctx, priorFrom, priorTo)
|
||||
if err != nil {
|
||||
s.loggerFor(ctx).Warn("prior-week watch time read failed", "error", err)
|
||||
} else {
|
||||
byID, byName := indexWatchTimeRanges(windows)
|
||||
for _, account := range accounts {
|
||||
window := lookupWatchTimeRange(byID, byName, identities[account.ID], account.Username)
|
||||
priorWeek[account.ID] = window.Ms
|
||||
}
|
||||
}
|
||||
return attributed, priorWeek
|
||||
}
|
||||
|
||||
// summariseWatchTime turns the store's row into what the console reads, including the case
|
||||
// where there is no row.
|
||||
func summariseWatchTime(totals store.WatchTimeTotals, matched bool) watchTimeSummary {
|
||||
// where there is no row. priorWeekMs is last week's viewing to the same point, for the
|
||||
// week-over-week arrow — zero when there is nothing recorded, which weekOverWeek reads as
|
||||
// "no change to show" rather than as a fall to nothing.
|
||||
func summariseWatchTime(totals store.WatchTimeTotals, matched bool, priorWeekMs int64) watchTimeSummary {
|
||||
if !matched {
|
||||
return watchTimeSummary{}
|
||||
}
|
||||
return watchTimeSummary{
|
||||
Matched: true, Username: totals.Username,
|
||||
WeekMs: totals.WeekMs, MonthMs: totals.MonthMs, TotalMs: totals.TotalMs,
|
||||
WeekMs: totals.WeekMs, WeekTrend: weekOverWeek(totals.WeekMs, priorWeekMs),
|
||||
MonthMs: totals.MonthMs, TotalMs: totals.TotalMs,
|
||||
WeekSessions: totals.WeekSessions, MonthSessions: totals.MonthSessions,
|
||||
LastWatchedAt: totals.LastWatchedAt,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user