From 6d3eb33ee586efd171e9775d47c09094a307bacc Mon Sep 17 00:00:00 2001 From: Poul Clementsen Date: Fri, 30 Oct 2026 14:21:04 +0100 Subject: [PATCH] fix(claude_code): resume incremental collection from the last collected day --- .../claude_code/tasks/collector_utils.go | 7 ++- .../claude_code/tasks/collector_utils_test.go | 51 +++++++++++++++++-- 2 files changed, 53 insertions(+), 5 deletions(-) diff --git a/backend/plugins/claude_code/tasks/collector_utils.go b/backend/plugins/claude_code/tasks/collector_utils.go index fbf52764e00..9aa1916401e 100644 --- a/backend/plugins/claude_code/tasks/collector_utils.go +++ b/backend/plugins/claude_code/tasks/collector_utils.go @@ -82,12 +82,14 @@ func utcDate(t time.Time) time.Time { // computeUsageDateRange returns the [start, until] date range for incremental collection. // The Anthropic Analytics API only serves data from 2026-01-01 onwards and requires // dates to be at least three days old, so the current day and previous two days are skipped. +// since is the start time of the last successful run, so collection resumes from the last +// day that run collected. func computeUsageDateRange(now time.Time, since *time.Time) (start, until time.Time) { apiFloor := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) until = utcDate(now).AddDate(0, 0, -claudeCodeAvailabilityLagDays) start = until.AddDate(0, 0, -(claudeCodeInitialBackfillDays - 1)) if since != nil { - start = utcDate(*since).AddDate(0, 0, 1) + start = utcDate(*since).AddDate(0, 0, -claudeCodeAvailabilityLagDays) minStart := until.AddDate(0, 0, -(claudeCodeInitialBackfillDays - 1)) if start.Before(minStart) { start = minStart @@ -96,6 +98,9 @@ func computeUsageDateRange(now time.Time, since *time.Time) (start, until time.T if start.Before(apiFloor) { start = apiFloor } + if start.After(until) { + start = until + } return start, until } diff --git a/backend/plugins/claude_code/tasks/collector_utils_test.go b/backend/plugins/claude_code/tasks/collector_utils_test.go index 86ba3165679..b66ea059cbe 100644 --- a/backend/plugins/claude_code/tasks/collector_utils_test.go +++ b/backend/plugins/claude_code/tasks/collector_utils_test.go @@ -42,14 +42,57 @@ func TestComputeUsageDateRangeHonorsApiFloor(t *testing.T) { assert.Equal(t, time.Date(2026, 3, 16, 0, 0, 0, 0, time.UTC), until) } -func TestComputeUsageDateRangeProducesEmptyWindowInsideLag(t *testing.T) { +func TestComputeUsageDateRangeResumesFromLastCollectedDay(t *testing.T) { now := time.Date(2026, 6, 19, 14, 30, 0, 0, time.UTC) since := time.Date(2026, 6, 16, 12, 0, 0, 0, time.UTC) start, until := computeUsageDateRange(now, &since) - assert.Equal(t, time.Date(2026, 6, 17, 0, 0, 0, 0, time.UTC), start) + assert.Equal(t, time.Date(2026, 6, 13, 0, 0, 0, 0, time.UTC), start) assert.Equal(t, time.Date(2026, 6, 16, 0, 0, 0, 0, time.UTC), until) - assert.False(t, newClaudeCodeDayIterator(start, until).HasNext()) - assert.False(t, newClaudeCodeDateRangeIterator(start, until, claudeCodeSummaryMaxDays).HasNext()) +} + +func TestComputeUsageDateRangeCollectsWhenSinceIsNow(t *testing.T) { + now := time.Date(2026, 9, 10, 12, 31, 0, 0, time.UTC) + + start, until := computeUsageDateRange(now, &now) + + assert.Equal(t, time.Date(2026, 9, 7, 0, 0, 0, 0, time.UTC), start) + assert.Equal(t, time.Date(2026, 9, 7, 0, 0, 0, 0, time.UTC), until) + assert.True(t, newClaudeCodeDayIterator(start, until).HasNext()) + assert.True(t, newClaudeCodeDateRangeIterator(start, until, claudeCodeSummaryMaxDays).HasNext()) +} + +func TestComputeUsageDateRangeCapsBackfillHorizon(t *testing.T) { + now := time.Date(2026, 9, 10, 12, 31, 0, 0, time.UTC) + since := time.Date(2026, 2, 1, 0, 0, 0, 0, time.UTC) + + start, until := computeUsageDateRange(now, &since) + + assert.Equal(t, time.Date(2026, 6, 10, 0, 0, 0, 0, time.UTC), start) + assert.Equal(t, time.Date(2026, 9, 7, 0, 0, 0, 0, time.UTC), until) +} + +func TestComputeUsageDateRangeCollectsEveryDayAcrossRuns(t *testing.T) { + first := time.Date(2026, 8, 6, 7, 0, 0, 0, time.UTC) + for _, interval := range []int{1, 2, 3, 7} { + collected := map[time.Time]bool{} + var since *time.Time + var now time.Time + for run := 0; run < 6; run++ { + now = first.AddDate(0, 0, run*interval) + start, until := computeUsageDateRange(now, since) + for d := start; !d.After(until); d = d.AddDate(0, 0, 1) { + collected[d] = true + } + runStart := now + since = &runStart + } + + start, _ := computeUsageDateRange(first, nil) + _, until := computeUsageDateRange(now, nil) + for d := start; !d.After(until); d = d.AddDate(0, 0, 1) { + assert.Truef(t, collected[d], "interval %d: %s not collected", interval, d.Format(claudeCodeDateLayout)) + } + } }