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")) })