Skip to content

Commit 0e4cc08

Browse files
committed
fix: address review on telemetry events command
- Multi-category filtering: send each category as a repeated query param via option.WithQueryAdd. The SDK serializes []string as one comma-joined value, which the endpoint reads as a single bogus category; repeating the param matches the endpoint's expected form. - Validate --limit client-side (1-100) instead of silently passing invalid values to the server. - Add a client-side --types filter for parity with telemetry stream. - Durable read: make the identifier-to-session lookup best-effort and fall back to the identifier when Get 404s, so an ended session's archive stays readable (verified live against staging). - Frame the default vs --all paging in help, and document the events command in the README.
1 parent 31248f0 commit 0e4cc08

4 files changed

Lines changed: 104 additions & 22 deletions

File tree

README.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ Commands with JSON output support:
130130
- **Deploy**: `deploy` (JSONL streaming), `history`
131131
- **Invoke**: `invoke` (JSONL streaming), `history`
132132
- **Browser Sub-commands**: `replays list/start`, `process exec/spawn`, `fs file-info/list-files`
133-
- **Browser NDJSON streaming**: `telemetry stream`
133+
- **Browser telemetry**: `telemetry stream` (live NDJSON), `telemetry events` (recorded archive)
134134

135135
### Authentication
136136

@@ -320,6 +320,16 @@ Per-category updates are partial — only categories you name are changed; other
320320
- `-o, --output json` - Output newline-delimited JSON envelopes
321321
- Default output: tab-separated `<time>\t[<category>]\t<type>`, e.g. `15:04:05 [network] network_response`
322322

323+
- `kernel browsers telemetry events <id>` - Read recorded telemetry events from the archive (the durable counterpart to live `stream`)
324+
- By default returns the first page of events in the window; use `--all` to walk every page and dump the full archive
325+
- `--limit <n>` - Max events per page (1-100, default 20)
326+
- `--since <t>` / `--until <t>` - Window bounds as an RFC-3339 timestamp or a duration like `5m` (default window: last 5m)
327+
- `--categories <list>` - Filter by event category (`console`, `network`, `page`, `interaction`, `control`, `connection`, `system`, `screenshot`, `captcha`, `monitor`)
328+
- `--types <list>` - Filter by event type (e.g. `network_response`, `console_error`)
329+
- `--all` - Fetch every page in the window instead of just the first
330+
- `-o, --output json` - Output newline-delimited JSON envelopes
331+
- Default output: tab-separated `<time>\t<seq>\t[<category>]\t<type>`
332+
323333
### Browser Process Control
324334

325335
- `kernel browsers process exec <id> [--] [command...]` - Execute a command synchronously

cmd/browsers.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2750,12 +2750,21 @@ followed automatically by Chromium.`,
27502750
telemetryStream.Flags().StringP("output", "o", "", "Output format: json for newline-delimited JSON envelopes")
27512751
telemetryRoot.AddCommand(telemetryStream)
27522752

2753-
telemetryEvents := &cobra.Command{Use: "events <id>", Short: "Read recorded telemetry events", Args: cobra.ExactArgs(1), RunE: runBrowsersTelemetryEvents}
2753+
telemetryEvents := &cobra.Command{
2754+
Use: "events <id>",
2755+
Short: "Read recorded telemetry events",
2756+
Long: "Read recorded telemetry events for a browser session, in ascending sequence order.\n\n" +
2757+
"By default this returns the first page of events in the window (most recent window is the\n" +
2758+
"last 5m; adjust with --since/--until). Use --all to walk every page and dump the full archive.",
2759+
Args: cobra.ExactArgs(1),
2760+
RunE: runBrowsersTelemetryEvents,
2761+
}
27542762
telemetryEvents.Flags().Int64("limit", 0, "Max events per page (1-100, default 20)")
27552763
telemetryEvents.Flags().String("since", "", "Window start: RFC-3339 timestamp or duration like 5m (default 5m)")
27562764
telemetryEvents.Flags().String("until", "", "Window end (exclusive): RFC-3339 timestamp or duration like 5m")
27572765
telemetryEvents.Flags().StringSlice("categories", []string{}, "Filter by event category (console,network,page,interaction,control,connection,system,screenshot,captcha,monitor)")
2758-
telemetryEvents.Flags().Bool("all", false, "Fetch every page instead of just the first")
2766+
telemetryEvents.Flags().StringSlice("types", []string{}, "Filter by event type (e.g. network_response,console_error)")
2767+
telemetryEvents.Flags().Bool("all", false, "Fetch every page (the full archive in the window) instead of just the first")
27592768
telemetryEvents.Flags().StringP("output", "o", "", "Output format: json for newline-delimited JSON envelopes")
27602769
telemetryRoot.AddCommand(telemetryEvents)
27612770

cmd/browsers_telemetry.go

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,7 @@ type BrowsersTelemetryEventsInput struct {
242242
Since string
243243
Until string
244244
Categories []string
245+
Types []string
245246
All bool
246247
Output string
247248
}
@@ -253,6 +254,9 @@ func (b BrowsersCmd) TelemetryEvents(ctx context.Context, in BrowsersTelemetryEv
253254
if err := validateJSONOutput(in.Output); err != nil {
254255
return err
255256
}
257+
if in.Limit != 0 && (in.Limit < 1 || in.Limit > 100) {
258+
return fmt.Errorf("invalid --limit value %d: must be between 1 and 100", in.Limit)
259+
}
256260
for _, c := range in.Categories {
257261
if !slices.Contains(streamFilterCategories, c) {
258262
return fmt.Errorf("invalid --categories value %q: must be one of %s", c, strings.Join(streamFilterCategories, ", "))
@@ -269,16 +273,27 @@ func (b BrowsersCmd) TelemetryEvents(ctx context.Context, in BrowsersTelemetryEv
269273
if in.Until != "" {
270274
params.Until = kernel.Opt(in.Until)
271275
}
272-
if len(in.Categories) > 0 {
273-
params.Category = in.Categories
276+
// Send each category as a repeated query param. The SDK serializes a []string
277+
// field as a single comma-joined value, but the endpoint expects the parameter
278+
// repeated, so a comma-joined value matches no category.
279+
opts := make([]option.RequestOption, 0, len(in.Categories))
280+
for _, c := range in.Categories {
281+
opts = append(opts, option.WithQueryAdd("category", c))
274282
}
275283

276-
br, err := b.browsers.Get(ctx, in.Identifier, kernel.BrowserGetParams{})
277-
if err != nil {
278-
return util.CleanedUpSdkError{Err: err}
284+
// Resolve a name to a session ID when possible, but fall back to the identifier
285+
// as-is: the events archive outlives the session, so Get can 404 for an ended
286+
// session whose telemetry is still readable.
287+
sessionID := in.Identifier
288+
if br, gerr := b.browsers.Get(ctx, in.Identifier, kernel.BrowserGetParams{}); gerr == nil {
289+
sessionID = br.SessionID
279290
}
280291

281292
emit := func(ev kernel.BrowserTelemetryEventsResponse) error {
293+
// Category filtering is server-side; types are filtered here, matching stream.
294+
if !shouldEmit(ev.Event.Category, ev.Event.Type, nil, in.Types) {
295+
return nil
296+
}
282297
if in.Output == "json" {
283298
return util.PrintCompactJSONLine(ev)
284299
}
@@ -288,7 +303,7 @@ func (b BrowsersCmd) TelemetryEvents(ctx context.Context, in BrowsersTelemetryEv
288303
}
289304

290305
if in.All {
291-
pager := b.telemetry.EventsAutoPaging(ctx, br.SessionID, params)
306+
pager := b.telemetry.EventsAutoPaging(ctx, sessionID, params, opts...)
292307
for pager.Next() {
293308
if err := emit(pager.Current()); err != nil {
294309
return err
@@ -300,7 +315,7 @@ func (b BrowsersCmd) TelemetryEvents(ctx context.Context, in BrowsersTelemetryEv
300315
return nil
301316
}
302317

303-
page, err := b.telemetry.Events(ctx, br.SessionID, params)
318+
page, err := b.telemetry.Events(ctx, sessionID, params, opts...)
304319
if err != nil {
305320
return util.CleanedUpSdkError{Err: err}
306321
}
@@ -322,6 +337,7 @@ func runBrowsersTelemetryEvents(cmd *cobra.Command, args []string) error {
322337
since, _ := cmd.Flags().GetString("since")
323338
until, _ := cmd.Flags().GetString("until")
324339
categories, _ := cmd.Flags().GetStringSlice("categories")
340+
types, _ := cmd.Flags().GetStringSlice("types")
325341
all, _ := cmd.Flags().GetBool("all")
326342
b := BrowsersCmd{browsers: &svc, telemetry: &svc.Telemetry}
327343
return b.TelemetryEvents(cmd.Context(), BrowsersTelemetryEventsInput{
@@ -330,6 +346,7 @@ func runBrowsersTelemetryEvents(cmd *cobra.Command, args []string) error {
330346
Since: since,
331347
Until: until,
332348
Categories: categories,
349+
Types: types,
333350
All: all,
334351
Output: out,
335352
})

cmd/browsers_telemetry_test.go

Lines changed: 58 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ func captureStdout(t *testing.T, fn func()) string {
3636

3737
type FakeBrowserTelemetryService struct {
3838
StreamFunc func() *ssestream.Stream[kernel.BrowserTelemetryStreamResponse]
39-
EventsFunc func(id string, query kernel.BrowserTelemetryEventsParams) (*pagination.OffsetPagination[kernel.BrowserTelemetryEventsResponse], error)
39+
EventsFunc func(id string, query kernel.BrowserTelemetryEventsParams, opts ...option.RequestOption) (*pagination.OffsetPagination[kernel.BrowserTelemetryEventsResponse], error)
4040
EventsAutoPagingFunc func() *pagination.OffsetPaginationAutoPager[kernel.BrowserTelemetryEventsResponse]
4141
}
4242

@@ -49,7 +49,7 @@ func (f *FakeBrowserTelemetryService) StreamStreaming(ctx context.Context, id st
4949

5050
func (f *FakeBrowserTelemetryService) Events(ctx context.Context, id string, query kernel.BrowserTelemetryEventsParams, opts ...option.RequestOption) (*pagination.OffsetPagination[kernel.BrowserTelemetryEventsResponse], error) {
5151
if f.EventsFunc != nil {
52-
return f.EventsFunc(id, query)
52+
return f.EventsFunc(id, query, opts...)
5353
}
5454
return &pagination.OffsetPagination[kernel.BrowserTelemetryEventsResponse]{}, nil
5555
}
@@ -108,8 +108,9 @@ func TestTelemetryEvents_SinglePageTextAndParams(t *testing.T) {
108108
}}
109109
var gotID string
110110
var gotQuery kernel.BrowserTelemetryEventsParams
111-
fakeTelemetry := &FakeBrowserTelemetryService{EventsFunc: func(id string, query kernel.BrowserTelemetryEventsParams) (*pagination.OffsetPagination[kernel.BrowserTelemetryEventsResponse], error) {
112-
gotID, gotQuery = id, query
111+
var gotOpts []option.RequestOption
112+
fakeTelemetry := &FakeBrowserTelemetryService{EventsFunc: func(id string, query kernel.BrowserTelemetryEventsParams, opts ...option.RequestOption) (*pagination.OffsetPagination[kernel.BrowserTelemetryEventsResponse], error) {
113+
gotID, gotQuery, gotOpts = id, query, opts
113114
return telemetryEventsPage(t,
114115
`{"event":{"type":"network_response","category":"network","ts":1000000},"seq":7}`,
115116
`{"event":{"type":"network_request","category":"network","ts":2000000},"seq":8}`,
@@ -122,15 +123,17 @@ func TestTelemetryEvents_SinglePageTextAndParams(t *testing.T) {
122123
Limit: 5,
123124
Since: "5m",
124125
Until: "2020-01-01T00:00:00Z",
125-
Categories: []string{"network"},
126+
Categories: []string{"console", "network"},
126127
})
127128

128129
assert.NoError(t, err)
129130
assert.Equal(t, "session123", gotID)
130131
assert.Equal(t, int64(5), gotQuery.Limit.Value)
131132
assert.Equal(t, "5m", gotQuery.Since.Value)
132133
assert.Equal(t, "2020-01-01T00:00:00Z", gotQuery.Until.Value)
133-
assert.Equal(t, []string{"network"}, gotQuery.Category)
134+
// Categories go out as repeated query params, not the comma-joined typed field.
135+
assert.Empty(t, gotQuery.Category)
136+
assert.Len(t, gotOpts, 2)
134137
out := outBuf.String()
135138
assert.Contains(t, out, "network_response")
136139
assert.Contains(t, out, "network_request")
@@ -142,7 +145,7 @@ func TestTelemetryEvents_SinglePageJSON(t *testing.T) {
142145
fakeBrowsers := &FakeBrowsersService{GetFunc: func(ctx context.Context, id string, query kernel.BrowserGetParams, opts ...option.RequestOption) (*kernel.BrowserGetResponse, error) {
143146
return &kernel.BrowserGetResponse{SessionID: id}, nil
144147
}}
145-
fakeTelemetry := &FakeBrowserTelemetryService{EventsFunc: func(id string, query kernel.BrowserTelemetryEventsParams) (*pagination.OffsetPagination[kernel.BrowserTelemetryEventsResponse], error) {
148+
fakeTelemetry := &FakeBrowserTelemetryService{EventsFunc: func(id string, query kernel.BrowserTelemetryEventsParams, opts ...option.RequestOption) (*pagination.OffsetPagination[kernel.BrowserTelemetryEventsResponse], error) {
146149
return telemetryEventsPage(t, `{"event":{"type":"network_response","ts":1000000},"seq":1}`), nil
147150
}}
148151
b := BrowsersCmd{browsers: fakeBrowsers, telemetry: fakeTelemetry}
@@ -156,15 +159,58 @@ func TestTelemetryEvents_SinglePageJSON(t *testing.T) {
156159
assert.Contains(t, out, "network_response")
157160
}
158161

159-
func TestTelemetryEvents_GetErrorSurfaces(t *testing.T) {
162+
func TestTelemetryEvents_InvalidLimitErrors(t *testing.T) {
163+
b := BrowsersCmd{browsers: &FakeBrowsersService{}, telemetry: &FakeBrowserTelemetryService{}}
164+
165+
for _, lim := range []int64{-1, 101} {
166+
err := b.TelemetryEvents(context.Background(), BrowsersTelemetryEventsInput{Identifier: "session123", Limit: lim})
167+
assert.Error(t, err)
168+
assert.Contains(t, err.Error(), "invalid --limit value")
169+
}
170+
}
171+
172+
func TestTelemetryEvents_TypesFilterDropsNonMatching(t *testing.T) {
173+
setupStdoutCapture(t)
160174
fakeBrowsers := &FakeBrowsersService{GetFunc: func(ctx context.Context, id string, query kernel.BrowserGetParams, opts ...option.RequestOption) (*kernel.BrowserGetResponse, error) {
161-
return nil, fmt.Errorf("boom")
175+
return &kernel.BrowserGetResponse{SessionID: id}, nil
162176
}}
163-
b := BrowsersCmd{browsers: fakeBrowsers, telemetry: &FakeBrowserTelemetryService{}}
177+
fakeTelemetry := &FakeBrowserTelemetryService{EventsFunc: func(id string, query kernel.BrowserTelemetryEventsParams, opts ...option.RequestOption) (*pagination.OffsetPagination[kernel.BrowserTelemetryEventsResponse], error) {
178+
return telemetryEventsPage(t,
179+
`{"event":{"type":"network_request","category":"network","ts":1000000},"seq":1}`,
180+
`{"event":{"type":"network_response","category":"network","ts":2000000},"seq":2}`,
181+
), nil
182+
}}
183+
b := BrowsersCmd{browsers: fakeBrowsers, telemetry: fakeTelemetry}
164184

165-
err := b.TelemetryEvents(context.Background(), BrowsersTelemetryEventsInput{Identifier: "session123"})
185+
err := b.TelemetryEvents(context.Background(), BrowsersTelemetryEventsInput{
186+
Identifier: "session123",
187+
Types: []string{"network_response"},
188+
})
166189

167-
assert.Error(t, err)
190+
assert.NoError(t, err)
191+
assert.Contains(t, outBuf.String(), "network_response")
192+
assert.NotContains(t, outBuf.String(), "network_request")
193+
}
194+
195+
func TestTelemetryEvents_FallsBackToIdentifierWhenGetFails(t *testing.T) {
196+
setupStdoutCapture(t)
197+
// Get 404s (e.g. an ended session); the events archive is still readable, so the
198+
// command falls back to the identifier as the session id rather than failing.
199+
fakeBrowsers := &FakeBrowsersService{GetFunc: func(ctx context.Context, id string, query kernel.BrowserGetParams, opts ...option.RequestOption) (*kernel.BrowserGetResponse, error) {
200+
return nil, fmt.Errorf("not found")
201+
}}
202+
var gotID string
203+
fakeTelemetry := &FakeBrowserTelemetryService{EventsFunc: func(id string, query kernel.BrowserTelemetryEventsParams, opts ...option.RequestOption) (*pagination.OffsetPagination[kernel.BrowserTelemetryEventsResponse], error) {
204+
gotID = id
205+
return telemetryEventsPage(t, `{"event":{"type":"cdp_disconnect","category":"connection","ts":1000000},"seq":1}`), nil
206+
}}
207+
b := BrowsersCmd{browsers: fakeBrowsers, telemetry: fakeTelemetry}
208+
209+
err := b.TelemetryEvents(context.Background(), BrowsersTelemetryEventsInput{Identifier: "ended-session-id"})
210+
211+
assert.NoError(t, err)
212+
assert.Equal(t, "ended-session-id", gotID)
213+
assert.Contains(t, outBuf.String(), "cdp_disconnect")
168214
}
169215

170216
func TestTelemetryStream_NilTelemetryErrors(t *testing.T) {

0 commit comments

Comments
 (0)