Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "0.129.0"
".": "0.130.0"
}
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Changelog

## [0.130.0](https://github.com/kernel/kernel-go-sdk/compare/v0.129.0...v0.130.0) (2026-10-24)


### Features

* Support Link recurring spend requests on vault Link cards ([0f094b3](https://github.com/kernel/kernel-go-sdk/commit/0f094b3f2d251c535aecd71101bbf5841cb18277))

## [0.129.0](https://github.com/kernel/kernel-go-sdk/compare/v0.128.0...v0.129.0) (2026-10-21)


Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Or to pin the version:
<!-- x-release-please-start-version -->

```sh
go get -u 'github.com/kernel/kernel-go-sdk@v0.129.0'
go get -u 'github.com/kernel/kernel-go-sdk@v0.130.0'
```

<!-- x-release-please-end -->
Expand Down
2 changes: 2 additions & 0 deletions api.md
Original file line number Diff line number Diff line change
Expand Up @@ -597,6 +597,8 @@ Response Types:
- <a href="https://pkg.go.dev/github.com/kernel/kernel-go-sdk">kernel</a>.<a href="https://pkg.go.dev/github.com/kernel/kernel-go-sdk#KernelCredentialVaultItemState">KernelCredentialVaultItemState</a>
- <a href="https://pkg.go.dev/github.com/kernel/kernel-go-sdk">kernel</a>.<a href="https://pkg.go.dev/github.com/kernel/kernel-go-sdk#KernelWalletState">KernelWalletState</a>
- <a href="https://pkg.go.dev/github.com/kernel/kernel-go-sdk">kernel</a>.<a href="https://pkg.go.dev/github.com/kernel/kernel-go-sdk#KernelWalletVaultItemSpec">KernelWalletVaultItemSpec</a>
- <a href="https://pkg.go.dev/github.com/kernel/kernel-go-sdk">kernel</a>.<a href="https://pkg.go.dev/github.com/kernel/kernel-go-sdk#LinkShippingAddress">LinkShippingAddress</a>
- <a href="https://pkg.go.dev/github.com/kernel/kernel-go-sdk">kernel</a>.<a href="https://pkg.go.dev/github.com/kernel/kernel-go-sdk#LinkUserInfo">LinkUserInfo</a>
- <a href="https://pkg.go.dev/github.com/kernel/kernel-go-sdk">kernel</a>.<a href="https://pkg.go.dev/github.com/kernel/kernel-go-sdk#ManagedAuthCredentialVaultField">ManagedAuthCredentialVaultField</a>
- <a href="https://pkg.go.dev/github.com/kernel/kernel-go-sdk">kernel</a>.<a href="https://pkg.go.dev/github.com/kernel/kernel-go-sdk#ManagedAuthCredentialVaultItemSpec">ManagedAuthCredentialVaultItemSpec</a>
- <a href="https://pkg.go.dev/github.com/kernel/kernel-go-sdk">kernel</a>.<a href="https://pkg.go.dev/github.com/kernel/kernel-go-sdk#ManagedAuthCredentialVaultItemState">ManagedAuthCredentialVaultItemState</a>
Expand Down
2 changes: 1 addition & 1 deletion internal/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@

package internal

const PackageVersion = "0.129.0" // x-release-please-version
const PackageVersion = "0.130.0" // x-release-please-version
35 changes: 23 additions & 12 deletions packages/pagination/pagination.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,26 +139,27 @@ func (r *OffsetPagination[T]) UnmarshalJSON(data []byte) error {
// there is no next page, this function will return a 'nil' for the page value, but
// will not return an error
func (r *OffsetPagination[T]) GetNextPage() (res *OffsetPagination[T], err error) {
if len(r.Items) == 0 {
return nil, nil
}
cfg := r.cfg.Clone(r.cfg.Context)

nextStr := r.res.Header.Get("X-Next-Offset")
if nextStr == "" && len(r.Items) == 0 {
return nil, nil
}
next, err := strconv.ParseInt(nextStr, 10, 64)
if err != nil {
return nil, err
}
length := int64(len(r.Items))

if length > 0 && next != 0 {
err = cfg.Apply(option.WithQuery("offset", strconv.FormatInt(next, 10)))
if err != nil {
return nil, err
}
} else {
// An empty page can still have more pages behind it. Cursors can move in
// either direction, so only 0 or a repeated cursor ends pagination.
requested, _ := strconv.ParseInt(cfg.Request.URL.Query().Get("offset"), 10, 64)
if next == 0 || next == requested {
return nil, nil
}
err = cfg.Apply(option.WithQuery("offset", strconv.FormatInt(next, 10)))
if err != nil {
return nil, err
}
var raw *http.Response
cfg.ResponseInto = &raw
cfg.ResponseBodyInto = &res
Expand Down Expand Up @@ -195,13 +196,23 @@ func NewOffsetPaginationAutoPager[T any](page *OffsetPagination[T], err error) *
}

func (r *OffsetPaginationAutoPager[T]) Next() bool {
if r.page == nil || len(r.page.Items) == 0 {
if r.page == nil {
return false
}
if r.idx >= len(r.page.Items) {
r.idx = 0
r.page, r.err = r.page.GetNextPage()
if r.err != nil || r.page == nil || len(r.page.Items) == 0 {
if r.err != nil || r.page == nil {
return false
}
}
// if the API returned empty data then keep iterating
// until we either get more data or there are no more pages
// to fetch
for len(r.page.Items) == 0 {
r.idx = 0
r.page, r.err = r.page.GetNextPage()
if r.err != nil || r.page == nil {
return false
}
}
Expand Down
126 changes: 126 additions & 0 deletions pagination_empty_page_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package kernel_test

import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"testing"

"github.com/kernel/kernel-go-sdk"
"github.com/kernel/kernel-go-sdk/option"
)

type emptyPageResponse struct {
body string
headers map[string]string
}

// countEventsAcrossPages auto-pages the telemetry events endpoint against a
// server that serves pages keyed by the request's offset query value.
func countEventsAcrossPages(t *testing.T, pages map[string]emptyPageResponse) (events, requests int, err error) {
t.Helper()
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requests++
page, ok := pages[r.URL.Query().Get("offset")]
if !ok {
t.Errorf("unexpected request for offset %q", r.URL.Query().Get("offset"))
http.Error(w, "unexpected offset", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
for k, v := range page.headers {
w.Header().Set(k, v)
}
fmt.Fprint(w, page.body)
}))
defer srv.Close()

client := kernel.NewClient(option.WithBaseURL(srv.URL), option.WithAPIKey("test-api-key"), option.WithMaxRetries(0))
iter := client.Browsers.Telemetry.EventsAutoPaging(context.Background(), "browser", kernel.BrowserTelemetryEventsParams{})
for iter.Next() {
events++
}
return events, requests, iter.Err()
}

const emptyPageEvent = `{"seq":1,"event":{"ts":1,"type":"page_load","category":"page","source":{"kind":"kernel_api"},"data":{}}}`

func TestOffsetPaginationContinuesPastEmptyPages(t *testing.T) {
events, requests, err := countEventsAcrossPages(t, map[string]emptyPageResponse{
"": {"[" + emptyPageEvent + "]", map[string]string{"X-Has-More": "true", "X-Next-Offset": "20"}},
"20": {"[]", map[string]string{"X-Has-More": "true", "X-Next-Offset": "40"}},
"40": {"[]", map[string]string{"X-Has-More": "true", "X-Next-Offset": "60"}},
"60": {"[" + emptyPageEvent + "]", map[string]string{"X-Has-More": "false", "X-Next-Offset": "0"}},
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if events != 2 || requests != 4 {
t.Fatalf("got %d events in %d requests, want 2 events in 4 requests", events, requests)
}
}

func TestOffsetPaginationEmptyFirstPageContinues(t *testing.T) {
events, requests, err := countEventsAcrossPages(t, map[string]emptyPageResponse{
"": {"[]", map[string]string{"X-Has-More": "true", "X-Next-Offset": "20"}},
"20": {"[" + emptyPageEvent + "]", map[string]string{"X-Has-More": "false", "X-Next-Offset": "0"}},
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if events != 1 || requests != 2 {
t.Fatalf("got %d events in %d requests, want 1 event in 2 requests", events, requests)
}
}

func TestOffsetPaginationStopsOnEmptyLastPage(t *testing.T) {
events, requests, err := countEventsAcrossPages(t, map[string]emptyPageResponse{
"": {"[]", map[string]string{"X-Has-More": "false", "X-Next-Offset": "0"}},
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if events != 0 || requests != 1 {
t.Fatalf("got %d events in %d requests, want 0 events in 1 request", events, requests)
}
}

func TestOffsetPaginationStopsOnEmptyPageWithoutCursor(t *testing.T) {
events, requests, err := countEventsAcrossPages(t, map[string]emptyPageResponse{
"": {"[]", nil},
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if events != 0 || requests != 1 {
t.Fatalf("got %d events in %d requests, want 0 events in 1 request", events, requests)
}
}

func TestOffsetPaginationStopsWhenCursorDoesNotAdvance(t *testing.T) {
events, requests, err := countEventsAcrossPages(t, map[string]emptyPageResponse{
"": {"[" + emptyPageEvent + "]", map[string]string{"X-Has-More": "true", "X-Next-Offset": "20"}},
"20": {"[]", map[string]string{"X-Has-More": "true", "X-Next-Offset": "20"}},
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if events != 1 || requests != 2 {
t.Fatalf("got %d events in %d requests, want 1 event in 2 requests", events, requests)
}
}

func TestOffsetPaginationFollowsDescendingCursorAcrossEmptyPage(t *testing.T) {
events, requests, err := countEventsAcrossPages(t, map[string]emptyPageResponse{
"": {"[" + emptyPageEvent + "]", map[string]string{"X-Has-More": "true", "X-Next-Offset": "300"}},
"300": {"[]", map[string]string{"X-Has-More": "true", "X-Next-Offset": "200"}},
"200": {"[" + emptyPageEvent + "]", map[string]string{"X-Has-More": "false", "X-Next-Offset": "0"}},
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if events != 2 || requests != 3 {
t.Fatalf("got %d events in %d requests, want 2 events in 3 requests", events, requests)
}
}
Loading
Loading