Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
e424621
update sdk
archandatta May 27, 2026
baa3558
feat: add PrintCompactJSONLine helper for NDJSON output
archandatta May 27, 2026
6f8006f
feat: add --telemetry flag on browsers create
archandatta May 27, 2026
4540e3f
feat: add browsers telemetry stop subcommand
archandatta May 27, 2026
e97378f
feat: add browsers telemetry stream subcommand with NDJSON output
archandatta May 27, 2026
a4d8cd9
feat: add browsers telemetry start subcommand
archandatta May 27, 2026
1ccc8c9
chore: document browser telemetry create flag, start, stop, and stream
archandatta May 27, 2026
9e4ef95
feat: add browsers telemetry set, status subcommands and simplify sta…
archandatta May 27, 2026
116851b
review: address browser telemetry code review feedback
archandatta May 27, 2026
bd84338
fix: status misreports off when telemetry uses VM defaults
archandatta May 27, 2026
832790f
fix: stream --categories uses API category field and corrects known c…
archandatta May 27, 2026
ba5ea92
fix: remove incorrect api category, validated against kernel-images
archandatta May 27, 2026
1cbcb11
fix: telemetry status shows enabled on/off and skips categories when …
archandatta May 27, 2026
3ac29ac
chore: split browser telemetry into cmd/browsers_telemetry.go
archandatta May 27, 2026
fc4affb
review: address browsers_telemetry code review feedback
archandatta May 27, 2026
87922b3
review: fix readme inaccuracies in telemetry documentation
archandatta May 27, 2026
4f4a8ae
review: collapse telemetry start/stop/set/status into browsers update…
archandatta May 28, 2026
c179c81
review: require explicit --telemetry=all instead of bare --telemetry
archandatta May 28, 2026
dac6596
review: drop knownTelemetryTypes warning; collapse category lists
archandatta May 28, 2026
54a7620
review: derive event category from Type field; drop json.Unmarshal pe…
archandatta May 28, 2026
4a0ba01
nit: inline validateJSONOutput helper; drop errors import
archandatta May 28, 2026
373f618
review: fix create/update --telemetry parity; fix append aliasing; gofmt
archandatta May 28, 2026
1c71174
review: DRY telemetry param logic; inline hasTelemetryChange; trim RE…
archandatta May 28, 2026
a213e33
review: address should-fix and nit feedback on telemetry stream
archandatta May 28, 2026
92c6805
review: fix tab alignment, rename buildTelemetryParam, validate --seq…
archandatta May 28, 2026
6df0ac3
nit: validate --seq before browsers.Get to fail fast
archandatta May 28, 2026
26e6b5b
docs: fix browser telemetry README inaccuracies
archandatta May 28, 2026
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
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ Commands with JSON output support:
- **Deploy**: `deploy` (JSONL streaming), `history`
- **Invoke**: `invoke` (JSONL streaming), `history`
- **Browser Sub-commands**: `replays list/start`, `process exec/spawn`, `fs file-info/list-files`
- **Browser NDJSON streaming**: `telemetry stream`

### Authentication

Expand Down Expand Up @@ -211,13 +212,21 @@ Commands with JSON output support:
- `--start-url <url>` - Initial page to open on launch
- `--pool-id <id>` - Acquire a browser from the specified pool (mutually exclusive with --pool-name; ignores other session flags)
- `--pool-name <name>` - Acquire a browser from the pool name (mutually exclusive with --pool-id; ignores other session flags)
- `--telemetry=all` - Enable telemetry for all categories
- `--telemetry=off` - Disable telemetry
- `--telemetry=<list>` - Per-category config, e.g. `--telemetry=network=on,page=off`
- `--output json`, `-o json` - Output raw JSON object
- _Note: When a pool is specified, omit other session configuration flags—pool settings determine profile, proxy, viewport, etc._
- `kernel browsers delete <id>` - Delete a browser
- `kernel browsers view <id>` - Get live view URL for a browser
- `--output json`, `-o json` - Output JSON with liveViewUrl
- `kernel browsers get <id>` - Get detailed browser session info
- `--output json`, `-o json` - Output raw JSON object
- `kernel browsers update <id>` - Update a running browser session
- `--telemetry=all` - Enable telemetry for all categories
- `--telemetry=off` - Disable telemetry
- `--telemetry=<list>` - Per-category config, e.g. `--telemetry=network=on,page=off`
- `--output json`, `-o json` - Output raw JSON object
- `kernel browsers curl <id> <url>` - Make HTTP requests through a browser session's Chrome network stack
- `-X, --request <method>` - HTTP method (default: GET; defaults to POST when `--data` is set)
- `-H, --header <header>` - HTTP header, repeatable (`"Key: Value"` format)
Expand Down Expand Up @@ -280,6 +289,23 @@ Commands with JSON output support:
- `kernel browsers replays download <id> <replay-id>` - Download a replay video
- `-f, --output-file <path>` - Output file path for the replay video

### Browser Telemetry

Telemetry config is a sub-field of the browser session. Use `browsers create` or `browsers update` to enable, disable, or configure it, and `browsers get` to inspect the current state.

- Enable all categories: `kernel browsers update <id> --telemetry=all`
- Disable: `kernel browsers update <id> --telemetry=off`
- Per-category: `kernel browsers update <id> --telemetry=network=on,page=off` (valid: `console`, `interaction`, `network`, `page`; `system` always emits and cannot be toggled)

Per-category updates are partial — only categories you name are changed; others retain their current state. `--telemetry=all` and `--telemetry=off` reset the entire config.

- `kernel browsers telemetry stream <id>` - Stream live telemetry events (NDJSON with `-o json`)
- `--categories <list>` - Filter by event category (`console`, `network`, `page`, `interaction`, `system`); `system` matches `monitor_*` event types
- `--types <list>` - Filter by event type (e.g. `network_response`, `console_error`)
- `--seq <n>` - Resume from sequence number (Last-Event-ID); `--seq=0` replays from the beginning. Omit to stream from now.
- `-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`

### Browser Process Control

- `kernel browsers process exec <id> [--] [command...]` - Execute a command synchronously
Expand Down
4 changes: 2 additions & 2 deletions cmd/auth_connections.go
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ func printManagedAuthSummary(auth *kernel.ManagedAuth) {
{"Can Reauth", fmt.Sprintf("%t", auth.CanReauth)},
}
if auth.CanReauthReason != "" {
tableData = append(tableData, []string{"Can Reauth Reason", auth.CanReauthReason})
tableData = append(tableData, []string{"Can Reauth Reason", string(auth.CanReauthReason)})
}
if auth.Credential.Name != "" {
tableData = append(tableData, []string{"Credential Name", auth.Credential.Name})
Expand Down Expand Up @@ -326,7 +326,7 @@ func (c AuthConnectionCmd) Get(ctx context.Context, in AuthConnectionGetInput) e
{"Can Reauth", fmt.Sprintf("%t", auth.CanReauth)},
}
if auth.CanReauthReason != "" {
tableData = append(tableData, []string{"Can Reauth Reason", auth.CanReauthReason})
tableData = append(tableData, []string{"Can Reauth Reason", string(auth.CanReauthReason)})
}
if auth.Credential.Name != "" {
tableData = append(tableData, []string{"Credential Name", auth.Credential.Name})
Expand Down
30 changes: 28 additions & 2 deletions cmd/browsers.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ type BrowsersCreateInput struct {
StartURL string
Extensions []string
Viewport string
Telemetry string
Output string
}

Expand Down Expand Up @@ -214,6 +215,7 @@ type BrowsersUpdateInput struct {
ProfileSaveChanges BoolFlag
Viewport string
Force bool
Telemetry string
Output string
}

Expand All @@ -227,6 +229,7 @@ type BrowsersCmd struct {
logs BrowserLogService
computer BrowserComputerService
playwright BrowserPlaywrightService
telemetry BrowserTelemetryService
}

type BrowsersListInput struct {
Expand Down Expand Up @@ -422,6 +425,14 @@ func (b BrowsersCmd) Create(ctx context.Context, in BrowsersCreateInput) error {
}
}

if in.Telemetry != "" {
t, err := buildTelemetryParam(in.Telemetry)
if err != nil {
return err
}
params.Telemetry = t
}

browser, err := b.browsers.New(ctx, params)
if err != nil {
return util.CleanedUpSdkError{Err: err}
Expand Down Expand Up @@ -588,8 +599,8 @@ func (b BrowsersCmd) Update(ctx context.Context, in BrowsersUpdateInput) error {
}

// Validate that at least one update option is provided
if !hasProxyChange && !hasProfileChange && !hasViewportChange {
return fmt.Errorf("must specify at least one of: --proxy-id, --clear-proxy, --profile-id, --profile-name, or --viewport")
if !hasProxyChange && !hasProfileChange && !hasViewportChange && in.Telemetry == "" {
return fmt.Errorf("must specify at least one of: --proxy-id, --clear-proxy, --profile-id, --profile-name, --viewport, or --telemetry")
}

params := kernel.BrowserUpdateParams{}
Expand All @@ -614,6 +625,15 @@ func (b BrowsersCmd) Update(ctx context.Context, in BrowsersUpdateInput) error {
}
}

// Handle telemetry changes
if in.Telemetry != "" {
t, err := buildTelemetryParam(in.Telemetry)
if err != nil {
return err
}
params.Telemetry = t
}

// Handle viewport changes
if hasViewportChange {
width, height, refreshRate, err := parseViewport(in.Viewport)
Expand Down Expand Up @@ -2241,6 +2261,7 @@ func init() {
browsersUpdateCmd.Flags().Bool("save-changes", false, "If set, save changes back to the profile when the session ends")
browsersUpdateCmd.Flags().String("viewport", "", "Browser viewport size (e.g., 1920x1080@25). Supported: 2560x1440@10, 1920x1080@25, 1920x1200@25, 1440x900@25, 1024x768@60, 1200x800@60, 1280x800@60")
browsersUpdateCmd.Flags().Bool("force", false, "Force viewport resize even when a live view or recording/replay is active")
browsersUpdateCmd.Flags().String("telemetry", "", "Update telemetry: --telemetry=all to enable, --telemetry=off to disable, --telemetry=network=on,page=off for per-category")

browsersCmd.AddCommand(browsersListCmd)
browsersCmd.AddCommand(browsersCreateCmd)
Expand Down Expand Up @@ -2506,6 +2527,7 @@ func init() {
browsersCreateCmd.Flags().Bool("viewport-interactive", false, "Interactively select viewport size from list")
browsersCreateCmd.Flags().String("pool-id", "", "Browser pool ID to acquire from (mutually exclusive with --pool-name)")
browsersCreateCmd.Flags().String("pool-name", "", "Browser pool name to acquire from (mutually exclusive with --pool-id)")
browsersCreateCmd.Flags().String("telemetry", "", "Configure telemetry: --telemetry=all to enable, --telemetry=off to disable, --telemetry=network=on,page=off for per-category")

// curl
curlCmd := &cobra.Command{
Expand Down Expand Up @@ -2575,6 +2597,7 @@ func runBrowsersCreate(cmd *cobra.Command, args []string) error {
viewportInteractive, _ := cmd.Flags().GetBool("viewport-interactive")
poolID, _ := cmd.Flags().GetString("pool-id")
poolName, _ := cmd.Flags().GetString("pool-name")
telemetry, _ := cmd.Flags().GetString("telemetry")
output, _ := cmd.Flags().GetString("output")

if poolID != "" && poolName != "" {
Expand Down Expand Up @@ -2684,6 +2707,7 @@ func runBrowsersCreate(cmd *cobra.Command, args []string) error {
StartURL: startURL,
Extensions: extensions,
Viewport: viewport,
Telemetry: telemetry,
Output: output,
}

Expand Down Expand Up @@ -2742,6 +2766,7 @@ func runBrowsersUpdate(cmd *cobra.Command, args []string) error {
saveChanges, _ := cmd.Flags().GetBool("save-changes")
viewport, _ := cmd.Flags().GetString("viewport")
force, _ := cmd.Flags().GetBool("force")
telemetry, _ := cmd.Flags().GetString("telemetry")

svc := client.Browsers
b := BrowsersCmd{browsers: &svc}
Expand All @@ -2754,6 +2779,7 @@ func runBrowsersUpdate(cmd *cobra.Command, args []string) error {
ProfileSaveChanges: BoolFlag{Set: cmd.Flags().Changed("save-changes"), Value: saveChanges},
Viewport: viewport,
Force: force,
Telemetry: telemetry,
Output: out,
})
}
Expand Down
183 changes: 183 additions & 0 deletions cmd/browsers_telemetry.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
package cmd

import (
"context"
"fmt"
"slices"
"strconv"
"strings"
"time"

"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/ssestream"
"github.com/pterm/pterm"
"github.com/spf13/cobra"
)

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

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

// parseTelemetryCategories parses a comma-separated "name=on|off" string into
// a BrowserTelemetryCategoriesConfigParam. Unmentioned categories are omitted.
func parseTelemetryCategories(s string) (kernel.BrowserTelemetryCategoriesConfigParam, error) {
p := kernel.BrowserTelemetryCategoriesConfigParam{}
for _, part := range strings.Split(s, ",") {
name, val, ok := strings.Cut(strings.TrimSpace(part), "=")
if !ok {
return p, fmt.Errorf("invalid category assignment %q: expected name=on or name=off", part)
}
name, val = strings.TrimSpace(name), strings.TrimSpace(val)
var enabled bool
switch val {
case "on":
enabled = true
case "off":
enabled = false
default:
return p, fmt.Errorf("invalid value %q for category %q: must be 'on' or 'off'", val, name)
}
switch name {
case "console":
p.Console = kernel.BrowserTelemetryCategoryConfigParam{Enabled: kernel.Opt(enabled)}
case "interaction":
p.Interaction = kernel.BrowserTelemetryCategoryConfigParam{Enabled: kernel.Opt(enabled)}
case "network":
p.Network = kernel.BrowserTelemetryCategoryConfigParam{Enabled: kernel.Opt(enabled)}
case "page":
p.Page = kernel.BrowserTelemetryCategoryConfigParam{Enabled: kernel.Opt(enabled)}
default:
return p, fmt.Errorf("unknown category %q: must be one of %s", name, strings.Join(settableCategories, ", "))
}
}
return p, nil
}

// buildTelemetryParam converts a --telemetry flag value to the API param.
func buildTelemetryParam(s string) (kernel.BrowserTelemetryRequestConfigParam, error) {
switch s {
case "all":
return kernel.BrowserTelemetryRequestConfigParam{Enabled: kernel.Opt(true)}, nil
case "off":
return kernel.BrowserTelemetryRequestConfigParam{Enabled: kernel.Opt(false)}, nil
default:
p, err := parseTelemetryCategories(s)
if err != nil {
return kernel.BrowserTelemetryRequestConfigParam{}, err
}
return kernel.BrowserTelemetryRequestConfigParam{Enabled: kernel.Opt(true), Browser: p}, nil
}
}

// settableCategories are the categories accepted by --telemetry=<categories>.
// "system" is always-on and cannot be toggled, but is valid as a --categories stream filter.
var settableCategories = []string{"console", "interaction", "network", "page"}

// eventCategory derives the category from the event type prefix.
// "monitor_*" maps to "system"; all others use the prefix before the first "_".
// TODO(sdk): kernel-go-sdk should surface Category directly on BrowserTelemetryEventUnion.
func eventCategory(ev kernel.BrowserTelemetryEventUnion) string {
prefix, _, ok := strings.Cut(ev.Type, "_")
if !ok {
return ev.Type
}
if prefix == "monitor" {
return "system"
}
return prefix
}

// shouldEmit applies client-side category/type filters to a telemetry event.
func shouldEmit(ev kernel.BrowserTelemetryEventUnion, categories, types []string) bool {
if len(categories) > 0 && !slices.Contains(categories, eventCategory(ev)) {
return false
}
if len(types) > 0 && !slices.Contains(types, ev.Type) {
return false
}
return true
}

func (b BrowsersCmd) TelemetryStream(ctx context.Context, in BrowsersTelemetryStreamInput) error {
if b.telemetry == nil {
return fmt.Errorf("telemetry service not available")
}
if in.Output != "" && in.Output != "json" {
return fmt.Errorf("unsupported --output value: use 'json'")
}
if in.Seq < -1 {
return fmt.Errorf("--seq must be >= 0 (use --seq=0 to resume from the beginning, or omit to stream from now)")
}
br, err := b.browsers.Get(ctx, in.Identifier, kernel.BrowserGetParams{})
if err != nil {
return util.CleanedUpSdkError{Err: err}
}
params := kernel.BrowserTelemetryStreamParams{}
if in.Seq >= 0 {
params.LastEventID = kernel.Opt(strconv.FormatInt(in.Seq, 10))
}
stream := b.telemetry.StreamStreaming(ctx, br.SessionID, params)
defer stream.Close()
for stream.Next() {
ev := stream.Current()
cat := eventCategory(ev.Event)
if len(in.Categories) > 0 && !slices.Contains(in.Categories, cat) {
continue
}
if len(in.Types) > 0 && !slices.Contains(in.Types, ev.Event.Type) {
continue
}
if in.Output == "json" {
if err := util.PrintCompactJSONLine(ev); err != nil {
return err
}
continue
}
ts := time.UnixMicro(ev.Event.Ts).Local().Format("15:04:05")
pterm.Printf("%s\t[%s]\t%s\n", ts, cat, ev.Event.Type)
}
if err := stream.Err(); err != nil {
return util.CleanedUpSdkError{Err: err}
}
return nil
}

func init() {
// browsersCmd is a package-level var (browsers.go), initialized before init() runs.
telemetryRoot := &cobra.Command{Use: "telemetry", Short: "Browser telemetry operations"}
telemetryStream := &cobra.Command{Use: "stream <id>", Short: "Stream live telemetry events", Args: cobra.ExactArgs(1), RunE: runBrowsersTelemetryStream}
telemetryStream.Flags().StringSlice("categories", []string{}, "Filter by API event category (console,network,page,interaction,system); system covers all monitor_* events")
telemetryStream.Flags().StringSlice("types", []string{}, "Filter by event type (e.g. network_response,console_error)")
telemetryStream.Flags().Int64("seq", -1, "Resume stream from sequence number (Last-Event-ID); 0 means from the beginning")
telemetryStream.Flags().StringP("output", "o", "", "Output format: json for newline-delimited JSON envelopes")
telemetryRoot.AddCommand(telemetryStream)
browsersCmd.AddCommand(telemetryRoot)
}

func runBrowsersTelemetryStream(cmd *cobra.Command, args []string) error {
client := getKernelClient(cmd)
svc := client.Browsers
out, _ := cmd.Flags().GetString("output")
categories, _ := cmd.Flags().GetStringSlice("categories")
types, _ := cmd.Flags().GetStringSlice("types")
seq, _ := cmd.Flags().GetInt64("seq")
b := BrowsersCmd{browsers: &svc, telemetry: &svc.Telemetry}
return b.TelemetryStream(cmd.Context(), BrowsersTelemetryStreamInput{
Identifier: args[0],
Categories: categories,
Types: types,
Seq: seq,
Output: out,
})
}
Loading
Loading