Skip to content
Draft
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: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -266,18 +266,20 @@ Commands with JSON output support:
- `--refresh-on-profile-update` - Flush idle browsers when the pool's profile is updated (requires a profile)
- `--profile-id`, `--profile-name`, `--proxy-id`, `--start-url`, `--extension`, `--viewport` - Same semantics as `kernel browsers create`
- `--chrome-policy <json>` / `--chrome-policy-file <path>` - Custom Chrome enterprise policy applied to every browser in the pool, as a JSON object or from a file (`-` for stdin). Same semantics as `kernel browsers create`.
- `--telemetry=all` / `--telemetry=off` / `--telemetry=<categories>` - Telemetry applied to browsers warmed into the pool. Same semantics as `kernel browsers create`.
- `--output json`, `-o json` - Output raw JSON object
- `kernel browser-pools get <id-or-name>` - Get pool details
- `--output json`, `-o json` - Output raw JSON object
- `kernel browser-pools update <id-or-name>` - Update pool configuration
- Same flags as create plus `--clear-start-url` (remove the pool's start URL) and `--discard-all-idle` (discard all idle browsers and refill). An empty `--chrome-policy '{}'` is ignored and does not clear an existing policy; recreate the pool to remove one.
- Same flags as create plus `--clear-start-url` (remove the pool's start URL) and `--discard-all-idle` (discard all idle browsers and refill). An empty `--chrome-policy '{}'` is ignored and does not clear an existing policy; recreate the pool to remove one. `--telemetry` updates only apply to browsers warmed after the update.
- `--output json`, `-o json` - Output raw JSON object
- `kernel browser-pools delete <id-or-name>` - Delete a pool
- `--force` - Force delete even if browsers are leased
- `kernel browser-pools acquire <id-or-name>` - Acquire a browser from the pool
- `--timeout <seconds>` - Acquire timeout before returning 204
- `--name <name>` - Optional name for the acquired session (applies to this lease; cleared on release)
- `--tag <KEY=VALUE>` - Set a tag on the acquired session, repeatable; applies to this lease
- `--telemetry=all` / `--telemetry=off` / `--telemetry=<categories>` - Telemetry override for this lease only, merged onto the pool's config
- `--output json`, `-o json` - Output raw JSON object
- `kernel browser-pools release <id-or-name>` - Release a browser back to the pool
- `--session-id <id>` - Browser session ID to release (required)
Expand Down
81 changes: 77 additions & 4 deletions cmd/browser_pools.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,33 @@ func (c BrowserPoolsCmd) List(ctx context.Context, in BrowserPoolsListInput) err
return nil
}

// buildPoolNewTelemetryParam converts a --telemetry flag value to the pool create param.
func buildPoolNewTelemetryParam(s string) (kernel.BrowserPoolNewParamsTelemetry, error) {
enabled, browser, err := resolveTelemetryFlag(s)
return kernel.BrowserPoolNewParamsTelemetry{Enabled: enabled, Browser: browser}, err
}

// buildPoolUpdateTelemetryParam converts a --telemetry flag value to the pool update param.
func buildPoolUpdateTelemetryParam(s string) (kernel.BrowserPoolUpdateParamsTelemetry, error) {
enabled, browser, err := resolveTelemetryFlag(s)
return kernel.BrowserPoolUpdateParamsTelemetry{Enabled: enabled, Browser: browser}, err
}

// buildPoolAcquireTelemetryParam converts a --telemetry flag value to the acquire override param.
func buildPoolAcquireTelemetryParam(s string) (kernel.BrowserPoolAcquireParamsTelemetry, error) {
enabled, browser, err := resolveTelemetryFlag(s)
return kernel.BrowserPoolAcquireParamsTelemetry{Enabled: enabled, Browser: browser}, err
}

// formatPoolTelemetry renders a pool's active telemetry config for the details table.
func formatPoolTelemetry(cfg kernel.BrowserTelemetryConfig) string {
on := telemetryEnabledCategories(cfg)
if len(on) == 0 {
return "disabled"
}
return strings.Join(on, ", ")
}

type BrowserPoolsCreateInput struct {
Name string
Size int64
Expand All @@ -107,6 +134,7 @@ type BrowserPoolsCreateInput struct {
Viewport string
ChromePolicy string
ChromePolicyFile string
Telemetry string
Output string
}

Expand Down Expand Up @@ -183,6 +211,14 @@ func (c BrowserPoolsCmd) Create(ctx context.Context, in BrowserPoolsCreateInput)
params.ChromePolicy = chromePolicy
}

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

pool, err := c.client.New(ctx, params)
if err != nil {
return util.CleanedUpSdkError{Err: err}
Expand All @@ -197,6 +233,9 @@ func (c BrowserPoolsCmd) Create(ctx context.Context, in BrowserPoolsCreateInput)
} else {
pterm.Success.Printf("Created browser pool %s\n", pool.ID)
}
if in.Telemetry != "" {
printTelemetrySummary(pool.BrowserPoolConfig.Telemetry)
}
return nil
}

Expand Down Expand Up @@ -240,6 +279,7 @@ func (c BrowserPoolsCmd) Get(ctx context.Context, in BrowserPoolsGetInput) error
{"Start URL", util.OrDash(cfg.StartURL)},
{"Extensions", formatExtensions(cfg.Extensions)},
{"Viewport", formatViewport(cfg.Viewport)},
{"Telemetry", formatPoolTelemetry(cfg.Telemetry)},
}

PrintTableNoPad(rows, true)
Expand All @@ -265,6 +305,7 @@ type BrowserPoolsUpdateInput struct {
Viewport string
ChromePolicy string
ChromePolicyFile string
Telemetry string
DiscardAllIdle BoolFlag
Output string
}
Expand Down Expand Up @@ -356,6 +397,14 @@ func (c BrowserPoolsCmd) Update(ctx context.Context, in BrowserPoolsUpdateInput)
pterm.Warning.Println("An empty chrome policy is ignored and does not clear the pool's existing policy; recreate the pool to remove a policy.")
}

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

pool, err := c.client.Update(ctx, in.IDOrName, params)
if err != nil {
return util.CleanedUpSdkError{Err: err}
Expand All @@ -370,6 +419,9 @@ func (c BrowserPoolsCmd) Update(ctx context.Context, in BrowserPoolsUpdateInput)
} else {
pterm.Success.Printf("Updated browser pool %s\n", pool.ID)
}
if in.Telemetry != "" {
printTelemetrySummary(pool.BrowserPoolConfig.Telemetry)
}
return nil
}

Expand All @@ -396,13 +448,15 @@ type BrowserPoolsAcquireInput struct {
TimeoutSeconds int64
Name string
Tags map[string]string
Telemetry string
Output string
}

// buildAcquireParams builds the SDK params for acquiring a browser from a pool.
// Shared by `browser-pools acquire` and the `browsers create --pool-id/--pool-name`
// path so the per-lease name/tags forwarding cannot silently diverge between them.
func buildAcquireParams(name string, tags map[string]string, timeoutSeconds int64) kernel.BrowserPoolAcquireParams {
// path so the per-lease name/tags/telemetry forwarding cannot silently diverge
// between them. The telemetry override merges onto the pool's config for this lease.
func buildAcquireParams(name string, tags map[string]string, timeoutSeconds int64, telemetry string) (kernel.BrowserPoolAcquireParams, error) {
params := kernel.BrowserPoolAcquireParams{}
if timeoutSeconds > 0 {
params.AcquireTimeoutSeconds = kernel.Int(timeoutSeconds)
Expand All @@ -413,15 +467,25 @@ func buildAcquireParams(name string, tags map[string]string, timeoutSeconds int6
if len(tags) > 0 {
params.Tags = kernel.Tags(tags)
}
return params
if telemetry != "" {
t, err := buildPoolAcquireTelemetryParam(telemetry)
if err != nil {
return kernel.BrowserPoolAcquireParams{}, err
}
params.Telemetry = t
}
return params, nil
}

func (c BrowserPoolsCmd) Acquire(ctx context.Context, in BrowserPoolsAcquireInput) error {
if err := validateJSONOutput(in.Output); err != nil {
return err
}

params := buildAcquireParams(in.Name, in.Tags, in.TimeoutSeconds)
params, err := buildAcquireParams(in.Name, in.Tags, in.TimeoutSeconds, in.Telemetry)
if err != nil {
return err
}
resp, err := c.client.Acquire(ctx, in.IDOrName, params)
if err != nil {
return util.CleanedUpSdkError{Err: err}
Expand Down Expand Up @@ -583,6 +647,7 @@ func init() {
browserPoolsCreateCmd.Flags().String("viewport", "", "Viewport size (e.g. 1280x800)")
browserPoolsCreateCmd.Flags().String("chrome-policy", "", "Custom Chrome enterprise policy as a JSON object")
browserPoolsCreateCmd.Flags().String("chrome-policy-file", "", "Read Chrome enterprise policy (JSON object) from a file (use '-' for stdin)")
browserPoolsCreateCmd.Flags().String("telemetry", "", "Configure telemetry for browsers warmed into the pool (opt-in): --telemetry=all (default set), --telemetry=off (disable), or --telemetry=console,network (capture exactly those categories)")
browserPoolsCreateCmd.MarkFlagsMutuallyExclusive("chrome-policy", "chrome-policy-file")

addJSONOutputFlag(browserPoolsGetCmd)
Expand All @@ -605,6 +670,7 @@ func init() {
browserPoolsUpdateCmd.Flags().String("chrome-policy", "", "Custom Chrome enterprise policy as a JSON object")
browserPoolsUpdateCmd.Flags().String("chrome-policy-file", "", "Read Chrome enterprise policy (JSON object) from a file (use '-' for stdin)")
browserPoolsUpdateCmd.MarkFlagsMutuallyExclusive("chrome-policy", "chrome-policy-file")
browserPoolsUpdateCmd.Flags().String("telemetry", "", "Update pool telemetry: --telemetry=all (reset to default set), --telemetry=off (disable), or --telemetry=console,network (merge those categories into the current selection). Applies only to browsers warmed after the update.")
browserPoolsUpdateCmd.Flags().Bool("discard-all-idle", false, "Discard all idle browsers")
addJSONOutputFlag(browserPoolsUpdateCmd)

Expand All @@ -613,6 +679,7 @@ func init() {
browserPoolsAcquireCmd.Flags().Int64("timeout", 0, "Acquire timeout in seconds")
browserPoolsAcquireCmd.Flags().String("name", "", "Optional name for the acquired session (applies to this lease; cleared on release)")
browserPoolsAcquireCmd.Flags().StringArray("tag", nil, "Set a tag KEY=VALUE on the acquired session (repeatable; applies to this lease)")
browserPoolsAcquireCmd.Flags().String("telemetry", "", "Telemetry override for this lease only, merged onto the pool's config: --telemetry=all, --telemetry=off, or --telemetry=console,network")
addJSONOutputFlag(browserPoolsAcquireCmd)

browserPoolsReleaseCmd.Flags().String("session-id", "", "Browser session ID to release")
Expand Down Expand Up @@ -663,6 +730,7 @@ func runBrowserPoolsCreate(cmd *cobra.Command, args []string) error {
viewport, _ := cmd.Flags().GetString("viewport")
chromePolicy, _ := cmd.Flags().GetString("chrome-policy")
chromePolicyFile, _ := cmd.Flags().GetString("chrome-policy-file")
telemetry, _ := cmd.Flags().GetString("telemetry")
output, _ := cmd.Flags().GetString("output")

in := BrowserPoolsCreateInput{
Expand All @@ -682,6 +750,7 @@ func runBrowserPoolsCreate(cmd *cobra.Command, args []string) error {
Viewport: viewport,
ChromePolicy: chromePolicy,
ChromePolicyFile: chromePolicyFile,
Telemetry: telemetry,
Output: output,
}

Expand Down Expand Up @@ -716,6 +785,7 @@ func runBrowserPoolsUpdate(cmd *cobra.Command, args []string) error {
viewport, _ := cmd.Flags().GetString("viewport")
chromePolicy, _ := cmd.Flags().GetString("chrome-policy")
chromePolicyFile, _ := cmd.Flags().GetString("chrome-policy-file")
telemetry, _ := cmd.Flags().GetString("telemetry")
discardIdle, _ := cmd.Flags().GetBool("discard-all-idle")
output, _ := cmd.Flags().GetString("output")

Expand All @@ -738,6 +808,7 @@ func runBrowserPoolsUpdate(cmd *cobra.Command, args []string) error {
Viewport: viewport,
ChromePolicy: chromePolicy,
ChromePolicyFile: chromePolicyFile,
Telemetry: telemetry,
DiscardAllIdle: BoolFlag{Set: cmd.Flags().Changed("discard-all-idle"), Value: discardIdle},
Output: output,
}
Expand All @@ -758,13 +829,15 @@ func runBrowserPoolsAcquire(cmd *cobra.Command, args []string) error {
timeout, _ := cmd.Flags().GetInt64("timeout")
name, _ := cmd.Flags().GetString("name")
tags, _ := tagsFromFlag(cmd, "tag")
telemetry, _ := cmd.Flags().GetString("telemetry")
output, _ := cmd.Flags().GetString("output")
c := BrowserPoolsCmd{client: &client.BrowserPools}
return c.Acquire(cmd.Context(), BrowserPoolsAcquireInput{
IDOrName: args[0],
TimeoutSeconds: timeout,
Name: name,
Tags: tags,
Telemetry: telemetry,
Output: output,
})
}
Expand Down
113 changes: 109 additions & 4 deletions cmd/browser_pools_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,21 +120,29 @@ func TestBrowserPoolsList_ForwardsLimitOffset(t *testing.T) {
assert.Equal(t, int64(8), captured.Offset.Value)
}

// TestBuildAcquireParams covers the shared name/tags/timeout forwarding used by
// both `browser-pools acquire` and the `browsers create --pool-id` lease path.
// TestBuildAcquireParams covers the shared name/tags/timeout/telemetry forwarding
// used by both `browser-pools acquire` and the `browsers create --pool-id` lease path.
func TestBuildAcquireParams(t *testing.T) {
p := buildAcquireParams("lease", map[string]string{"env": "prod"}, 30)
p, err := buildAcquireParams("lease", map[string]string{"env": "prod"}, 30, "console,network")
assert.NoError(t, err)
assert.True(t, p.Name.Valid())
assert.Equal(t, "lease", p.Name.Value)
assert.Equal(t, "prod", p.Tags["env"])
assert.True(t, p.AcquireTimeoutSeconds.Valid())
assert.Equal(t, int64(30), p.AcquireTimeoutSeconds.Value)
assert.True(t, p.Telemetry.Browser.Console.Enabled.Value)
assert.True(t, p.Telemetry.Browser.Network.Enabled.Value)

// Unset inputs produce an empty params struct (nothing forwarded).
empty := buildAcquireParams("", nil, 0)
empty, err := buildAcquireParams("", nil, 0, "")
assert.NoError(t, err)
assert.False(t, empty.Name.Valid())
assert.Len(t, empty.Tags, 0)
assert.False(t, empty.AcquireTimeoutSeconds.Valid())

// An invalid category surfaces an error rather than a partial param.
_, err = buildAcquireParams("", nil, 0, "bogus")
assert.Error(t, err)
}

func TestBrowserPoolsCreate_WithRefreshOnProfileUpdate(t *testing.T) {
Expand Down Expand Up @@ -276,3 +284,100 @@ func TestBrowserPoolsUpdate_EmptyChromePolicyQuietInJSONMode(t *testing.T) {
// The warning must not leak onto stdout in json mode, where it would corrupt the payload.
assert.NotContains(t, outBuf.String(), "does not clear")
}

func TestBrowserPoolsCreate_WithTelemetry(t *testing.T) {
setupStdoutCapture(t)

var captured kernel.BrowserPoolNewParams
fake := &FakeBrowserPoolsService{
NewFunc: func(ctx context.Context, body kernel.BrowserPoolNewParams, opts ...option.RequestOption) (*kernel.BrowserPool, error) {
captured = body
return &kernel.BrowserPool{ID: "pool-tel"}, nil
},
}

c := BrowserPoolsCmd{client: fake}
err := c.Create(context.Background(), BrowserPoolsCreateInput{
Size: 1,
Telemetry: "console,network",
})
assert.NoError(t, err)
assert.True(t, captured.Telemetry.Browser.Console.Enabled.Value)
assert.True(t, captured.Telemetry.Browser.Network.Enabled.Value)
assert.False(t, captured.Telemetry.Enabled.Valid())
}

func TestBrowserPoolsCreate_TelemetryAllAndOff(t *testing.T) {
setupStdoutCapture(t)

var captured kernel.BrowserPoolNewParams
fake := &FakeBrowserPoolsService{
NewFunc: func(ctx context.Context, body kernel.BrowserPoolNewParams, opts ...option.RequestOption) (*kernel.BrowserPool, error) {
captured = body
return &kernel.BrowserPool{ID: "pool-tel"}, nil
},
}
c := BrowserPoolsCmd{client: fake}

assert.NoError(t, c.Create(context.Background(), BrowserPoolsCreateInput{Size: 1, Telemetry: "all"}))
assert.True(t, captured.Telemetry.Enabled.Value)

assert.NoError(t, c.Create(context.Background(), BrowserPoolsCreateInput{Size: 1, Telemetry: "off"}))
assert.True(t, captured.Telemetry.Enabled.Valid())
assert.False(t, captured.Telemetry.Enabled.Value)
}

func TestBrowserPoolsCreate_TelemetryInvalidCategory(t *testing.T) {
setupStdoutCapture(t)

fake := &FakeBrowserPoolsService{
NewFunc: func(ctx context.Context, body kernel.BrowserPoolNewParams, opts ...option.RequestOption) (*kernel.BrowserPool, error) {
t.Fatal("New should not be called when telemetry parsing fails")
return nil, nil
},
}
c := BrowserPoolsCmd{client: fake}
err := c.Create(context.Background(), BrowserPoolsCreateInput{Size: 1, Telemetry: "bogus"})
assert.Error(t, err)
}

func TestBrowserPoolsUpdate_WithTelemetry(t *testing.T) {
setupStdoutCapture(t)

var captured kernel.BrowserPoolUpdateParams
fake := &FakeBrowserPoolsService{
UpdateFunc: func(ctx context.Context, id string, body kernel.BrowserPoolUpdateParams, opts ...option.RequestOption) (*kernel.BrowserPool, error) {
captured = body
return &kernel.BrowserPool{ID: id}, nil
},
}

c := BrowserPoolsCmd{client: fake}
err := c.Update(context.Background(), BrowserPoolsUpdateInput{
IDOrName: "pool-1",
Telemetry: "off",
})
assert.NoError(t, err)
assert.True(t, captured.Telemetry.Enabled.Valid())
assert.False(t, captured.Telemetry.Enabled.Value)
}

func TestBrowserPoolsAcquire_WithTelemetryOverride(t *testing.T) {
setupStdoutCapture(t)

var captured kernel.BrowserPoolAcquireParams
fake := &FakeBrowserPoolsService{
AcquireFunc: func(ctx context.Context, id string, body kernel.BrowserPoolAcquireParams, opts ...option.RequestOption) (*kernel.BrowserPoolAcquireResponse, error) {
captured = body
return &kernel.BrowserPoolAcquireResponse{SessionID: "sess-1"}, nil
},
}

c := BrowserPoolsCmd{client: fake}
err := c.Acquire(context.Background(), BrowserPoolsAcquireInput{
IDOrName: "pool-1",
Telemetry: "page",
})
assert.NoError(t, err)
assert.True(t, captured.Telemetry.Browser.Page.Enabled.Value)
}
Loading
Loading