Skip to content

Commit f327c67

Browse files
IlyaasKclaude
andcommitted
Adopt kernel-go-sdk v0.70.0; add extensions get + telemetry events
- Bump kernel-go-sdk v0.66.0 -> v0.70.0; adapt to APIKeyService.Get's new APIKeyGetParams arg (interface + call site + fake). - projects get: pass the identifier to ProjectService.Get so the server resolves id-or-name (drops the client-side list-and-scan; delete/limits keep resolveProjectArg since those endpoints don't resolve names). - Add 'extensions get <id-or-name>' (ExtensionService.Get metadata incl. last_used_at), 'browsers telemetry events <id>' (paged read with --limit/--offset/--since/--until + X-Next-Offset paging hint), and a '--replay' flag on 'telemetry stream' (validated to all, mutually exclusive with --seq). - Tests for the new commands + updated service fakes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent d52581b commit f327c67

10 files changed

Lines changed: 265 additions & 15 deletions

cmd/api_keys.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import (
1414

1515
type APIKeysService interface {
1616
New(ctx context.Context, body kernel.APIKeyNewParams, opts ...option.RequestOption) (*kernel.CreatedAPIKey, error)
17-
Get(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.APIKey, error)
17+
Get(ctx context.Context, id string, query kernel.APIKeyGetParams, opts ...option.RequestOption) (*kernel.APIKey, error)
1818
Update(ctx context.Context, id string, body kernel.APIKeyUpdateParams, opts ...option.RequestOption) (*kernel.APIKey, error)
1919
List(ctx context.Context, query kernel.APIKeyListParams, opts ...option.RequestOption) (*pagination.OffsetPagination[kernel.APIKey], error)
2020
Delete(ctx context.Context, id string, opts ...option.RequestOption) error
@@ -145,7 +145,7 @@ func (c APIKeysCmd) Get(ctx context.Context, in APIKeysGetInput) error {
145145
return err
146146
}
147147

148-
key, err := c.apiKeys.Get(ctx, in.ID)
148+
key, err := c.apiKeys.Get(ctx, in.ID, kernel.APIKeyGetParams{})
149149
if err != nil {
150150
return util.CleanedUpSdkError{Err: err}
151151
}

cmd/api_keys_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import (
1616

1717
type FakeAPIKeysService struct {
1818
NewFunc func(ctx context.Context, body kernel.APIKeyNewParams, opts ...option.RequestOption) (*kernel.CreatedAPIKey, error)
19-
GetFunc func(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.APIKey, error)
19+
GetFunc func(ctx context.Context, id string, query kernel.APIKeyGetParams, opts ...option.RequestOption) (*kernel.APIKey, error)
2020
UpdateFunc func(ctx context.Context, id string, body kernel.APIKeyUpdateParams, opts ...option.RequestOption) (*kernel.APIKey, error)
2121
ListFunc func(ctx context.Context, query kernel.APIKeyListParams, opts ...option.RequestOption) (*pagination.OffsetPagination[kernel.APIKey], error)
2222
DeleteFunc func(ctx context.Context, id string, opts ...option.RequestOption) error
@@ -29,9 +29,9 @@ func (f *FakeAPIKeysService) New(ctx context.Context, body kernel.APIKeyNewParam
2929
return createdAPIKeyFromJSON(`{"id":"key_123","name":"default","key":"sk_test","masked_key":"sk_...test","created_at":"2026-05-27T12:00:00Z","created_by":{"id":"user_123","email":"dev@example.com","name":"Dev"},"expires_at":null,"project_id":null,"project_name":null}`), nil
3030
}
3131

32-
func (f *FakeAPIKeysService) Get(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.APIKey, error) {
32+
func (f *FakeAPIKeysService) Get(ctx context.Context, id string, query kernel.APIKeyGetParams, opts ...option.RequestOption) (*kernel.APIKey, error) {
3333
if f.GetFunc != nil {
34-
return f.GetFunc(ctx, id, opts...)
34+
return f.GetFunc(ctx, id, query, opts...)
3535
}
3636
return apiKeyFromJSON(`{"id":"` + id + `","name":"default","masked_key":"sk_...test","created_at":"2026-05-27T12:00:00Z","created_by":{"id":"user_123","email":"dev@example.com","name":"Dev"},"expires_at":null,"project_id":null,"project_name":null}`), nil
3737
}
@@ -125,7 +125,7 @@ func TestAPIKeysRejectInvalidOutputBeforeCallingAPI(t *testing.T) {
125125
t.Fatal("New should not be called")
126126
return nil, nil
127127
},
128-
GetFunc: func(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.APIKey, error) {
128+
GetFunc: func(ctx context.Context, id string, query kernel.APIKeyGetParams, opts ...option.RequestOption) (*kernel.APIKey, error) {
129129
t.Fatal("Get should not be called")
130130
return nil, nil
131131
},

cmd/browsers.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2748,7 +2748,17 @@ followed automatically by Chromium.`,
27482748
telemetryStream.Flags().StringSlice("types", []string{}, "Filter by event type (e.g. network_response,console_error)")
27492749
telemetryStream.Flags().Int64("seq", -1, "Resume after sequence number N (Last-Event-ID); replays events with seq > N. Default -1 streams from now")
27502750
telemetryStream.Flags().StringP("output", "o", "", "Output format: json for newline-delimited JSON envelopes")
2751+
telemetryStream.Flags().String("replay", "", "Replay buffered events on connect: --replay=all starts from the oldest retained event")
2752+
telemetryStream.MarkFlagsMutuallyExclusive("seq", "replay")
27512753
telemetryRoot.AddCommand(telemetryStream)
2754+
2755+
telemetryEvents := &cobra.Command{Use: "events <id>", Short: "Read historical telemetry events (paged)", Args: cobra.ExactArgs(1), RunE: runBrowsersTelemetryEvents}
2756+
telemetryEvents.Flags().Int64("limit", 0, "Maximum number of events per page (default 20)")
2757+
telemetryEvents.Flags().Int64("offset", 0, "Pagination cursor: pass the X-Next-Offset from a previous response")
2758+
telemetryEvents.Flags().String("since", "", "Window start: RFC-3339 timestamp or a duration like 5m (default 5m). Ignored when --offset is set")
2759+
telemetryEvents.Flags().String("until", "", "Window end (exclusive): RFC-3339 timestamp or a duration like 5m")
2760+
telemetryEvents.Flags().StringP("output", "o", "", "Output format: json")
2761+
telemetryRoot.AddCommand(telemetryEvents)
27522762
browsersCmd.AddCommand(telemetryRoot)
27532763

27542764
// no flags for view; it takes a single positional argument

cmd/browsers_telemetry.go

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"errors"
66
"fmt"
7+
"net/http"
78
"os"
89
"os/signal"
910
"slices"
@@ -15,6 +16,7 @@ import (
1516
"github.com/kernel/cli/pkg/util"
1617
kernel "github.com/kernel/kernel-go-sdk"
1718
"github.com/kernel/kernel-go-sdk/option"
19+
"github.com/kernel/kernel-go-sdk/packages/pagination"
1820
"github.com/kernel/kernel-go-sdk/packages/ssestream"
1921
"github.com/pterm/pterm"
2022
"github.com/spf13/cobra"
@@ -23,13 +25,24 @@ import (
2325
// BrowserTelemetryService defines the subset we use for browser telemetry streaming.
2426
type BrowserTelemetryService interface {
2527
StreamStreaming(ctx context.Context, id string, query kernel.BrowserTelemetryStreamParams, opts ...option.RequestOption) (stream *ssestream.Stream[kernel.BrowserTelemetryStreamResponse])
28+
Events(ctx context.Context, id string, query kernel.BrowserTelemetryEventsParams, opts ...option.RequestOption) (res *pagination.OffsetPagination[kernel.BrowserTelemetryEventsResponse], err error)
2629
}
2730

2831
type BrowsersTelemetryStreamInput struct {
2932
Identifier string
3033
Categories []string
3134
Types []string
3235
Seq int64
36+
Replay string
37+
Output string
38+
}
39+
40+
type BrowsersTelemetryEventsInput struct {
41+
Identifier string
42+
Limit int64
43+
Offset int64
44+
Since string
45+
Until string
3346
Output string
3447
}
3548

@@ -180,6 +193,9 @@ func (b BrowsersCmd) TelemetryStream(ctx context.Context, in BrowsersTelemetrySt
180193
return fmt.Errorf("invalid --categories value %q: must be one of %s", c, strings.Join(streamFilterCategories, ", "))
181194
}
182195
}
196+
if in.Replay != "" && in.Replay != "all" {
197+
return fmt.Errorf("invalid --replay value %q: only \"all\" is supported (omit --replay to stream from now)", in.Replay)
198+
}
183199
ctx, stop := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM)
184200
defer stop()
185201
br, err := b.browsers.Get(ctx, in.Identifier, kernel.BrowserGetParams{})
@@ -190,6 +206,9 @@ func (b BrowsersCmd) TelemetryStream(ctx context.Context, in BrowsersTelemetrySt
190206
if in.Seq >= 0 {
191207
params.LastEventID = kernel.Opt(strconv.FormatInt(in.Seq, 10))
192208
}
209+
if in.Replay != "" {
210+
params.Replay = kernel.Opt(in.Replay)
211+
}
193212
stream := b.telemetry.StreamStreaming(ctx, br.SessionID, params)
194213
defer stream.Close()
195214
for stream.Next() {
@@ -223,12 +242,105 @@ func runBrowsersTelemetryStream(cmd *cobra.Command, args []string) error {
223242
categories, _ := cmd.Flags().GetStringSlice("categories")
224243
types, _ := cmd.Flags().GetStringSlice("types")
225244
seq, _ := cmd.Flags().GetInt64("seq")
245+
replay, _ := cmd.Flags().GetString("replay")
226246
b := BrowsersCmd{browsers: &svc, telemetry: &svc.Telemetry}
227247
return b.TelemetryStream(cmd.Context(), BrowsersTelemetryStreamInput{
228248
Identifier: args[0],
229249
Categories: categories,
230250
Types: types,
231251
Seq: seq,
252+
Replay: replay,
253+
Output: out,
254+
})
255+
}
256+
257+
func (b BrowsersCmd) TelemetryEvents(ctx context.Context, in BrowsersTelemetryEventsInput) error {
258+
if b.telemetry == nil {
259+
return fmt.Errorf("telemetry service not available")
260+
}
261+
if err := validateJSONOutput(in.Output); err != nil {
262+
return err
263+
}
264+
265+
br, err := b.browsers.Get(ctx, in.Identifier, kernel.BrowserGetParams{})
266+
if err != nil {
267+
return util.CleanedUpSdkError{Err: err}
268+
}
269+
270+
params := kernel.BrowserTelemetryEventsParams{}
271+
if in.Limit > 0 {
272+
params.Limit = kernel.Opt(in.Limit)
273+
}
274+
if in.Offset > 0 {
275+
params.Offset = kernel.Opt(in.Offset)
276+
}
277+
if in.Since != "" {
278+
params.Since = kernel.Opt(in.Since)
279+
}
280+
if in.Until != "" {
281+
params.Until = kernel.Opt(in.Until)
282+
}
283+
284+
var raw *http.Response
285+
page, err := b.telemetry.Events(ctx, br.SessionID, params, option.WithResponseInto(&raw))
286+
if err != nil {
287+
return util.CleanedUpSdkError{Err: err}
288+
}
289+
290+
var items []kernel.BrowserTelemetryEventsResponse
291+
if page != nil {
292+
items = page.Items
293+
}
294+
295+
if in.Output == "json" {
296+
if len(items) == 0 {
297+
fmt.Println("[]")
298+
return nil
299+
}
300+
return util.PrintPrettyJSONSlice(items)
301+
}
302+
303+
if len(items) == 0 {
304+
pterm.Info.Println("No telemetry events found")
305+
return nil
306+
}
307+
308+
rows := pterm.TableData{{"Seq", "Time", "Category", "Type"}}
309+
for _, it := range items {
310+
ts := time.UnixMicro(it.Event.Ts).Local().Format("2006-01-02 15:04:05")
311+
rows = append(rows, []string{
312+
strconv.FormatInt(it.Seq, 10),
313+
ts,
314+
it.Event.Category,
315+
it.Event.Type,
316+
})
317+
}
318+
PrintTableNoPad(rows, true)
319+
// The next-page cursor is the opaque X-Next-Offset header; surface it so
320+
// --offset is actually usable for paging.
321+
if raw != nil {
322+
if next := raw.Header.Get("X-Next-Offset"); next != "" && next != "0" {
323+
pterm.Info.Printf("More events available — re-run with --offset %s\n", next)
324+
}
325+
}
326+
return nil
327+
}
328+
329+
func runBrowsersTelemetryEvents(cmd *cobra.Command, args []string) error {
330+
client := getKernelClient(cmd)
331+
svc := client.Browsers
332+
out, _ := cmd.Flags().GetString("output")
333+
limit, _ := cmd.Flags().GetInt64("limit")
334+
offset, _ := cmd.Flags().GetInt64("offset")
335+
since, _ := cmd.Flags().GetString("since")
336+
until, _ := cmd.Flags().GetString("until")
337+
b := BrowsersCmd{browsers: &svc, telemetry: &svc.Telemetry}
338+
return b.TelemetryEvents(cmd.Context(), BrowsersTelemetryEventsInput{
339+
Identifier: args[0],
340+
Limit: limit,
341+
Offset: offset,
342+
Since: since,
343+
Until: until,
232344
Output: out,
233345
})
234346
}

cmd/browsers_telemetry_test.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010

1111
kernel "github.com/kernel/kernel-go-sdk"
1212
"github.com/kernel/kernel-go-sdk/option"
13+
"github.com/kernel/kernel-go-sdk/packages/pagination"
1314
"github.com/kernel/kernel-go-sdk/packages/ssestream"
1415
"github.com/stretchr/testify/assert"
1516
)
@@ -34,6 +35,7 @@ func captureStdout(t *testing.T, fn func()) string {
3435

3536
type FakeBrowserTelemetryService struct {
3637
StreamFunc func() *ssestream.Stream[kernel.BrowserTelemetryStreamResponse]
38+
EventsFunc func(ctx context.Context, id string, query kernel.BrowserTelemetryEventsParams, opts ...option.RequestOption) (*pagination.OffsetPagination[kernel.BrowserTelemetryEventsResponse], error)
3739
}
3840

3941
func (f *FakeBrowserTelemetryService) StreamStreaming(ctx context.Context, id string, query kernel.BrowserTelemetryStreamParams, opts ...option.RequestOption) *ssestream.Stream[kernel.BrowserTelemetryStreamResponse] {
@@ -43,6 +45,13 @@ func (f *FakeBrowserTelemetryService) StreamStreaming(ctx context.Context, id st
4345
return makeStream([]kernel.BrowserTelemetryStreamResponse{})
4446
}
4547

48+
func (f *FakeBrowserTelemetryService) Events(ctx context.Context, id string, query kernel.BrowserTelemetryEventsParams, opts ...option.RequestOption) (*pagination.OffsetPagination[kernel.BrowserTelemetryEventsResponse], error) {
49+
if f.EventsFunc != nil {
50+
return f.EventsFunc(ctx, id, query, opts...)
51+
}
52+
return &pagination.OffsetPagination[kernel.BrowserTelemetryEventsResponse]{}, nil
53+
}
54+
4655
func TestTelemetryStream_NilTelemetryErrors(t *testing.T) {
4756
b := BrowsersCmd{browsers: &FakeBrowsersService{}}
4857

@@ -350,3 +359,46 @@ func TestTelemetryEnabledCategories(t *testing.T) {
350359
}
351360
assert.Equal(t, []string{"control", "system"}, telemetryEnabledCategories(cfg))
352361
}
362+
363+
func TestTelemetryStream_RejectsInvalidReplay(t *testing.T) {
364+
b := BrowsersCmd{browsers: &FakeBrowsersService{}, telemetry: &FakeBrowserTelemetryService{}}
365+
err := b.TelemetryStream(context.Background(), BrowsersTelemetryStreamInput{Identifier: "br-1", Seq: -1, Replay: "oldest"})
366+
assert.Error(t, err)
367+
assert.Contains(t, err.Error(), "invalid --replay")
368+
}
369+
370+
func TestTelemetryEvents_Table(t *testing.T) {
371+
buf := capturePtermOutput(t)
372+
fakeBrowsers := &FakeBrowsersService{GetFunc: func(ctx context.Context, id string, query kernel.BrowserGetParams, opts ...option.RequestOption) (*kernel.BrowserGetResponse, error) {
373+
return &kernel.BrowserGetResponse{SessionID: "sess-1"}, nil
374+
}}
375+
fakeTelemetry := &FakeBrowserTelemetryService{
376+
EventsFunc: func(ctx context.Context, id string, query kernel.BrowserTelemetryEventsParams, opts ...option.RequestOption) (*pagination.OffsetPagination[kernel.BrowserTelemetryEventsResponse], error) {
377+
assert.Equal(t, "sess-1", id, "events should query the resolved session id")
378+
return &pagination.OffsetPagination[kernel.BrowserTelemetryEventsResponse]{
379+
Items: []kernel.BrowserTelemetryEventsResponse{
380+
{Seq: 7, Event: kernel.BrowserTelemetryEventUnion{Category: "network", Type: "network_response", Ts: 0}},
381+
},
382+
}, nil
383+
},
384+
}
385+
b := BrowsersCmd{browsers: fakeBrowsers, telemetry: fakeTelemetry}
386+
err := b.TelemetryEvents(context.Background(), BrowsersTelemetryEventsInput{Identifier: "br-1"})
387+
assert.NoError(t, err)
388+
out := buf.String()
389+
assert.Contains(t, out, "7")
390+
assert.Contains(t, out, "network")
391+
assert.Contains(t, out, "network_response")
392+
}
393+
394+
func TestTelemetryEvents_EmptyJSON(t *testing.T) {
395+
out := captureStdout(t, func() {
396+
fakeBrowsers := &FakeBrowsersService{GetFunc: func(ctx context.Context, id string, query kernel.BrowserGetParams, opts ...option.RequestOption) (*kernel.BrowserGetResponse, error) {
397+
return &kernel.BrowserGetResponse{SessionID: "sess-1"}, nil
398+
}}
399+
b := BrowsersCmd{browsers: fakeBrowsers, telemetry: &FakeBrowserTelemetryService{}}
400+
err := b.TelemetryEvents(context.Background(), BrowsersTelemetryEventsInput{Identifier: "br-1", Output: "json"})
401+
assert.NoError(t, err)
402+
})
403+
assert.Equal(t, "[]\n", out)
404+
}

0 commit comments

Comments
 (0)