From 138879ef5e417d513cfaf868eb01a055b4707eef Mon Sep 17 00:00:00 2001 From: Pablo BC Date: Wed, 7 Oct 2026 17:45:01 +0200 Subject: [PATCH] fix(tempo): send "to" with "from" when collecting worklogs Since #9164 a full sync asks Tempo for worklogs from timeAfter with no end date. Tempo rejects that period on every page: {"errors":[{"message":"Non valid period, from '2026-01-28' to 'null'"}]} (400) so full syncs (and the first run of each team after upgrading to beta18, whose collector state starts fresh) fail in collect_worklogs. Whenever the query has "from" and no "to", set "to" to the date the collection started (one value for every page). Incremental runs keep using updatedFrom, which Tempo accepts on its own. --- .../plugins/tempo/tasks/worklog_collector.go | 12 ++++++--- .../tempo/tasks/worklog_collector_test.go | 25 +++++++++++++------ 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/backend/plugins/tempo/tasks/worklog_collector.go b/backend/plugins/tempo/tasks/worklog_collector.go index 0571956ec9f..3e56b9a653c 100644 --- a/backend/plugins/tempo/tasks/worklog_collector.go +++ b/backend/plugins/tempo/tasks/worklog_collector.go @@ -64,6 +64,8 @@ func CollectWorklogs(taskCtx plugin.SubTaskContext) errors.Error { urlTemplate = fmt.Sprintf("worklogs/team/%d", data.Options.TeamId) } + // one period end for every page, even if the collection crosses midnight + startedAt := time.Now() err = apiCollector.InitCollector(api.ApiCollectorArgs{ RawDataSubTaskArgs: rawDataSubTaskArgs, ApiClient: data.ApiClient, @@ -74,7 +76,7 @@ func CollectWorklogs(taskCtx plugin.SubTaskContext) errors.Error { // fetched until one comes back short. Query: func(reqData *api.RequestData) (url.Values, errors.Error) { return buildWorklogQuery(data.Options, reqData.Pager, - apiCollector.IsIncremental(), apiCollector.GetSince()), nil + apiCollector.IsIncremental(), apiCollector.GetSince(), startedAt), nil }, ResponseParser: func(res *http.Response) ([]json.RawMessage, errors.Error) { var response struct { @@ -98,8 +100,9 @@ func CollectWorklogs(taskCtx plugin.SubTaskContext) errors.Error { // buildWorklogQuery builds the query for one page of worklogs. Explicit // fromDate/toDate options win; otherwise an incremental run asks for what // changed since the last successful collection (updatedFrom) and a full sync -// starts at the sync policy's timeAfter (from). -func buildWorklogQuery(opts *TempoOptions, pager *api.Pager, incremental bool, since *time.Time) url.Values { +// starts at the sync policy's timeAfter (from). Tempo rejects a period with +// "from" but no "to" ("Non valid period ... to 'null'"), so "to" defaults to now. +func buildWorklogQuery(opts *TempoOptions, pager *api.Pager, incremental bool, since *time.Time, now time.Time) url.Values { if pager == nil { pager = &api.Pager{Page: 1, Skip: 0, Size: 1000} } @@ -120,5 +123,8 @@ func buildWorklogQuery(opts *TempoOptions, pager *api.Pager, incremental bool, s case since != nil: query.Set("from", since.UTC().Format("2006-01-02")) } + if query.Get("from") != "" && query.Get("to") == "" { + query.Set("to", now.UTC().Format("2006-01-02")) + } return query } diff --git a/backend/plugins/tempo/tasks/worklog_collector_test.go b/backend/plugins/tempo/tasks/worklog_collector_test.go index d484d0d8869..6fef72997d0 100644 --- a/backend/plugins/tempo/tasks/worklog_collector_test.go +++ b/backend/plugins/tempo/tasks/worklog_collector_test.go @@ -29,41 +29,50 @@ func TestBuildWorklogQuery(t *testing.T) { pager := &api.Pager{Page: 3, Skip: 2000, Size: 1000} timeAfter := time.Date(2026, 1, 28, 0, 0, 0, 0, time.UTC) lastSuccess := time.Date(2026, 9, 22, 0, 0, 13, 0, time.UTC) + now := time.Date(2026, 10, 7, 15, 0, 0, 0, time.UTC) t.Run("pages with offset and limit", func(t *testing.T) { - q := buildWorklogQuery(&TempoOptions{TeamId: 4}, pager, false, nil) + q := buildWorklogQuery(&TempoOptions{TeamId: 4}, pager, false, nil, now) assert.Equal(t, "2000", q.Get("offset")) assert.Equal(t, "1000", q.Get("limit")) }) - t.Run("full sync starts at timeAfter, without a rolling window", func(t *testing.T) { - q := buildWorklogQuery(&TempoOptions{TeamId: 4}, pager, false, &timeAfter) + t.Run("full sync covers timeAfter up to today, without a rolling window", func(t *testing.T) { + // Tempo rejects "from" without "to": "Non valid period, from '2026-01-28' to 'null'" (400) + q := buildWorklogQuery(&TempoOptions{TeamId: 4}, pager, false, &timeAfter, now) assert.Equal(t, "2026-01-28", q.Get("from")) - assert.Empty(t, q.Get("to")) + assert.Equal(t, "2026-10-07", q.Get("to")) assert.Empty(t, q.Get("updatedFrom")) }) t.Run("incremental sync fetches what changed since the last success", func(t *testing.T) { - q := buildWorklogQuery(&TempoOptions{TeamId: 4}, pager, true, &lastSuccess) + q := buildWorklogQuery(&TempoOptions{TeamId: 4}, pager, true, &lastSuccess, now) assert.Equal(t, "2026-09-22T00:00:13Z", q.Get("updatedFrom")) assert.Empty(t, q.Get("from")) }) t.Run("global endpoint gets the same bounds", func(t *testing.T) { - q := buildWorklogQuery(&TempoOptions{}, pager, false, &timeAfter) + q := buildWorklogQuery(&TempoOptions{}, pager, false, &timeAfter, now) assert.Equal(t, "2026-01-28", q.Get("from")) + assert.Equal(t, "2026-10-07", q.Get("to")) + }) + + t.Run("a fromDate option alone also ends today", func(t *testing.T) { + q := buildWorklogQuery(&TempoOptions{TeamId: 4, FromDate: "2026-03-01"}, pager, false, nil, now) + assert.Equal(t, "2026-03-01", q.Get("from")) + assert.Equal(t, "2026-10-07", q.Get("to")) }) t.Run("explicit fromDate/toDate options win", func(t *testing.T) { opts := &TempoOptions{TeamId: 4, FromDate: "2026-03-01", ToDate: "2026-03-31"} - q := buildWorklogQuery(opts, pager, true, &lastSuccess) + q := buildWorklogQuery(opts, pager, true, &lastSuccess, now) assert.Equal(t, "2026-03-01", q.Get("from")) assert.Equal(t, "2026-03-31", q.Get("to")) assert.Empty(t, q.Get("updatedFrom")) }) t.Run("no bounds when nothing is known", func(t *testing.T) { - q := buildWorklogQuery(&TempoOptions{TeamId: 4}, pager, false, nil) + q := buildWorklogQuery(&TempoOptions{TeamId: 4}, pager, false, nil, now) assert.Empty(t, q.Get("from")) assert.Empty(t, q.Get("updatedFrom")) })