Skip to content
Open
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
4 changes: 2 additions & 2 deletions cmd/api_keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (

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

key, err := c.apiKeys.Get(ctx, in.ID)
key, err := c.apiKeys.Get(ctx, in.ID, kernel.APIKeyGetParams{})
if err != nil {
return util.CleanedUpSdkError{Err: err}
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/api_keys_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ func (f *FakeAPIKeysService) New(ctx context.Context, body kernel.APIKeyNewParam
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
}

func (f *FakeAPIKeysService) Get(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.APIKey, error) {
func (f *FakeAPIKeysService) Get(ctx context.Context, id string, query kernel.APIKeyGetParams, opts ...option.RequestOption) (*kernel.APIKey, error) {
if f.GetFunc != nil {
return f.GetFunc(ctx, id, opts...)
}
Expand Down
10 changes: 10 additions & 0 deletions cmd/browsers.go
Original file line number Diff line number Diff line change
Expand Up @@ -2749,6 +2749,16 @@ followed automatically by Chromium.`,
telemetryStream.Flags().Int64("seq", -1, "Resume after sequence number N (Last-Event-ID); replays events with seq > N. Default -1 streams from now")
telemetryStream.Flags().StringP("output", "o", "", "Output format: json for newline-delimited JSON envelopes")
telemetryRoot.AddCommand(telemetryStream)

telemetryEvents := &cobra.Command{Use: "events <id>", Short: "Read recorded telemetry events", Args: cobra.ExactArgs(1), RunE: runBrowsersTelemetryEvents}
telemetryEvents.Flags().Int64("limit", 0, "Max events per page (1-100, default 20)")
telemetryEvents.Flags().String("since", "", "Window start: RFC-3339 timestamp or duration like 5m (default 5m)")
telemetryEvents.Flags().String("until", "", "Window end (exclusive): RFC-3339 timestamp or duration like 5m")
telemetryEvents.Flags().StringSlice("categories", []string{}, "Filter by event category (console,network,page,interaction,control,connection,system,screenshot,captcha,monitor)")
telemetryEvents.Flags().Bool("all", false, "Fetch every page instead of just the first")
telemetryEvents.Flags().StringP("output", "o", "", "Output format: json for newline-delimited JSON envelopes")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inline JSON output flag

Low Severity

The new telemetry events command registers -o/--output with a hand-rolled StringP and custom help instead of addJSONOutputFlag, which centralizes the flag and Output format: json for raw API response wording across CLI commands.

Fix in Cursor Fix in Web

Triggered by learned rule: Use shared JSON output helpers in CLI commands

Reviewed by Cursor Bugbot for commit d18dca1. Configure here.

telemetryRoot.AddCommand(telemetryEvents)

browsersCmd.AddCommand(telemetryRoot)

// no flags for view; it takes a single positional argument
Expand Down
104 changes: 103 additions & 1 deletion cmd/browsers_telemetry.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,17 @@ import (
"github.com/kernel/cli/pkg/util"
kernel "github.com/kernel/kernel-go-sdk"
"github.com/kernel/kernel-go-sdk/option"
"github.com/kernel/kernel-go-sdk/packages/pagination"
"github.com/kernel/kernel-go-sdk/packages/ssestream"
"github.com/pterm/pterm"
"github.com/spf13/cobra"
)

// BrowserTelemetryService defines the subset we use for browser telemetry streaming.
// BrowserTelemetryService defines the subset we use for browser telemetry.
type BrowserTelemetryService interface {
StreamStreaming(ctx context.Context, id string, query kernel.BrowserTelemetryStreamParams, opts ...option.RequestOption) (stream *ssestream.Stream[kernel.BrowserTelemetryStreamResponse])
Events(ctx context.Context, id string, query kernel.BrowserTelemetryEventsParams, opts ...option.RequestOption) (*pagination.OffsetPagination[kernel.BrowserTelemetryEventsResponse], error)
EventsAutoPaging(ctx context.Context, id string, query kernel.BrowserTelemetryEventsParams, opts ...option.RequestOption) *pagination.OffsetPaginationAutoPager[kernel.BrowserTelemetryEventsResponse]
}

type BrowsersTelemetryStreamInput struct {
Expand Down Expand Up @@ -232,3 +235,102 @@ func runBrowsersTelemetryStream(cmd *cobra.Command, args []string) error {
Output: out,
})
}

type BrowsersTelemetryEventsInput struct {
Identifier string
Limit int64
Since string
Until string
Categories []string
All bool
Output string
}

func (b BrowsersCmd) TelemetryEvents(ctx context.Context, in BrowsersTelemetryEventsInput) error {
if b.telemetry == nil {
return fmt.Errorf("telemetry service not available")
}
if err := validateJSONOutput(in.Output); err != nil {
return err
}
for _, c := range in.Categories {
if !slices.Contains(streamFilterCategories, c) {
return fmt.Errorf("invalid --categories value %q: must be one of %s", c, strings.Join(streamFilterCategories, ", "))
}
}

params := kernel.BrowserTelemetryEventsParams{}
if in.Limit > 0 {
params.Limit = kernel.Opt(in.Limit)
}
if in.Since != "" {
params.Since = kernel.Opt(in.Since)
}
if in.Until != "" {
params.Until = kernel.Opt(in.Until)
}
if len(in.Categories) > 0 {
params.Category = in.Categories
}

br, err := b.browsers.Get(ctx, in.Identifier, kernel.BrowserGetParams{})
if err != nil {
return util.CleanedUpSdkError{Err: err}
}

emit := func(ev kernel.BrowserTelemetryEventsResponse) error {
if in.Output == "json" {
return util.PrintCompactJSONLine(ev)
}
ts := time.UnixMicro(ev.Event.Ts).Local().Format("2006-01-02 15:04:05")
pterm.Printf("%s\t%d\t[%s]\t%s\n", ts, ev.Seq, ev.Event.Category, ev.Event.Type)
return nil
}

if in.All {
pager := b.telemetry.EventsAutoPaging(ctx, br.SessionID, params)
for pager.Next() {
if err := emit(pager.Current()); err != nil {
return err
}
}
if err := pager.Err(); err != nil {
return util.CleanedUpSdkError{Err: err}
}
return nil
}

page, err := b.telemetry.Events(ctx, br.SessionID, params)
if err != nil {
return util.CleanedUpSdkError{Err: err}
}
if page != nil {
for i := range page.Items {
if err := emit(page.Items[i]); err != nil {
return err
}
}
}
return nil
}

func runBrowsersTelemetryEvents(cmd *cobra.Command, args []string) error {
client := getKernelClient(cmd)
svc := client.Browsers
out, _ := cmd.Flags().GetString("output")
limit, _ := cmd.Flags().GetInt64("limit")
since, _ := cmd.Flags().GetString("since")
until, _ := cmd.Flags().GetString("until")
categories, _ := cmd.Flags().GetStringSlice("categories")
all, _ := cmd.Flags().GetBool("all")
b := BrowsersCmd{browsers: &svc, telemetry: &svc.Telemetry}
return b.TelemetryEvents(cmd.Context(), BrowsersTelemetryEventsInput{
Identifier: args[0],
Limit: limit,
Since: since,
Until: until,
Categories: categories,
All: all,
Output: out,
})
}
126 changes: 125 additions & 1 deletion cmd/browsers_telemetry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"os"
"testing"

kernel "github.com/kernel/kernel-go-sdk"
"github.com/kernel/kernel-go-sdk/option"
"github.com/kernel/kernel-go-sdk/packages/pagination"
"github.com/kernel/kernel-go-sdk/packages/ssestream"
"github.com/stretchr/testify/assert"
)
Expand All @@ -33,7 +35,9 @@ func captureStdout(t *testing.T, fn func()) string {
}

type FakeBrowserTelemetryService struct {
StreamFunc func() *ssestream.Stream[kernel.BrowserTelemetryStreamResponse]
StreamFunc func() *ssestream.Stream[kernel.BrowserTelemetryStreamResponse]
EventsFunc func(id string, query kernel.BrowserTelemetryEventsParams) (*pagination.OffsetPagination[kernel.BrowserTelemetryEventsResponse], error)
EventsAutoPagingFunc func() *pagination.OffsetPaginationAutoPager[kernel.BrowserTelemetryEventsResponse]
}

func (f *FakeBrowserTelemetryService) StreamStreaming(ctx context.Context, id string, query kernel.BrowserTelemetryStreamParams, opts ...option.RequestOption) *ssestream.Stream[kernel.BrowserTelemetryStreamResponse] {
Expand All @@ -43,6 +47,126 @@ func (f *FakeBrowserTelemetryService) StreamStreaming(ctx context.Context, id st
return makeStream([]kernel.BrowserTelemetryStreamResponse{})
}

func (f *FakeBrowserTelemetryService) Events(ctx context.Context, id string, query kernel.BrowserTelemetryEventsParams, opts ...option.RequestOption) (*pagination.OffsetPagination[kernel.BrowserTelemetryEventsResponse], error) {
if f.EventsFunc != nil {
return f.EventsFunc(id, query)
}
return &pagination.OffsetPagination[kernel.BrowserTelemetryEventsResponse]{}, nil
}

func (f *FakeBrowserTelemetryService) EventsAutoPaging(ctx context.Context, id string, query kernel.BrowserTelemetryEventsParams, opts ...option.RequestOption) *pagination.OffsetPaginationAutoPager[kernel.BrowserTelemetryEventsResponse] {
if f.EventsAutoPagingFunc != nil {
return f.EventsAutoPagingFunc()
}
return nil
}

func telemetryEventsPage(t *testing.T, raws ...string) *pagination.OffsetPagination[kernel.BrowserTelemetryEventsResponse] {
t.Helper()
items := make([]kernel.BrowserTelemetryEventsResponse, 0, len(raws))
for _, raw := range raws {
var ev kernel.BrowserTelemetryEventsResponse
if err := json.Unmarshal([]byte(raw), &ev); err != nil {
t.Fatalf("unmarshal: %v", err)
}
items = append(items, ev)
}
return &pagination.OffsetPagination[kernel.BrowserTelemetryEventsResponse]{Items: items}
}

func TestTelemetryEvents_NilTelemetryErrors(t *testing.T) {
b := BrowsersCmd{browsers: &FakeBrowsersService{}}

err := b.TelemetryEvents(context.Background(), BrowsersTelemetryEventsInput{Identifier: "session123"})

assert.Error(t, err)
assert.Contains(t, err.Error(), "telemetry service not available")
}

func TestTelemetryEvents_UnsupportedOutputErrors(t *testing.T) {
b := BrowsersCmd{browsers: &FakeBrowsersService{}, telemetry: &FakeBrowserTelemetryService{}}

err := b.TelemetryEvents(context.Background(), BrowsersTelemetryEventsInput{Identifier: "session123", Output: "yaml"})

assert.Error(t, err)
assert.Contains(t, err.Error(), "unsupported --output value")
}

func TestTelemetryEvents_UnknownCategoryErrors(t *testing.T) {
b := BrowsersCmd{browsers: &FakeBrowsersService{}, telemetry: &FakeBrowserTelemetryService{}}

err := b.TelemetryEvents(context.Background(), BrowsersTelemetryEventsInput{Identifier: "session123", Categories: []string{"netowrk"}})

assert.Error(t, err)
assert.Contains(t, err.Error(), "invalid --categories value")
}

func TestTelemetryEvents_SinglePageTextAndParams(t *testing.T) {
setupStdoutCapture(t)
fakeBrowsers := &FakeBrowsersService{GetFunc: func(ctx context.Context, id string, query kernel.BrowserGetParams, opts ...option.RequestOption) (*kernel.BrowserGetResponse, error) {
return &kernel.BrowserGetResponse{SessionID: id}, nil
}}
var gotID string
var gotQuery kernel.BrowserTelemetryEventsParams
fakeTelemetry := &FakeBrowserTelemetryService{EventsFunc: func(id string, query kernel.BrowserTelemetryEventsParams) (*pagination.OffsetPagination[kernel.BrowserTelemetryEventsResponse], error) {
gotID, gotQuery = id, query
return telemetryEventsPage(t,
`{"event":{"type":"network_response","category":"network","ts":1000000},"seq":7}`,
`{"event":{"type":"network_request","category":"network","ts":2000000},"seq":8}`,
), nil
}}
b := BrowsersCmd{browsers: fakeBrowsers, telemetry: fakeTelemetry}

err := b.TelemetryEvents(context.Background(), BrowsersTelemetryEventsInput{
Identifier: "session123",
Limit: 5,
Since: "5m",
Until: "2020-01-01T00:00:00Z",
Categories: []string{"network"},
})

assert.NoError(t, err)
assert.Equal(t, "session123", gotID)
assert.Equal(t, int64(5), gotQuery.Limit.Value)
assert.Equal(t, "5m", gotQuery.Since.Value)
assert.Equal(t, "2020-01-01T00:00:00Z", gotQuery.Until.Value)
assert.Equal(t, []string{"network"}, gotQuery.Category)
out := outBuf.String()
assert.Contains(t, out, "network_response")
assert.Contains(t, out, "network_request")
assert.Contains(t, out, "7")
assert.Contains(t, out, "8")
}

func TestTelemetryEvents_SinglePageJSON(t *testing.T) {
fakeBrowsers := &FakeBrowsersService{GetFunc: func(ctx context.Context, id string, query kernel.BrowserGetParams, opts ...option.RequestOption) (*kernel.BrowserGetResponse, error) {
return &kernel.BrowserGetResponse{SessionID: id}, nil
}}
fakeTelemetry := &FakeBrowserTelemetryService{EventsFunc: func(id string, query kernel.BrowserTelemetryEventsParams) (*pagination.OffsetPagination[kernel.BrowserTelemetryEventsResponse], error) {
return telemetryEventsPage(t, `{"event":{"type":"network_response","ts":1000000},"seq":1}`), nil
}}
b := BrowsersCmd{browsers: fakeBrowsers, telemetry: fakeTelemetry}

var err error
out := captureStdout(t, func() {
err = b.TelemetryEvents(context.Background(), BrowsersTelemetryEventsInput{Identifier: "session123", Output: "json"})
})

assert.NoError(t, err)
assert.Contains(t, out, "network_response")
}

func TestTelemetryEvents_GetErrorSurfaces(t *testing.T) {
fakeBrowsers := &FakeBrowsersService{GetFunc: func(ctx context.Context, id string, query kernel.BrowserGetParams, opts ...option.RequestOption) (*kernel.BrowserGetResponse, error) {
return nil, fmt.Errorf("boom")
}}
b := BrowsersCmd{browsers: fakeBrowsers, telemetry: &FakeBrowserTelemetryService{}}

err := b.TelemetryEvents(context.Background(), BrowsersTelemetryEventsInput{Identifier: "session123"})

assert.Error(t, err)
}

func TestTelemetryStream_NilTelemetryErrors(t *testing.T) {
b := BrowsersCmd{browsers: &FakeBrowsersService{}}

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ require (
github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1
github.com/golang-jwt/jwt/v5 v5.2.2
github.com/joho/godotenv v1.5.1
github.com/kernel/kernel-go-sdk v0.66.0
github.com/kernel/kernel-go-sdk v0.70.0
github.com/klauspost/compress v1.18.5
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
github.com/pterm/pterm v0.12.80
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/kernel/kernel-go-sdk v0.66.0 h1:pn+fSHHo4fJ4kYm8uOkF5J2rj6k1FC6NqlLzoxy2jy4=
github.com/kernel/kernel-go-sdk v0.66.0/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ=
github.com/kernel/kernel-go-sdk v0.70.0 h1:tgg8suA8R9Y4ZLiby0jNc0KJ1KqHpqg+a4l0sJkZJVM=
github.com/kernel/kernel-go-sdk v0.70.0/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ=
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
Expand Down
Loading