This is an automated email from the ASF dual-hosted git repository.
klesh pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/devlake.git
The following commit(s) were added to refs/heads/main by this push:
new ca4601646 fix(tempo): page through all teams in the teams API and
scope search (#9321)
ca4601646 is described below
commit ca4601646a401680b6dc80b3d221f7a39fdbd5ca
Author: Shivam Sharma <[email protected]>
AuthorDate: Fri Nov 6 12:31:35 2026 +0530
fix(tempo): page through all teams in the teams API and scope search (#9321)
GetTeams decoded the GET /4/teams response into a slice, but Tempo v4
returns a page object (self, metadata, results), so
GET /plugins/tempo/connections/:id/teams always failed with "failed to
unmarshal teams response". searchTempoRemoteTeams sent a single request
without offset/limit, so it only searched the first 50 teams returned.
Add listAllTempoTeams, which follows metadata.next through every page,
and use it in both.
Closes #9320
Generated-by: Claude Code (Claude Opus 5.5)
Signed-off-by: Shivam Sharma
<[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
---
backend/plugins/tempo/api/connection.go | 10 +----
backend/plugins/tempo/api/remote.go | 58 +++++++++++++++++++--------
backend/plugins/tempo/api/remote_test.go | 68 ++++++++++++++++++++++++++++++--
3 files changed, 107 insertions(+), 29 deletions(-)
diff --git a/backend/plugins/tempo/api/connection.go
b/backend/plugins/tempo/api/connection.go
index 12d8e4cd7..7f329507b 100644
--- a/backend/plugins/tempo/api/connection.go
+++ b/backend/plugins/tempo/api/connection.go
@@ -190,15 +190,9 @@ func GetTeams(input *plugin.ApiResourceInput)
(*plugin.ApiResourceOutput, errors
}
// Get teams from Tempo API
- res, err := apiClient.Get("teams", nil, nil)
+ teams, err := listAllTempoTeams(apiClient, nil)
if err != nil {
- return nil, errors.Default.Wrap(err, "failed to get teams from
Tempo API")
- }
-
- var teams []models.TempoTeamResponse
- err = api.UnmarshalResponse(res, &teams)
- if err != nil {
- return nil, errors.Default.Wrap(err, "failed to unmarshal teams
response")
+ return nil, err
}
// Convert to tool layer models
diff --git a/backend/plugins/tempo/api/remote.go
b/backend/plugins/tempo/api/remote.go
index 40b2f53b7..93181cc50 100644
--- a/backend/plugins/tempo/api/remote.go
+++ b/backend/plugins/tempo/api/remote.go
@@ -95,6 +95,42 @@ func listTempoRemoteTeams(
return children, nextPage, nil
}
+// listAllTempoTeams fetches every page of GET /4/teams. Tempo v4 pagination
+// metadata has no total: "next" links the next page and is absent on the last.
+func listAllTempoTeams(apiClient plugin.ApiClient, query url.Values)
([]models.TempoTeamResponse, errors.Error) {
+ const pageSize = 50
+ var teams []models.TempoTeamResponse
+ for offset := 0; ; offset += pageSize {
+ pageQuery := url.Values{}
+ for key, values := range query {
+ pageQuery[key] = values
+ }
+ pageQuery.Set("offset", strconv.Itoa(offset))
+ pageQuery.Set("limit", strconv.Itoa(pageSize))
+
+ res, err := apiClient.Get("teams", pageQuery, nil)
+ if err != nil {
+ return nil, errors.Default.Wrap(err, "failed to get
teams from Tempo API")
+ }
+
+ var response struct {
+ Metadata struct {
+ Next string `json:"next"`
+ } `json:"metadata"`
+ Results []models.TempoTeamResponse `json:"results"`
+ }
+ err = api.UnmarshalResponse(res, &response)
+ if err != nil {
+ return nil, errors.Default.Wrap(err, "failed to
unmarshal teams response")
+ }
+
+ teams = append(teams, response.Results...)
+ if response.Metadata.Next == "" || len(response.Results) == 0 {
+ return teams, nil
+ }
+ }
+}
+
// RemoteScopes list all available scopes on the remote server
// @Summary list all available scopes on the remote server
// @Description list all available scopes on the remote server
@@ -125,26 +161,14 @@ func searchTempoRemoteTeams(
}
}
- res, err := apiClient.Get("teams", queryParams, nil)
+ // all matching teams, not just the first page of the API: the result is
+ // filtered and paged below
+ teams, err := listAllTempoTeams(apiClient, queryParams)
if err != nil {
- return nil, errors.Default.Wrap(err, "failed to get teams from
Tempo API")
+ return nil, err
}
- var response struct {
- Metadata struct {
- Count int `json:"count"`
- Limit int `json:"limit"`
- Offset int `json:"offset"`
- Total int `json:"total"`
- } `json:"metadata"`
- Results []models.TempoTeamResponse `json:"results"`
- }
- err = api.UnmarshalResponse(res, &response)
- if err != nil {
- return nil, errors.Default.Wrap(err, "failed to unmarshal teams
response")
- }
-
- for _, team := range response.Results {
+ for _, team := range teams {
if params.Search == "" ||
strings.Contains(strings.ToLower(team.Name), strings.ToLower(params.Search)) {
children = append(children,
dsmodels.DsRemoteApiScopeListEntry[models.TempoTeam]{
Type: api.RAS_ENTRY_TYPE_SCOPE,
diff --git a/backend/plugins/tempo/api/remote_test.go
b/backend/plugins/tempo/api/remote_test.go
index c0aee4031..d13a28b0b 100644
--- a/backend/plugins/tempo/api/remote_test.go
+++ b/backend/plugins/tempo/api/remote_test.go
@@ -26,20 +26,34 @@ import (
"github.com/apache/devlake/core/errors"
"github.com/apache/devlake/core/plugin"
+ dsmodels "github.com/apache/devlake/helpers/pluginhelper/api/models"
"github.com/apache/devlake/plugins/tempo/models"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
-// fakeTempoClient answers every GET with the same body and records the query.
+// fakeTempoClient answers every GET with the same body, or with the body of
+// the requested offset (0 by default) when pages is set, and records the
+// queries.
type fakeTempoClient struct {
- body string
- query url.Values
+ body string
+ pages map[string]string
+ query url.Values
+ queries []url.Values
}
func (c *fakeTempoClient) Get(_ string, query url.Values, _ http.Header)
(*http.Response, errors.Error) {
c.query = query
- return &http.Response{StatusCode: http.StatusOK, Body:
io.NopCloser(strings.NewReader(c.body))}, nil
+ c.queries = append(c.queries, query)
+ body := c.body
+ if c.pages != nil {
+ offset := query.Get("offset")
+ if offset == "" {
+ offset = "0"
+ }
+ body = c.pages[offset]
+ }
+ return &http.Response{StatusCode: http.StatusOK, Body:
io.NopCloser(strings.NewReader(body))}, nil
}
func (c *fakeTempoClient) Post(string, url.Values, interface{}, http.Header)
(*http.Response, errors.Error) {
@@ -92,3 +106,49 @@ func TestListTempoRemoteTeamsPagination(t *testing.T) {
assert.Nil(t, nextPage)
})
}
+
+// two pages of GET /4/teams (PageableTeam): the first links the second
+var twoTeamPages = map[string]string{
+ "0": `{
+ "self": "https://api.tempo.io/4/teams?offset=0&limit=50",
+ "metadata": {"count": 2, "offset": 0, "limit": 50,
+ "next":
"https://api.tempo.io/4/teams?offset=50&limit=50"},
+ "results": [
+ {"id": 17, "name": "Backend", "summary": "", "key": ""},
+ {"id": 71, "name": "Frontend", "summary": "", "key": ""}
+ ]
+ }`,
+ "50": `{
+ "self": "https://api.tempo.io/4/teams?offset=50&limit=50",
+ "metadata": {"count": 1, "offset": 50, "limit": 50,
+ "previous":
"https://api.tempo.io/4/teams?offset=0&limit=50"},
+ "results": [{"id": 90, "name": "Backend QA", "summary": "",
"key": ""}]
+ }`,
+}
+
+func TestListAllTempoTeams(t *testing.T) {
+ client := &fakeTempoClient{pages: twoTeamPages}
+ teams, err := listAllTempoTeams(client, url.Values{"name": {"x"}})
+ require.NoError(t, err)
+ require.Len(t, teams, 3)
+ assert.Equal(t, int64(17), teams[0].Id)
+ assert.Equal(t, int64(90), teams[2].Id)
+ require.Len(t, client.queries, 2)
+ assert.Equal(t, "0", client.queries[0].Get("offset"))
+ assert.Equal(t, "50", client.queries[1].Get("offset"))
+ assert.Equal(t, "50", client.queries[1].Get("limit"))
+ assert.Equal(t, "x", client.queries[1].Get("name"))
+}
+
+func TestSearchTempoRemoteTeamsSearchesEveryPage(t *testing.T) {
+ client := &fakeTempoClient{pages: twoTeamPages}
+ children, err := searchTempoRemoteTeams(client,
&dsmodels.DsRemoteApiScopeSearchParams{
+ Search: "backend", Page: 1, PageSize: 50,
+ })
+ require.NoError(t, err)
+ require.Len(t, children, 2)
+ assert.Equal(t, "17", children[0].Id)
+ // on the second page of the Tempo API
+ assert.Equal(t, "90", children[1].Id)
+ assert.Equal(t, "backend", client.query.Get("name"))
+}