Skip to content
Closed
Show file tree
Hide file tree
Changes from 6 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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,7 @@ Per-category updates are partial — only categories you name are changed; other
- `--categories <list>` - Filter by event category (`console`, `network`, `page`, `interaction`, `control`, `connection`, `system`, `screenshot`, `captcha`, `monitor`)
- `--types <list>` - Filter by event type (e.g. `network_response`, `console_error`)
- `--seq <n>` - Resume after sequence number N (Last-Event-ID); replays events with `seq > N`. Omit to stream from now.
- `--replay-all` - Replay from the oldest retained event instead of streaming from now. Mutually exclusive with `--seq`. The buffer is bounded, so the first event may not be `seq 1` if older events were evicted.
- `-o, --output json` - Output newline-delimited JSON envelopes
- Default output: tab-separated `<time>\t[<category>]\t<type>`, e.g. `15:04:05 [network] network_response`

Expand Down
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
1 change: 1 addition & 0 deletions cmd/browsers.go
Original file line number Diff line number Diff line change
Expand Up @@ -2747,6 +2747,7 @@ followed automatically by Chromium.`,
telemetryStream.Flags().StringSlice("categories", []string{}, "Filter by event category (console,network,page,interaction,control,connection,system,screenshot,captcha,monitor)")
telemetryStream.Flags().StringSlice("types", []string{}, "Filter by event type (e.g. network_response,console_error)")
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().Bool("replay-all", false, "Replay from the oldest retained event instead of streaming from now. Mutually exclusive with --seq")
telemetryStream.Flags().StringP("output", "o", "", "Output format: json for newline-delimited JSON envelopes")
telemetryRoot.AddCommand(telemetryStream)
browsersCmd.AddCommand(telemetryRoot)
Expand Down
16 changes: 15 additions & 1 deletion cmd/browsers_telemetry.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,17 @@ type BrowserTelemetryService interface {
StreamStreaming(ctx context.Context, id string, query kernel.BrowserTelemetryStreamParams, opts ...option.RequestOption) (stream *ssestream.Stream[kernel.BrowserTelemetryStreamResponse])
}

// replayAllValue is the telemetry stream `replay` query value that starts the
// stream from the oldest retained event. It must match the value the browser
// image matches on; there is no generated enum tying the two together.
const replayAllValue = "all"

type BrowsersTelemetryStreamInput struct {
Identifier string
Categories []string
Types []string
Seq int64
ReplayAll bool
Output string
}

Expand Down Expand Up @@ -172,6 +178,9 @@ func (b BrowsersCmd) TelemetryStream(ctx context.Context, in BrowsersTelemetrySt
if err := validateJSONOutput(in.Output); err != nil {
return err
}
if in.ReplayAll && in.Seq != -1 {
return fmt.Errorf("cannot combine --replay-all with --seq: --replay-all starts from the oldest retained event, --seq resumes after a specific sequence")
}
if in.Seq != -1 && in.Seq < 1 {
return fmt.Errorf("invalid --seq value %d: must be >= 1 (resumes after sequence N; omit --seq to stream from now)", in.Seq)
}
Expand All @@ -187,8 +196,11 @@ func (b BrowsersCmd) TelemetryStream(ctx context.Context, in BrowsersTelemetrySt
return util.CleanedUpSdkError{Err: err}
}
params := kernel.BrowserTelemetryStreamParams{}
if in.Seq >= 0 {
switch {
case in.Seq >= 0:
params.LastEventID = kernel.Opt(strconv.FormatInt(in.Seq, 10))
case in.ReplayAll:
params.Replay = kernel.Opt(replayAllValue)
}
stream := b.telemetry.StreamStreaming(ctx, br.SessionID, params)
defer stream.Close()
Expand Down Expand Up @@ -223,12 +235,14 @@ func runBrowsersTelemetryStream(cmd *cobra.Command, args []string) error {
categories, _ := cmd.Flags().GetStringSlice("categories")
types, _ := cmd.Flags().GetStringSlice("types")
seq, _ := cmd.Flags().GetInt64("seq")
replayAll, _ := cmd.Flags().GetBool("replay-all")
b := BrowsersCmd{browsers: &svc, telemetry: &svc.Telemetry}
return b.TelemetryStream(cmd.Context(), BrowsersTelemetryStreamInput{
Identifier: args[0],
Categories: categories,
Types: types,
Seq: seq,
ReplayAll: replayAll,
Output: out,
})
}
50 changes: 50 additions & 0 deletions cmd/browsers_telemetry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,11 @@ func captureStdout(t *testing.T, fn func()) string {

type FakeBrowserTelemetryService struct {
StreamFunc func() *ssestream.Stream[kernel.BrowserTelemetryStreamResponse]
LastQuery kernel.BrowserTelemetryStreamParams
}

func (f *FakeBrowserTelemetryService) StreamStreaming(ctx context.Context, id string, query kernel.BrowserTelemetryStreamParams, opts ...option.RequestOption) *ssestream.Stream[kernel.BrowserTelemetryStreamResponse] {
f.LastQuery = query
if f.StreamFunc != nil {
return f.StreamFunc()
}
Expand Down Expand Up @@ -83,6 +85,54 @@ func TestTelemetryStream_NegativeSeqErrors(t *testing.T) {
assert.Contains(t, err.Error(), "invalid --seq value -2")
}

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

err := b.TelemetryStream(context.Background(), BrowsersTelemetryStreamInput{
Identifier: "session123",
Seq: 5,
ReplayAll: true,
})

assert.Error(t, err)
assert.Contains(t, err.Error(), "cannot combine --replay-all with --seq")
}

func TestTelemetryStream_ReplayAllSetsReplayParam(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{}
b := BrowsersCmd{browsers: fakeBrowsers, telemetry: fakeTelemetry}

err := b.TelemetryStream(context.Background(), BrowsersTelemetryStreamInput{
Identifier: "session123",
Seq: -1,
ReplayAll: true,
})

assert.NoError(t, err)
assert.Equal(t, "all", fakeTelemetry.LastQuery.Replay.Or(""))
assert.False(t, fakeTelemetry.LastQuery.LastEventID.Valid(), "Last-Event-ID must not be set with --replay-all")
}

func TestTelemetryStream_SeqSetsLastEventID(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{}
b := BrowsersCmd{browsers: fakeBrowsers, telemetry: fakeTelemetry}

err := b.TelemetryStream(context.Background(), BrowsersTelemetryStreamInput{
Identifier: "session123",
Seq: 5,
})

assert.NoError(t, err)
assert.Equal(t, "5", fakeTelemetry.LastQuery.LastEventID.Or(""))
assert.False(t, fakeTelemetry.LastQuery.Replay.Valid(), "replay must not be set with --seq")
}

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

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