From 9cf24318fab8b6c38e54c143242ae353093babeb Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Mon, 17 Aug 2026 20:47:32 +0000 Subject: [PATCH 1/8] CLI: Update SDK to 0a28735 and add org entitlements command Bump github.com/kernel/kernel-go-sdk to v0.91.1-0.20260817203807-0a287359dcc5 (0a28735). Coverage gap found by enumerating all 140 methods in the SDK's api.md against the CLI command tree: the new Organization.Entitlements resource had no CLI surface. Everything else was already covered. New command: - `kernel org entitlements get` for client.Organization.Entitlements.Get (GET /org/entitlements). Renders Plan, Features, and Limits sections; supports --output json. Null constraint values mean unlimited in this API, and the SDK models them as non-pointer int64, so rendering keys off respjson field validity rather than the zero value. Tested against the real API: - kernel org entitlements get (table output, ENTERPRISE plan) - kernel org entitlements get --output json - kernel org entitlements get --output yaml (rejected as expected) - go build ./... and go test ./... pass, including 5 new unit tests covering populated constraints, null-as-unlimited, null plan fields, invalid --output, and API errors. Co-Authored-By: Claude Opus 5 --- README.md | 2 + cmd/org.go | 157 +++++++++++++++++++++++++++++++++++++++++++++++- cmd/org_test.go | 148 +++++++++++++++++++++++++++++++++++++++++++++ go.mod | 2 +- go.sum | 4 +- 5 files changed, 308 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 96eed04e..64611f30 100644 --- a/README.md +++ b/README.md @@ -729,6 +729,8 @@ Automated authentication for web services. The `run` command orchestrates the fu - `kernel org limits set` - Set the default per-project concurrency cap applied to projects without an explicit override - `--default-project-max-concurrent-sessions ` - Default maximum concurrent browsers for projects without an explicit override (`0` to remove the default) - `--output json`, `-o json` - Output raw JSON object +- `kernel org entitlements get` - Show the organization's effective feature access and constraints after applying its plan, active trial treatment, plan status, and organization-specific overrides; unlimited constraints are shown as `unlimited` + - `--output json`, `-o json` - Output raw JSON object ## Examples diff --git a/cmd/org.go b/cmd/org.go index ffd8c02b..790087bc 100644 --- a/cmd/org.go +++ b/cmd/org.go @@ -3,6 +3,7 @@ package cmd import ( "context" "fmt" + "time" "github.com/kernel/cli/pkg/util" "github.com/kernel/kernel-go-sdk" @@ -10,6 +11,7 @@ import ( "github.com/kernel/kernel-go-sdk/packages/param" "github.com/kernel/kernel-go-sdk/packages/respjson" "github.com/pterm/pterm" + "github.com/samber/lo" "github.com/spf13/cobra" ) @@ -19,14 +21,24 @@ type OrgLimitsService interface { Update(ctx context.Context, body kernel.OrganizationLimitUpdateParams, opts ...option.RequestOption) (res *kernel.OrgLimits, err error) } +// OrgEntitlementsService defines the subset of the Kernel SDK organization entitlements client that we use. +type OrgEntitlementsService interface { + Get(ctx context.Context, opts ...option.RequestOption) (res *kernel.OrgEntitlements, err error) +} + type OrgCmd struct { - limits OrgLimitsService + limits OrgLimitsService + entitlements OrgEntitlementsService } type OrgLimitsGetInput struct { Output string } +type OrgEntitlementsGetInput struct { + Output string +} + type OrgLimitsSetInput struct { DefaultProjectMaxConcurrentSessions Int64Flag Output string @@ -88,6 +100,118 @@ func (c OrgCmd) LimitsSet(ctx context.Context, in OrgLimitsSetInput) error { return nil } +func (c OrgCmd) EntitlementsGet(ctx context.Context, in OrgEntitlementsGetInput) error { + if err := validateJSONOutput(in.Output); err != nil { + return err + } + + entitlements, err := c.entitlements.Get(ctx) + if err != nil { + return util.CleanedUpSdkError{Err: err} + } + + if in.Output == "json" { + if entitlements == nil { + fmt.Println("null") + return nil + } + return util.PrintPrettyJSON(entitlements) + } + + renderOrgEntitlements(entitlements) + return nil +} + +func renderOrgEntitlements(ent *kernel.OrgEntitlements) { + if ent == nil { + pterm.Info.Println("No organization entitlements found") + return + } + + plan := ent.Plan + planRows := pterm.TableData{ + {"Field", "Value"}, + {"Plan", plan.ID}, + // Active trials resolve to a different effective plan than the + // contractual one, so show both. + {"Effective Plan", plan.EffectiveID}, + {"Trialing", lo.Ternary(plan.IsTrialing, "yes", "no")}, + // Billing status and trial end are both nullable. + {"Billing Status", formatOrgEntitlementString(plan.Status, plan.JSON.Status)}, + {"Trial Ends At", formatOrgEntitlementTime(plan.TrialEndsAt, plan.JSON.TrialEndsAt)}, + } + pterm.DefaultSection.Println("Plan") + PrintTableNoPad(planRows, true) + + f := ent.Features + featureRows := pterm.TableData{ + {"Feature", "Enabled", "Constraints"}, + {"Browser Extensions", formatOrgEntitlementEnabled(f.BrowserExtensions.Enabled), fmt.Sprintf("max stored per org: %s", formatProjectLimitValue(f.BrowserExtensions.MaxStoredPerOrg, f.BrowserExtensions.JSON.MaxStoredPerOrg))}, + {"Browser Pools", formatOrgEntitlementEnabled(f.BrowserPools.Enabled), ""}, + {"Browser Replays", formatOrgEntitlementEnabled(f.BrowserReplays.Enabled), fmt.Sprintf("retention: %s", formatOrgEntitlementDays(f.BrowserReplays.RetentionDays, f.BrowserReplays.JSON.RetentionDays))}, + {"Credential Providers", formatOrgEntitlementEnabled(f.CredentialProviders.Enabled), ""}, + {"Credentials", formatOrgEntitlementEnabled(f.Credentials.Enabled), ""}, + {"Custom Proxies", formatOrgEntitlementEnabled(f.CustomProxies.Enabled), ""}, + {"File I/O", formatOrgEntitlementEnabled(f.FileIo.Enabled), ""}, + {"GPU", formatOrgEntitlementEnabled(f.GPU.Enabled), ""}, + {"Managed Auth", formatOrgEntitlementEnabled(f.ManagedAuth.Enabled), formatManagedAuthConstraints(f.ManagedAuth)}, + {"Managed Proxies", formatOrgEntitlementEnabled(f.ManagedProxies.Enabled), ""}, + {"Profiles", formatOrgEntitlementEnabled(f.Profiles.Enabled), ""}, + {"Proxy Bypass Hosts", formatOrgEntitlementEnabled(f.ProxyBypassHosts.Enabled), ""}, + } + pterm.DefaultSection.Println("Features") + PrintTableNoPad(featureRows, true) + + l := ent.Limits + limitRows := pterm.TableData{ + {"Limit", "Value"}, + {"Max Concurrent Browsers", formatProjectLimitValue(l.MaxConcurrentBrowsers, l.JSON.MaxConcurrentBrowsers)}, + {"Max Concurrent Invocations", formatProjectLimitValue(l.MaxConcurrentInvocations, l.JSON.MaxConcurrentInvocations)}, + {"Default Max Concurrent Invocations Per App", formatProjectLimitValue(l.DefaultMaxConcurrentInvocationsPerApp, l.JSON.DefaultMaxConcurrentInvocationsPerApp)}, + } + pterm.DefaultSection.Println("Limits") + PrintTableNoPad(limitRows, true) +} + +func formatOrgEntitlementEnabled(enabled bool) string { + return lo.Ternary(enabled, "yes", "no") +} + +// formatManagedAuthConstraints summarizes the managed auth connection cap and the +// accepted health-check interval window in a single cell. +func formatManagedAuthConstraints(ma kernel.OrgEntitlementsFeaturesManagedAuth) string { + return fmt.Sprintf( + "max connections: %s, health check interval: %ds default (%ds-%ds)", + formatProjectLimitValue(ma.MaxConnections, ma.JSON.MaxConnections), + ma.HealthCheckIntervalDefaultSeconds, + ma.HealthCheckIntervalMinSeconds, + ma.HealthCheckIntervalMaxSeconds, + ) +} + +// formatOrgEntitlementDays renders a retention window, treating a null value as +// unlimited retention rather than zero days. +func formatOrgEntitlementDays(value int64, field respjson.Field) string { + if !field.Valid() { + return "unlimited" + } + return fmt.Sprintf("%d days", value) +} + +func formatOrgEntitlementString(value string, field respjson.Field) string { + if !field.Valid() || value == "" { + return "-" + } + return value +} + +func formatOrgEntitlementTime(value time.Time, field respjson.Field) string { + if !field.Valid() || value.IsZero() { + return "-" + } + return util.FormatLocal(value) +} + func renderOrgLimits(limits *kernel.OrgLimits) { if limits == nil { pterm.Info.Println("No organization limits found") @@ -144,6 +268,22 @@ var orgLimitsCmd = &cobra.Command{ }, } +var orgEntitlementsCmd = &cobra.Command{ + Use: "entitlements", + Short: "Read organization entitlements", + Run: func(cmd *cobra.Command, args []string) { + _ = cmd.Help() + }, +} + +var orgEntitlementsGetCmd = &cobra.Command{ + Use: "get", + Short: "Get organization entitlements", + Long: "Show the organization's effective feature access and constraints after applying its plan, active trial treatment, plan status, and organization-specific overrides. Unlimited constraints are shown as \"unlimited\".", + Args: cobra.NoArgs, + RunE: runOrgEntitlementsGet, +} + var orgLimitsGetCmd = &cobra.Command{ Use: "get", Short: "Get organization limits", @@ -162,7 +302,16 @@ var orgLimitsSetCmd = &cobra.Command{ func getOrgHandler(cmd *cobra.Command) OrgCmd { client := getKernelClient(cmd) - return OrgCmd{limits: &client.Organization.Limits} + return OrgCmd{ + limits: &client.Organization.Limits, + entitlements: &client.Organization.Entitlements, + } +} + +func runOrgEntitlementsGet(cmd *cobra.Command, args []string) error { + c := getOrgHandler(cmd) + output, _ := cmd.Flags().GetString("output") + return c.EntitlementsGet(cmd.Context(), OrgEntitlementsGetInput{Output: output}) } func runOrgLimitsGet(cmd *cobra.Command, args []string) error { @@ -189,7 +338,11 @@ func init() { orgLimitsSetCmd.Flags().Int64("default-project-max-concurrent-sessions", 0, "Default maximum concurrent browsers for projects without an explicit override (0 to remove the default)") addJSONOutputFlag(orgLimitsSetCmd) + addJSONOutputFlag(orgEntitlementsGetCmd) + orgLimitsCmd.AddCommand(orgLimitsGetCmd) orgLimitsCmd.AddCommand(orgLimitsSetCmd) + orgEntitlementsCmd.AddCommand(orgEntitlementsGetCmd) orgCmd.AddCommand(orgLimitsCmd) + orgCmd.AddCommand(orgEntitlementsCmd) } diff --git a/cmd/org_test.go b/cmd/org_test.go index 946800b0..ea2110db 100644 --- a/cmd/org_test.go +++ b/cmd/org_test.go @@ -4,6 +4,7 @@ import ( "context" "errors" "testing" + "time" "github.com/kernel/kernel-go-sdk" "github.com/kernel/kernel-go-sdk/option" @@ -177,3 +178,150 @@ func TestOrgLimitsSet_RejectsNegative(t *testing.T) { assert.Error(t, err) assert.Contains(t, err.Error(), "must be non-negative") } + +type FakeOrgEntitlementsService struct { + GetFunc func(ctx context.Context, opts ...option.RequestOption) (*kernel.OrgEntitlements, error) +} + +func (f *FakeOrgEntitlementsService) Get(ctx context.Context, opts ...option.RequestOption) (*kernel.OrgEntitlements, error) { + if f.GetFunc != nil { + return f.GetFunc(ctx, opts...) + } + return &kernel.OrgEntitlements{}, nil +} + +// populatedEntitlements builds an entitlements payload with every nullable field +// present, so renders exercise the non-"unlimited" branches. +func populatedEntitlements() *kernel.OrgEntitlements { + ent := &kernel.OrgEntitlements{} + + ent.Plan.ID = "START_UP" + ent.Plan.EffectiveID = "START_UP" + ent.Plan.IsTrialing = true + ent.Plan.Status = "ACTIVE" + ent.Plan.TrialEndsAt = time.Date(2030, 1, 2, 3, 4, 5, 0, time.UTC) + ent.Plan.JSON.Status = respjson.NewField(`"ACTIVE"`) + ent.Plan.JSON.TrialEndsAt = respjson.NewField(`"2030-01-02T03:04:05Z"`) + + ent.Features.BrowserExtensions.Enabled = true + ent.Features.BrowserExtensions.MaxStoredPerOrg = 25 + ent.Features.BrowserExtensions.JSON.MaxStoredPerOrg = respjson.NewField("25") + ent.Features.BrowserPools.Enabled = true + ent.Features.BrowserReplays.Enabled = true + ent.Features.BrowserReplays.RetentionDays = 7 + ent.Features.BrowserReplays.JSON.RetentionDays = respjson.NewField("7") + ent.Features.CredentialProviders.Enabled = true + ent.Features.Credentials.Enabled = true + ent.Features.CustomProxies.Enabled = false + ent.Features.FileIo.Enabled = true + ent.Features.GPU.Enabled = false + ent.Features.ManagedAuth.Enabled = true + ent.Features.ManagedAuth.MaxConnections = 10 + ent.Features.ManagedAuth.HealthCheckIntervalDefaultSeconds = 600 + ent.Features.ManagedAuth.HealthCheckIntervalMinSeconds = 300 + ent.Features.ManagedAuth.HealthCheckIntervalMaxSeconds = 86400 + ent.Features.ManagedAuth.JSON.MaxConnections = respjson.NewField("10") + ent.Features.ManagedProxies.Enabled = true + ent.Features.Profiles.Enabled = true + ent.Features.ProxyBypassHosts.Enabled = true + + ent.Limits.MaxConcurrentBrowsers = 50 + ent.Limits.MaxConcurrentInvocations = 20 + ent.Limits.DefaultMaxConcurrentInvocationsPerApp = 5 + ent.Limits.JSON.MaxConcurrentBrowsers = respjson.NewField("50") + ent.Limits.JSON.MaxConcurrentInvocations = respjson.NewField("20") + ent.Limits.JSON.DefaultMaxConcurrentInvocationsPerApp = respjson.NewField("5") + + return ent +} + +func TestOrgEntitlementsGet_RendersPlanFeaturesAndLimits(t *testing.T) { + buf := capturePtermOutput(t) + fake := &FakeOrgEntitlementsService{ + GetFunc: func(ctx context.Context, opts ...option.RequestOption) (*kernel.OrgEntitlements, error) { + return populatedEntitlements(), nil + }, + } + c := OrgCmd{entitlements: fake} + assert.NoError(t, c.EntitlementsGet(context.Background(), OrgEntitlementsGetInput{})) + + out := buf.String() + // Plan section + assert.Contains(t, out, "START_UP") + assert.Contains(t, out, "Effective Plan") + assert.Contains(t, out, "Trialing") + assert.Contains(t, out, "ACTIVE") + // Features section — every feature should get a row. + for _, feature := range []string{ + "Browser Extensions", "Browser Pools", "Browser Replays", "Credential Providers", + "Credentials", "Custom Proxies", "File I/O", "GPU", "Managed Auth", + "Managed Proxies", "Profiles", "Proxy Bypass Hosts", + } { + assert.Contains(t, out, feature) + } + assert.Contains(t, out, "max stored per org: 25") + assert.Contains(t, out, "retention: 7 days") + assert.Contains(t, out, "max connections: 10") + assert.Contains(t, out, "600s default (300s-86400s)") + // Limits section + assert.Contains(t, out, "Max Concurrent Browsers") + assert.Contains(t, out, "Max Concurrent Invocations") + assert.Contains(t, out, "Default Max Concurrent Invocations Per App") +} + +func TestOrgEntitlementsGet_NullConstraintsShownAsUnlimited(t *testing.T) { + buf := capturePtermOutput(t) + fake := &FakeOrgEntitlementsService{ + GetFunc: func(ctx context.Context, opts ...option.RequestOption) (*kernel.OrgEntitlements, error) { + ent := populatedEntitlements() + // Null (not omitted) constraints mean unlimited. + ent.Features.BrowserExtensions.JSON.MaxStoredPerOrg = respjson.NewField(respjson.Null) + ent.Features.ManagedAuth.JSON.MaxConnections = respjson.NewField(respjson.Null) + ent.Limits.JSON.MaxConcurrentBrowsers = respjson.NewField(respjson.Null) + return ent, nil + }, + } + c := OrgCmd{entitlements: fake} + assert.NoError(t, c.EntitlementsGet(context.Background(), OrgEntitlementsGetInput{})) + + out := buf.String() + assert.Contains(t, out, "max stored per org: unlimited") + assert.Contains(t, out, "max connections: unlimited") + assert.Contains(t, out, "unlimited") +} + +func TestOrgEntitlementsGet_NullPlanFieldsShownAsDash(t *testing.T) { + buf := capturePtermOutput(t) + fake := &FakeOrgEntitlementsService{ + GetFunc: func(ctx context.Context, opts ...option.RequestOption) (*kernel.OrgEntitlements, error) { + ent := populatedEntitlements() + ent.Plan.IsTrialing = false + ent.Plan.JSON.Status = respjson.NewField(respjson.Null) + ent.Plan.JSON.TrialEndsAt = respjson.NewField(respjson.Null) + return ent, nil + }, + } + c := OrgCmd{entitlements: fake} + assert.NoError(t, c.EntitlementsGet(context.Background(), OrgEntitlementsGetInput{})) + + out := buf.String() + assert.Contains(t, out, "Billing Status") + assert.Contains(t, out, "Trial Ends At") + assert.NotContains(t, out, "ACTIVE") +} + +func TestOrgEntitlementsGet_RejectsUnknownOutput(t *testing.T) { + c := OrgCmd{entitlements: &FakeOrgEntitlementsService{}} + assert.Error(t, c.EntitlementsGet(context.Background(), OrgEntitlementsGetInput{Output: "yaml"})) +} + +func TestOrgEntitlementsGet_SurfacesAPIError(t *testing.T) { + capturePtermOutput(t) + fake := &FakeOrgEntitlementsService{ + GetFunc: func(ctx context.Context, opts ...option.RequestOption) (*kernel.OrgEntitlements, error) { + return nil, errors.New("boom") + }, + } + c := OrgCmd{entitlements: fake} + assert.Error(t, c.EntitlementsGet(context.Background(), OrgEntitlementsGetInput{})) +} diff --git a/go.mod b/go.mod index ebe99635..85d0a029 100644 --- a/go.mod +++ b/go.mod @@ -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.91.0 + github.com/kernel/kernel-go-sdk v0.91.1-0.20260817203807-0a287359dcc5 github.com/klauspost/compress v1.18.5 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/pterm/pterm v0.12.80 diff --git a/go.sum b/go.sum index 7808eb39..b91fd6b3 100644 --- a/go.sum +++ b/go.sum @@ -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.91.0 h1:/bJKFJQ8ZwAyl+r8P1sUW8NQYEjDekYZJ5R8Sml5bus= -github.com/kernel/kernel-go-sdk v0.91.0/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= +github.com/kernel/kernel-go-sdk v0.91.1-0.20260817203807-0a287359dcc5 h1:Kaq0Dhh1VW36HzqOUOpvWnB1PF3XtPekC++RYdgePNQ= +github.com/kernel/kernel-go-sdk v0.91.1-0.20260817203807-0a287359dcc5/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= From dfdba4fccc49d1e67bce163b8122769024932fbc Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Mon, 17 Aug 2026 21:07:08 +0000 Subject: [PATCH 2/8] CLI: Update Go SDK to v0.92.0 (a156820) Updates github.com/kernel/kernel-go-sdk from v0.91.1-0.20260817203807-0a287359dcc5 to v0.92.0. ## Coverage Analysis Diffing the two module sources shows the SDK API surface is byte-identical between these versions -- the only changes are release metadata (.release-please-manifest.json, CHANGELOG.md, README.md, internal/version.go). A full enumeration was still performed: - All 140 SDK methods in api.md have corresponding CLI commands. - The 4 x-cli-skip endpoints (/site-configs/lookup, /site-configs/resolve, /site-configs/analyses/{id}, /auth/connections/{id}/exchange) are absent from the SDK surface, so nothing to skip. - All params struct fields are covered by CLI flags except three, each intentional: - AuthConnectionLoginParams.BrowserTelemetry -- deprecated in favor of browser.telemetry, which the CLI already uses via ManagedAuthBrowserConfigParam. - AuditLogListParams.PageToken -- opaque cursor handled internally by ListAutoPaging; CLI exposes --limit instead. - BrowserCurlParams.TimeoutMs / ResponseEncoding -- `browsers curl` is implemented against browsers.HTTPClient rather than the SDK curl endpoint; --max-time covers the timeout and raw bytes are streamed, so response encoding is not applicable. No coverage gaps found; no new commands or flags added. ## Tested - go build ./... and go vet ./... clean - go test ./... all packages pass - Smoke tested rebuilt binary against the live API: `kernel browsers list` Triggered by: kernel/kernel-go-sdk@a1568205c576686eeafc634fff0ea72b75c28c0e Co-Authored-By: Claude Opus 5 --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 85d0a029..502421b9 100644 --- a/go.mod +++ b/go.mod @@ -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.91.1-0.20260817203807-0a287359dcc5 + github.com/kernel/kernel-go-sdk v0.92.0 github.com/klauspost/compress v1.18.5 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/pterm/pterm v0.12.80 diff --git a/go.sum b/go.sum index b91fd6b3..04679b80 100644 --- a/go.sum +++ b/go.sum @@ -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.91.1-0.20260817203807-0a287359dcc5 h1:Kaq0Dhh1VW36HzqOUOpvWnB1PF3XtPekC++RYdgePNQ= -github.com/kernel/kernel-go-sdk v0.91.1-0.20260817203807-0a287359dcc5/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= +github.com/kernel/kernel-go-sdk v0.92.0 h1:3EeoPahTcGEo97BCbwT50gu8QJnawfL166z12hc8Ucg= +github.com/kernel/kernel-go-sdk v0.92.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= From 8a7b36334f3cc3f878df9b17d90da78d24e5b6d2 Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Tue, 18 Aug 2026 21:13:13 +0000 Subject: [PATCH 3/8] CLI: Update Go SDK to 6e62bf5 and track managed-auth field reason Bumps kernel-go-sdk to 6e62bf5b91e5d315b90b6c9c7296e09e312fb338. That SDK release reshapes the canonical managed-auth input field: the boolean `replace_existing` is gone and a `reason` enum ("missing" | "rejected") takes its place, so `auth connections get` and the `auth connections follow` event stream now render `reason=` instead of the `replace-existing` marker. A rejected credential is still visible, now alongside the missing-value case it could not previously express. A full enumeration of api.md against the CLI's service interfaces and flags found no other coverage gaps: all 136 non-x-cli-skip SDK methods have commands, and every params field maps to an existing flag. Tested: auth connections list, auth connections get (table + json), browsers create -t 60, browsers get , browsers delete against the live API; go build ./... and go test ./cmd/... pass. Co-Authored-By: Claude Opus 5 --- cmd/auth_connections.go | 46 ++++++++++++++++++------------------ cmd/auth_connections_test.go | 5 +++- go.mod | 2 +- go.sum | 4 ++-- 4 files changed, 30 insertions(+), 27 deletions(-) diff --git a/cmd/auth_connections.go b/cmd/auth_connections.go index 79a34567..4237e9a3 100644 --- a/cmd/auth_connections.go +++ b/cmd/auth_connections.go @@ -414,13 +414,13 @@ func (c AuthConnectionCmd) Update(ctx context.Context, in AuthConnectionUpdateIn // models the one on `get` and the one on the `follow` event stream as two // identical but distinct types, so both are converted to this before rendering. type managedAuthInputField struct { - ID string - Label string - Type string - Ref string - Hint string - Required bool - ReplaceExisting bool + ID string + Label string + Type string + Ref string + Hint string + Reason string + Required bool } // managedAuthInputChoice is the choice counterpart of managedAuthInputField. @@ -448,8 +448,8 @@ func formatManagedAuthField(f managedAuthInputField) string { if f.Required { meta = append(meta, "required") } - if f.ReplaceExisting { - meta = append(meta, "replace-existing") + if f.Reason != "" { + meta = append(meta, "reason="+f.Reason) } if f.Hint != "" { meta = append(meta, fmt.Sprintf("hint=%q", f.Hint)) @@ -542,13 +542,13 @@ func (c AuthConnectionCmd) Get(ctx context.Context, in AuthConnectionGetInput) e fields := make([]string, 0, len(auth.Fields)) for _, f := range auth.Fields { fields = append(fields, formatManagedAuthField(managedAuthInputField{ - ID: f.ID, - Label: f.Label, - Type: f.Type, - Ref: f.Ref, - Hint: f.Hint, - Required: f.Required, - ReplaceExisting: f.ReplaceExisting, + ID: f.ID, + Label: f.Label, + Type: f.Type, + Ref: f.Ref, + Hint: f.Hint, + Reason: f.Reason, + Required: f.Required, })) } tableData = append(tableData, []string{"Fields", strings.Join(fields, "; ")}) @@ -1067,13 +1067,13 @@ func (c AuthConnectionCmd) Follow(ctx context.Context, in AuthConnectionFollowIn fields := make([]string, 0, len(state.Fields)) for _, f := range state.Fields { fields = append(fields, formatManagedAuthField(managedAuthInputField{ - ID: f.ID, - Label: f.Label, - Type: f.Type, - Ref: f.Ref, - Hint: f.Hint, - Required: f.Required, - ReplaceExisting: f.ReplaceExisting, + ID: f.ID, + Label: f.Label, + Type: f.Type, + Ref: f.Ref, + Hint: f.Hint, + Reason: f.Reason, + Required: f.Required, })) } pterm.Info.Printf(" Fields: %s\n", strings.Join(fields, ", ")) diff --git a/cmd/auth_connections_test.go b/cmd/auth_connections_test.go index b1c654a9..a403464e 100644 --- a/cmd/auth_connections_test.go +++ b/cmd/auth_connections_test.go @@ -154,6 +154,7 @@ func TestAuthConnectionsGet_PrintsCanonicalInputMetadata(t *testing.T) { Type: "code", Ref: "totp_code", Hint: "Enter the code sent to +1 ••• ••• 1234", + Reason: "rejected", Required: true, }, }, @@ -182,7 +183,9 @@ func TestAuthConnectionsGet_PrintsCanonicalInputMetadata(t *testing.T) { out := outBuf.String() assert.Contains(t, out, `otp (One-time code)`) - assert.Contains(t, out, `code, ref=totp_code, required`) + // The reason tells the user why the field is being asked for: "rejected" + // means a stored credential was refused, so a new value has to replace it. + assert.Contains(t, out, `code, ref=totp_code, required, reason=rejected`) assert.Contains(t, out, `hint="Enter the code sent to +1 ••• ••• 1234"`) assert.Contains(t, out, `mfa_sms (Text message)`) assert.Contains(t, out, `mfa_method, sms, to=+1 ••• ••• 1234`) diff --git a/go.mod b/go.mod index 502421b9..52d4a655 100644 --- a/go.mod +++ b/go.mod @@ -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.92.0 + github.com/kernel/kernel-go-sdk v0.92.1-0.20260818210401-6e62bf5b91e5 github.com/klauspost/compress v1.18.5 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/pterm/pterm v0.12.80 diff --git a/go.sum b/go.sum index 04679b80..ef49c798 100644 --- a/go.sum +++ b/go.sum @@ -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.92.0 h1:3EeoPahTcGEo97BCbwT50gu8QJnawfL166z12hc8Ucg= -github.com/kernel/kernel-go-sdk v0.92.0/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= +github.com/kernel/kernel-go-sdk v0.92.1-0.20260818210401-6e62bf5b91e5 h1:xnui88jn6CAp2Ys15AP7aagFGPveqrT/3LfdDsuIeY4= +github.com/kernel/kernel-go-sdk v0.92.1-0.20260818210401-6e62bf5b91e5/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= From ca46838accfb10bf06678d3de3185d3dbca51f3a Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Wed, 19 Aug 2026 19:02:46 +0000 Subject: [PATCH 4/8] CLI: Update Go SDK to 796d424 and bind canonical submits to interactions Bumps kernel-go-sdk to 796d4245c87a39acbb0d408b05f0de830c500772. That SDK release adds `interaction_id` to managed auth state and to the submit request. The API requires it for canonical submissions (field_values / selected_choice_id) and rejects it when paired with a legacy submit mode, so before this change every canonical `auth connections submit` failed with "interaction_id is required for canonical submissions". `auth connections submit` gains --interaction-id. Left off, the CLI reads the connection's current interaction ID, since the ID changes on every actionable pause and the freshly read one is the only sane default; passing it pins the submission so the API can reject it as stale. Legacy submit modes never send one, and --interaction-id with a legacy mode is rejected locally with the same rule the API enforces. `auth connections get` and `follow` now show the interaction ID next to the canonical fields and choices it scopes. Also resolves the stale merge of main into this branch, which had left two competing org entitlements implementations in cmd/org.go (the branch built `org entitlements get`; main shipped `org entitlements` in #232) so the package no longer compiled. Main's reviewed version wins. A full enumeration of api.md against the CLI found no other gaps: all 136 non-x-cli-skip SDK methods have commands, and the only new params field in this release is SubmitFieldsRequest.interaction_id. Tested against the live API: created a managed auth connection, started a login flow, and confirmed `get` (table + JSON) and `follow` render the interaction ID at AWAITING_INPUT; canonical submit with and without --interaction-id now clears the API's interaction validation (it stops at this org's submit-v2 feature gate, while the same request sent without interaction_id still returns "interaction_id is required"); legacy `--field` submit still accepted; `--interaction-id` with `--field` rejected locally; org entitlements, browsers create/get/delete pass. go build ./..., go vet ./... and go test ./... pass. Co-Authored-By: Claude Opus 5 --- README.md | 3 +- cmd/auth_connections.go | 54 ++++++++++++- cmd/auth_connections_test.go | 127 +++++++++++++++++++++++++++--- cmd/org.go | 137 -------------------------------- cmd/org_test.go | 147 ----------------------------------- go.mod | 2 +- go.sum | 4 +- 7 files changed, 174 insertions(+), 300 deletions(-) diff --git a/README.md b/README.md index b45c27af..4f7655e4 100644 --- a/README.md +++ b/README.md @@ -619,6 +619,7 @@ Managed auth connections (`kernel auth connections`). The commands below are new - `kernel auth connections submit ` - New flags: - `--field-value ` - Canonical field-id=value pair from the connection's `fields` list (repeatable); preferred over the legacy `--field` - `--choice-id ` - Canonical choice ID from the connection's `choices` list + - `--interaction-id ` - Canonical interaction the submitted values answer. Only valid with `--field-value` or `--choice-id`; omit it and the CLI reads the connection's current interaction ID for you. Pass it to pin the submission, so the API rejects it if the flow has already moved on. `kernel auth connections get` and `follow` list those IDs alongside the metadata the API captured for them, so you can tell the options apart before submitting. Fields show their type, ref, and any hint (which names the masked destination a one-time code was sent to); choices show their type, semantic MFA method (`sms`, `totp`, `push`, …), and masked destination. @@ -731,8 +732,6 @@ Automated authentication for web services. The `run` command orchestrates the fu - `kernel org limits set` - Set the default per-project concurrency cap applied to projects without an explicit override - `--default-project-max-concurrent-sessions ` - Default maximum concurrent browsers for projects without an explicit override (`0` to remove the default) - `--output json`, `-o json` - Output raw JSON object -- `kernel org entitlements get` - Show the organization's effective feature access and constraints after applying its plan, active trial treatment, plan status, and organization-specific overrides; unlimited constraints are shown as `unlimited` - - `--output json`, `-o json` - Output raw JSON object ## Examples diff --git a/cmd/auth_connections.go b/cmd/auth_connections.go index 4237e9a3..c6aedcfa 100644 --- a/cmd/auth_connections.go +++ b/cmd/auth_connections.go @@ -130,7 +130,11 @@ type AuthConnectionSubmitInput struct { // canonical `field_values` keyed by the field IDs the API returned. CanonicalFieldValues map[string]string // SelectedChoiceID is the canonical choice ID from the API's `choices` list. - SelectedChoiceID string + SelectedChoiceID string + // InteractionID pins the submission to the canonical interaction the values + // were read from. Left empty, the CLI reads the connection's current + // interaction ID, since the API requires one for canonical submissions. + InteractionID string MfaOptionID string SignInOptionID string SSOButtonSelector string @@ -538,6 +542,11 @@ func (c AuthConnectionCmd) Get(ctx context.Context, in AuthConnectionGetInput) e // Canonical fields/choices supersede discovered_fields, mfa_options and // pending_sso_buttons. Show them first so the IDs needed by `submit // --field-value` and `submit --choice-id` are the first thing visible. + // The interaction ID scopes those submissions and only accompanies canonical + // input, so show it alongside them. + if auth.InteractionID != "" { + tableData = append(tableData, []string{"Interaction ID", auth.InteractionID}) + } if len(auth.Fields) > 0 { fields := make([]string, 0, len(auth.Fields)) for _, f := range auth.Fields { @@ -838,6 +847,28 @@ func (c AuthConnectionCmd) Submit(ctx context.Context, in AuthConnectionSubmitIn return fmt.Errorf("provide exactly one of: %s", submitModeFlags) } + // The API binds canonical submissions to the interaction the values were read + // from, and rejects an interaction ID sent with a legacy submit mode. + isCanonical := hasCanonicalFields || hasChoice + if in.InteractionID != "" && !isCanonical { + return fmt.Errorf("the --interaction-id flag is only valid with --field-value or --choice-id") + } + if isCanonical && in.InteractionID == "" { + // Resolve the current interaction rather than making the user copy it out + // of `get` or `follow` first. The ID changes on every actionable pause, so + // the freshly read one is the only one worth defaulting to; passing + // --interaction-id explicitly pins the submission to an older interaction + // and lets the API reject it as stale. + conn, err := c.svc.Get(ctx, in.ID) + if err != nil { + return util.CleanedUpSdkError{Err: fmt.Errorf("failed to fetch connection for interaction ID resolution: %w", err)} + } + if conn == nil || conn.InteractionID == "" { + return fmt.Errorf("connection %s has no canonical interaction awaiting input; run 'kernel auth connections get %s' to see what the flow is waiting on", in.ID, in.ID) + } + in.InteractionID = conn.InteractionID + } + // Resolve MFA option: the user may pass the label (e.g. "Get a text"), the // type (e.g. "sms"), or the display string ("Get a text (sms)"). The API // expects the type, so look up the connection's available options and map @@ -884,6 +915,9 @@ func (c AuthConnectionCmd) Submit(ctx context.Context, in AuthConnectionSubmitIn if hasChoice { params.SubmitFieldsRequest.SelectedChoiceID = kernel.Opt(in.SelectedChoiceID) } + if in.InteractionID != "" { + params.SubmitFieldsRequest.InteractionID = kernel.Opt(in.InteractionID) + } if hasMfaOption { params.SubmitFieldsRequest.MfaOptionID = kernel.Opt(in.MfaOptionID) } @@ -1063,6 +1097,9 @@ func (c AuthConnectionCmd) Follow(ctx context.Context, in AuthConnectionFollowIn state.Timestamp.Local().Format(time.RFC3339), state.FlowStatus, state.FlowStep) + if state.InteractionID != "" { + pterm.Info.Printf(" Interaction ID: %s\n", state.InteractionID) + } if len(state.Fields) > 0 { fields := make([]string, 0, len(state.Fields)) for _, f := range state.Fields { @@ -1181,8 +1218,18 @@ var authConnectionsSubmitCmd = &cobra.Command{ Short: "Submit field values to a login flow", Long: `Submit field values for the login form. Poll the managed auth to track progress. +Canonical submissions (--field-value, --choice-id) are bound to the interaction +they answer. The CLI reads the connection's current interaction ID for you; pass +--interaction-id to pin the submission to a specific interaction instead. + Examples: - # Submit field values + # Submit canonical field values from the connection's fields list + kernel auth connections submit --field-value field_email=me@example.com --field-value field_password=secret + + # Answer a specific interaction (rejected if the flow has moved on) + kernel auth connections submit --choice-id mfa_sms --interaction-id mai_abc123xyz + + # Submit legacy field values kernel auth connections submit --field username=myuser --field password=mypass # Select an MFA option @@ -1291,6 +1338,7 @@ func init() { addJSONOutputFlag(authConnectionsSubmitCmd) authConnectionsSubmitCmd.Flags().StringArray("field-value", []string{}, "Canonical field-id=value pair from the connection's `fields` list (repeatable)") authConnectionsSubmitCmd.Flags().String("choice-id", "", "Canonical choice ID from the connection's `choices` list") + authConnectionsSubmitCmd.Flags().String("interaction-id", "", "Canonical interaction ID the submitted values belong to; defaults to the connection's current interaction. Only valid with --field-value or --choice-id") authConnectionsSubmitCmd.Flags().StringArray("field", []string{}, "Legacy field name=value pair (repeatable); prefer --field-value") authConnectionsSubmitCmd.Flags().String("mfa-option-id", "", "MFA option ID if user selected an MFA method") authConnectionsSubmitCmd.Flags().String("sign-in-option-id", "", "Sign-in option ID if the flow returned non-MFA choices") @@ -1516,6 +1564,7 @@ func runAuthConnectionsSubmit(cmd *cobra.Command, args []string) error { fieldPairs, _ := cmd.Flags().GetStringArray("field") canonicalFieldPairs, _ := cmd.Flags().GetStringArray("field-value") choiceID, _ := cmd.Flags().GetString("choice-id") + interactionID, _ := cmd.Flags().GetString("interaction-id") mfaOptionID, _ := cmd.Flags().GetString("mfa-option-id") signInOptionID, _ := cmd.Flags().GetString("sign-in-option-id") ssoButtonSelector, _ := cmd.Flags().GetString("sso-button-selector") @@ -1543,6 +1592,7 @@ func runAuthConnectionsSubmit(cmd *cobra.Command, args []string) error { FieldValues: fieldValues, CanonicalFieldValues: canonicalFieldValues, SelectedChoiceID: choiceID, + InteractionID: interactionID, MfaOptionID: mfaOptionID, SignInOptionID: signInOptionID, SSOButtonSelector: ssoButtonSelector, diff --git a/cmd/auth_connections_test.go b/cmd/auth_connections_test.go index a403464e..4466e483 100644 --- a/cmd/auth_connections_test.go +++ b/cmd/auth_connections_test.go @@ -147,6 +147,9 @@ func TestAuthConnectionsGet_PrintsCanonicalInputMetadata(t *testing.T) { Status: kernel.ManagedAuthStatusNeedsAuth, FlowStatus: kernel.ManagedAuthFlowStatusInProgress, FlowStep: kernel.ManagedAuthFlowStepAwaitingInput, + // Canonical fields and choices always arrive with the interaction + // they belong to, which `submit` needs. + InteractionID: "mai_abc123xyz", Fields: []kernel.ManagedAuthField{ { ID: "otp", @@ -182,6 +185,7 @@ func TestAuthConnectionsGet_PrintsCanonicalInputMetadata(t *testing.T) { require.NoError(t, c.Get(context.Background(), AuthConnectionGetInput{ID: "e0x3vbw4z66kpwny3k5k46tj"})) out := outBuf.String() + assert.Contains(t, out, `mai_abc123xyz`) assert.Contains(t, out, `otp (One-time code)`) // The reason tells the user why the field is being asked for: "rejected" // means a stored credential was refused, so a new value has to replace it. @@ -823,16 +827,24 @@ func TestLogin_TelemetryOverride(t *testing.T) { assert.True(t, captured.Browser.Telemetry.Browser.Screenshot.Enabled.Value) } -func TestSubmit_CanonicalChoiceID(t *testing.T) { - capturePtermOutput(t) - var captured kernel.AuthConnectionSubmitParams - fake := &FakeAuthConnectionService{ +// canonicalSubmitFake serves the current interaction ID from `get` and captures +// what `submit` sends, which is what every canonical submission needs. +func canonicalSubmitFake(interactionID string, captured *kernel.AuthConnectionSubmitParams) *FakeAuthConnectionService { + return &FakeAuthConnectionService{ + GetFunc: func(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.ManagedAuth, error) { + return &kernel.ManagedAuth{ID: id, InteractionID: interactionID}, nil + }, SubmitFunc: func(ctx context.Context, id string, body kernel.AuthConnectionSubmitParams, opts ...option.RequestOption) (*kernel.SubmitFieldsResponse, error) { - captured = body + *captured = body return &kernel.SubmitFieldsResponse{Accepted: true}, nil }, } - c := AuthConnectionCmd{svc: fake} +} + +func TestSubmit_CanonicalChoiceID(t *testing.T) { + capturePtermOutput(t) + var captured kernel.AuthConnectionSubmitParams + c := AuthConnectionCmd{svc: canonicalSubmitFake("mai_current", &captured)} require.NoError(t, c.Submit(context.Background(), AuthConnectionSubmitInput{ ID: "auth_1", SelectedChoiceID: "choice_sms", @@ -844,6 +856,53 @@ func TestSubmit_CanonicalChoiceID(t *testing.T) { } func TestSubmit_CanonicalFieldValues(t *testing.T) { + capturePtermOutput(t) + var captured kernel.AuthConnectionSubmitParams + c := AuthConnectionCmd{svc: canonicalSubmitFake("mai_current", &captured)} + require.NoError(t, c.Submit(context.Background(), AuthConnectionSubmitInput{ + ID: "auth_1", + CanonicalFieldValues: map[string]string{"field_email": "me@example.com"}, + })) + assert.Equal(t, map[string]string{"field_email": "me@example.com"}, captured.SubmitFieldsRequest.FieldValues) + assert.Nil(t, captured.SubmitFieldsRequest.Fields) +} + +func TestSubmit_CanonicalResolvesCurrentInteractionID(t *testing.T) { + capturePtermOutput(t) + var captured kernel.AuthConnectionSubmitParams + c := AuthConnectionCmd{svc: canonicalSubmitFake("mai_current", &captured)} + require.NoError(t, c.Submit(context.Background(), AuthConnectionSubmitInput{ + ID: "auth_1", + CanonicalFieldValues: map[string]string{"field_email": "me@example.com"}, + })) + require.True(t, captured.SubmitFieldsRequest.InteractionID.Valid()) + assert.Equal(t, "mai_current", captured.SubmitFieldsRequest.InteractionID.Value) +} + +func TestSubmit_ExplicitInteractionIDIsNotOverwritten(t *testing.T) { + capturePtermOutput(t) + var captured kernel.AuthConnectionSubmitParams + fake := canonicalSubmitFake("mai_current", &captured) + getCalls := 0 + inner := fake.GetFunc + fake.GetFunc = func(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.ManagedAuth, error) { + getCalls++ + return inner(ctx, id, opts...) + } + c := AuthConnectionCmd{svc: fake} + require.NoError(t, c.Submit(context.Background(), AuthConnectionSubmitInput{ + ID: "auth_1", + SelectedChoiceID: "choice_sms", + // Pinning an older interaction is how a caller detects that the flow moved + // on, so the CLI must forward it untouched. + InteractionID: "mai_pinned", + })) + assert.Equal(t, 0, getCalls) + require.True(t, captured.SubmitFieldsRequest.InteractionID.Valid()) + assert.Equal(t, "mai_pinned", captured.SubmitFieldsRequest.InteractionID.Value) +} + +func TestSubmit_LegacyModeOmitsInteractionID(t *testing.T) { capturePtermOutput(t) var captured kernel.AuthConnectionSubmitParams fake := &FakeAuthConnectionService{ @@ -854,11 +913,61 @@ func TestSubmit_CanonicalFieldValues(t *testing.T) { } c := AuthConnectionCmd{svc: fake} require.NoError(t, c.Submit(context.Background(), AuthConnectionSubmitInput{ + ID: "auth_1", + FieldValues: map[string]string{"username": "me"}, + })) + // The API rejects an interaction ID paired with a legacy submit mode. + assert.False(t, captured.SubmitFieldsRequest.InteractionID.Valid()) +} + +func TestSubmit_InteractionIDRequiresCanonicalMode(t *testing.T) { + capturePtermOutput(t) + c := AuthConnectionCmd{svc: &FakeAuthConnectionService{}} + err := c.Submit(context.Background(), AuthConnectionSubmitInput{ + ID: "auth_1", + FieldValues: map[string]string{"username": "me"}, + InteractionID: "mai_current", + }) + require.Error(t, err) + assert.Contains(t, err.Error(), "the --interaction-id flag is only valid with --field-value or --choice-id") +} + +func TestSubmit_CanonicalWithoutPendingInteractionErrors(t *testing.T) { + capturePtermOutput(t) + submitted := false + fake := &FakeAuthConnectionService{ + GetFunc: func(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.ManagedAuth, error) { + return &kernel.ManagedAuth{ID: id}, nil + }, + SubmitFunc: func(ctx context.Context, id string, body kernel.AuthConnectionSubmitParams, opts ...option.RequestOption) (*kernel.SubmitFieldsResponse, error) { + submitted = true + return &kernel.SubmitFieldsResponse{Accepted: true}, nil + }, + } + c := AuthConnectionCmd{svc: fake} + err := c.Submit(context.Background(), AuthConnectionSubmitInput{ + ID: "auth_1", + SelectedChoiceID: "choice_sms", + }) + require.Error(t, err) + assert.Contains(t, err.Error(), "no canonical interaction awaiting input") + assert.False(t, submitted) +} + +func TestSubmit_CanonicalGetErrorSurfaced(t *testing.T) { + capturePtermOutput(t) + fake := &FakeAuthConnectionService{ + GetFunc: func(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.ManagedAuth, error) { + return nil, errors.New("boom") + }, + } + c := AuthConnectionCmd{svc: fake} + err := c.Submit(context.Background(), AuthConnectionSubmitInput{ ID: "auth_1", CanonicalFieldValues: map[string]string{"field_email": "me@example.com"}, - })) - assert.Equal(t, map[string]string{"field_email": "me@example.com"}, captured.SubmitFieldsRequest.FieldValues) - assert.Nil(t, captured.SubmitFieldsRequest.Fields) + }) + require.Error(t, err) + assert.Contains(t, err.Error(), "interaction ID resolution") } func TestSubmit_CanonicalAndLegacyAreMutuallyExclusive(t *testing.T) { diff --git a/cmd/org.go b/cmd/org.go index f6b58675..e8ba0d52 100644 --- a/cmd/org.go +++ b/cmd/org.go @@ -4,7 +4,6 @@ import ( "context" "encoding/json" "fmt" - "time" "github.com/kernel/cli/pkg/util" "github.com/kernel/kernel-go-sdk" @@ -12,7 +11,6 @@ import ( "github.com/kernel/kernel-go-sdk/packages/param" "github.com/kernel/kernel-go-sdk/packages/respjson" "github.com/pterm/pterm" - "github.com/samber/lo" "github.com/spf13/cobra" ) @@ -36,10 +34,6 @@ type OrgLimitsGetInput struct { Output string } -type OrgEntitlementsGetInput struct { - Output string -} - type OrgLimitsSetInput struct { DefaultProjectMaxConcurrentSessions Int64Flag Output string @@ -127,118 +121,6 @@ func (c OrgCmd) LimitsSet(ctx context.Context, in OrgLimitsSetInput) error { return nil } -func (c OrgCmd) EntitlementsGet(ctx context.Context, in OrgEntitlementsGetInput) error { - if err := validateJSONOutput(in.Output); err != nil { - return err - } - - entitlements, err := c.entitlements.Get(ctx) - if err != nil { - return util.CleanedUpSdkError{Err: err} - } - - if in.Output == "json" { - if entitlements == nil { - fmt.Println("null") - return nil - } - return util.PrintPrettyJSON(entitlements) - } - - renderOrgEntitlements(entitlements) - return nil -} - -func renderOrgEntitlements(ent *kernel.OrgEntitlements) { - if ent == nil { - pterm.Info.Println("No organization entitlements found") - return - } - - plan := ent.Plan - planRows := pterm.TableData{ - {"Field", "Value"}, - {"Plan", plan.ID}, - // Active trials resolve to a different effective plan than the - // contractual one, so show both. - {"Effective Plan", plan.EffectiveID}, - {"Trialing", lo.Ternary(plan.IsTrialing, "yes", "no")}, - // Billing status and trial end are both nullable. - {"Billing Status", formatOrgEntitlementString(plan.Status, plan.JSON.Status)}, - {"Trial Ends At", formatOrgEntitlementTime(plan.TrialEndsAt, plan.JSON.TrialEndsAt)}, - } - pterm.DefaultSection.Println("Plan") - PrintTableNoPad(planRows, true) - - f := ent.Features - featureRows := pterm.TableData{ - {"Feature", "Enabled", "Constraints"}, - {"Browser Extensions", formatOrgEntitlementEnabled(f.BrowserExtensions.Enabled), fmt.Sprintf("max stored per org: %s", formatProjectLimitValue(f.BrowserExtensions.MaxStoredPerOrg, f.BrowserExtensions.JSON.MaxStoredPerOrg))}, - {"Browser Pools", formatOrgEntitlementEnabled(f.BrowserPools.Enabled), ""}, - {"Browser Replays", formatOrgEntitlementEnabled(f.BrowserReplays.Enabled), fmt.Sprintf("retention: %s", formatOrgEntitlementDays(f.BrowserReplays.RetentionDays, f.BrowserReplays.JSON.RetentionDays))}, - {"Credential Providers", formatOrgEntitlementEnabled(f.CredentialProviders.Enabled), ""}, - {"Credentials", formatOrgEntitlementEnabled(f.Credentials.Enabled), ""}, - {"Custom Proxies", formatOrgEntitlementEnabled(f.CustomProxies.Enabled), ""}, - {"File I/O", formatOrgEntitlementEnabled(f.FileIo.Enabled), ""}, - {"GPU", formatOrgEntitlementEnabled(f.GPU.Enabled), ""}, - {"Managed Auth", formatOrgEntitlementEnabled(f.ManagedAuth.Enabled), formatManagedAuthConstraints(f.ManagedAuth)}, - {"Managed Proxies", formatOrgEntitlementEnabled(f.ManagedProxies.Enabled), ""}, - {"Profiles", formatOrgEntitlementEnabled(f.Profiles.Enabled), ""}, - {"Proxy Bypass Hosts", formatOrgEntitlementEnabled(f.ProxyBypassHosts.Enabled), ""}, - } - pterm.DefaultSection.Println("Features") - PrintTableNoPad(featureRows, true) - - l := ent.Limits - limitRows := pterm.TableData{ - {"Limit", "Value"}, - {"Max Concurrent Browsers", formatProjectLimitValue(l.MaxConcurrentBrowsers, l.JSON.MaxConcurrentBrowsers)}, - {"Max Concurrent Invocations", formatProjectLimitValue(l.MaxConcurrentInvocations, l.JSON.MaxConcurrentInvocations)}, - {"Default Max Concurrent Invocations Per App", formatProjectLimitValue(l.DefaultMaxConcurrentInvocationsPerApp, l.JSON.DefaultMaxConcurrentInvocationsPerApp)}, - } - pterm.DefaultSection.Println("Limits") - PrintTableNoPad(limitRows, true) -} - -func formatOrgEntitlementEnabled(enabled bool) string { - return lo.Ternary(enabled, "yes", "no") -} - -// formatManagedAuthConstraints summarizes the managed auth connection cap and the -// accepted health-check interval window in a single cell. -func formatManagedAuthConstraints(ma kernel.OrgEntitlementsFeaturesManagedAuth) string { - return fmt.Sprintf( - "max connections: %s, health check interval: %ds default (%ds-%ds)", - formatProjectLimitValue(ma.MaxConnections, ma.JSON.MaxConnections), - ma.HealthCheckIntervalDefaultSeconds, - ma.HealthCheckIntervalMinSeconds, - ma.HealthCheckIntervalMaxSeconds, - ) -} - -// formatOrgEntitlementDays renders a retention window, treating a null value as -// unlimited retention rather than zero days. -func formatOrgEntitlementDays(value int64, field respjson.Field) string { - if !field.Valid() { - return "unlimited" - } - return fmt.Sprintf("%d days", value) -} - -func formatOrgEntitlementString(value string, field respjson.Field) string { - if !field.Valid() || value == "" { - return "-" - } - return value -} - -func formatOrgEntitlementTime(value time.Time, field respjson.Field) string { - if !field.Valid() || value.IsZero() { - return "-" - } - return util.FormatLocal(value) -} - func renderOrgLimits(limits *kernel.OrgLimits) { if limits == nil { pterm.Info.Println("No organization limits found") @@ -370,22 +252,6 @@ var orgLimitsCmd = &cobra.Command{ }, } -var orgEntitlementsCmd = &cobra.Command{ - Use: "entitlements", - Short: "Read organization entitlements", - Run: func(cmd *cobra.Command, args []string) { - _ = cmd.Help() - }, -} - -var orgEntitlementsGetCmd = &cobra.Command{ - Use: "get", - Short: "Get organization entitlements", - Long: "Show the organization's effective feature access and constraints after applying its plan, active trial treatment, plan status, and organization-specific overrides. Unlimited constraints are shown as \"unlimited\".", - Args: cobra.NoArgs, - RunE: runOrgEntitlementsGet, -} - var orgLimitsGetCmd = &cobra.Command{ Use: "get", Short: "Get organization limits", @@ -449,11 +315,8 @@ func init() { addJSONOutputFlag(orgLimitsSetCmd) addJSONOutputFlag(orgEntitlementsCmd) - addJSONOutputFlag(orgEntitlementsGetCmd) - orgLimitsCmd.AddCommand(orgLimitsGetCmd) orgLimitsCmd.AddCommand(orgLimitsSetCmd) - orgEntitlementsCmd.AddCommand(orgEntitlementsGetCmd) orgCmd.AddCommand(orgLimitsCmd) orgCmd.AddCommand(orgEntitlementsCmd) } diff --git a/cmd/org_test.go b/cmd/org_test.go index 464d2a9a..e55713fd 100644 --- a/cmd/org_test.go +++ b/cmd/org_test.go @@ -413,150 +413,3 @@ func TestOrgLimitsSet_RejectsNegative(t *testing.T) { assert.Error(t, err) assert.Contains(t, err.Error(), "must be non-negative") } - -type FakeOrgEntitlementsService struct { - GetFunc func(ctx context.Context, opts ...option.RequestOption) (*kernel.OrgEntitlements, error) -} - -func (f *FakeOrgEntitlementsService) Get(ctx context.Context, opts ...option.RequestOption) (*kernel.OrgEntitlements, error) { - if f.GetFunc != nil { - return f.GetFunc(ctx, opts...) - } - return &kernel.OrgEntitlements{}, nil -} - -// populatedEntitlements builds an entitlements payload with every nullable field -// present, so renders exercise the non-"unlimited" branches. -func populatedEntitlements() *kernel.OrgEntitlements { - ent := &kernel.OrgEntitlements{} - - ent.Plan.ID = "START_UP" - ent.Plan.EffectiveID = "START_UP" - ent.Plan.IsTrialing = true - ent.Plan.Status = "ACTIVE" - ent.Plan.TrialEndsAt = time.Date(2030, 1, 2, 3, 4, 5, 0, time.UTC) - ent.Plan.JSON.Status = respjson.NewField(`"ACTIVE"`) - ent.Plan.JSON.TrialEndsAt = respjson.NewField(`"2030-01-02T03:04:05Z"`) - - ent.Features.BrowserExtensions.Enabled = true - ent.Features.BrowserExtensions.MaxStoredPerOrg = 25 - ent.Features.BrowserExtensions.JSON.MaxStoredPerOrg = respjson.NewField("25") - ent.Features.BrowserPools.Enabled = true - ent.Features.BrowserReplays.Enabled = true - ent.Features.BrowserReplays.RetentionDays = 7 - ent.Features.BrowserReplays.JSON.RetentionDays = respjson.NewField("7") - ent.Features.CredentialProviders.Enabled = true - ent.Features.Credentials.Enabled = true - ent.Features.CustomProxies.Enabled = false - ent.Features.FileIo.Enabled = true - ent.Features.GPU.Enabled = false - ent.Features.ManagedAuth.Enabled = true - ent.Features.ManagedAuth.MaxConnections = 10 - ent.Features.ManagedAuth.HealthCheckIntervalDefaultSeconds = 600 - ent.Features.ManagedAuth.HealthCheckIntervalMinSeconds = 300 - ent.Features.ManagedAuth.HealthCheckIntervalMaxSeconds = 86400 - ent.Features.ManagedAuth.JSON.MaxConnections = respjson.NewField("10") - ent.Features.ManagedProxies.Enabled = true - ent.Features.Profiles.Enabled = true - ent.Features.ProxyBypassHosts.Enabled = true - - ent.Limits.MaxConcurrentBrowsers = 50 - ent.Limits.MaxConcurrentInvocations = 20 - ent.Limits.DefaultMaxConcurrentInvocationsPerApp = 5 - ent.Limits.JSON.MaxConcurrentBrowsers = respjson.NewField("50") - ent.Limits.JSON.MaxConcurrentInvocations = respjson.NewField("20") - ent.Limits.JSON.DefaultMaxConcurrentInvocationsPerApp = respjson.NewField("5") - - return ent -} - -func TestOrgEntitlementsGet_RendersPlanFeaturesAndLimits(t *testing.T) { - buf := capturePtermOutput(t) - fake := &FakeOrgEntitlementsService{ - GetFunc: func(ctx context.Context, opts ...option.RequestOption) (*kernel.OrgEntitlements, error) { - return populatedEntitlements(), nil - }, - } - c := OrgCmd{entitlements: fake} - assert.NoError(t, c.EntitlementsGet(context.Background(), OrgEntitlementsGetInput{})) - - out := buf.String() - // Plan section - assert.Contains(t, out, "START_UP") - assert.Contains(t, out, "Effective Plan") - assert.Contains(t, out, "Trialing") - assert.Contains(t, out, "ACTIVE") - // Features section — every feature should get a row. - for _, feature := range []string{ - "Browser Extensions", "Browser Pools", "Browser Replays", "Credential Providers", - "Credentials", "Custom Proxies", "File I/O", "GPU", "Managed Auth", - "Managed Proxies", "Profiles", "Proxy Bypass Hosts", - } { - assert.Contains(t, out, feature) - } - assert.Contains(t, out, "max stored per org: 25") - assert.Contains(t, out, "retention: 7 days") - assert.Contains(t, out, "max connections: 10") - assert.Contains(t, out, "600s default (300s-86400s)") - // Limits section - assert.Contains(t, out, "Max Concurrent Browsers") - assert.Contains(t, out, "Max Concurrent Invocations") - assert.Contains(t, out, "Default Max Concurrent Invocations Per App") -} - -func TestOrgEntitlementsGet_NullConstraintsShownAsUnlimited(t *testing.T) { - buf := capturePtermOutput(t) - fake := &FakeOrgEntitlementsService{ - GetFunc: func(ctx context.Context, opts ...option.RequestOption) (*kernel.OrgEntitlements, error) { - ent := populatedEntitlements() - // Null (not omitted) constraints mean unlimited. - ent.Features.BrowserExtensions.JSON.MaxStoredPerOrg = respjson.NewField(respjson.Null) - ent.Features.ManagedAuth.JSON.MaxConnections = respjson.NewField(respjson.Null) - ent.Limits.JSON.MaxConcurrentBrowsers = respjson.NewField(respjson.Null) - return ent, nil - }, - } - c := OrgCmd{entitlements: fake} - assert.NoError(t, c.EntitlementsGet(context.Background(), OrgEntitlementsGetInput{})) - - out := buf.String() - assert.Contains(t, out, "max stored per org: unlimited") - assert.Contains(t, out, "max connections: unlimited") - assert.Contains(t, out, "unlimited") -} - -func TestOrgEntitlementsGet_NullPlanFieldsShownAsDash(t *testing.T) { - buf := capturePtermOutput(t) - fake := &FakeOrgEntitlementsService{ - GetFunc: func(ctx context.Context, opts ...option.RequestOption) (*kernel.OrgEntitlements, error) { - ent := populatedEntitlements() - ent.Plan.IsTrialing = false - ent.Plan.JSON.Status = respjson.NewField(respjson.Null) - ent.Plan.JSON.TrialEndsAt = respjson.NewField(respjson.Null) - return ent, nil - }, - } - c := OrgCmd{entitlements: fake} - assert.NoError(t, c.EntitlementsGet(context.Background(), OrgEntitlementsGetInput{})) - - out := buf.String() - assert.Contains(t, out, "Billing Status") - assert.Contains(t, out, "Trial Ends At") - assert.NotContains(t, out, "ACTIVE") -} - -func TestOrgEntitlementsGet_RejectsUnknownOutput(t *testing.T) { - c := OrgCmd{entitlements: &FakeOrgEntitlementsService{}} - assert.Error(t, c.EntitlementsGet(context.Background(), OrgEntitlementsGetInput{Output: "yaml"})) -} - -func TestOrgEntitlementsGet_SurfacesAPIError(t *testing.T) { - capturePtermOutput(t) - fake := &FakeOrgEntitlementsService{ - GetFunc: func(ctx context.Context, opts ...option.RequestOption) (*kernel.OrgEntitlements, error) { - return nil, errors.New("boom") - }, - } - c := OrgCmd{entitlements: fake} - assert.Error(t, c.EntitlementsGet(context.Background(), OrgEntitlementsGetInput{})) -} diff --git a/go.mod b/go.mod index 502421b9..900e422a 100644 --- a/go.mod +++ b/go.mod @@ -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.92.0 + github.com/kernel/kernel-go-sdk v0.92.1-0.20260819184853-796d4245c87a github.com/klauspost/compress v1.18.5 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/pterm/pterm v0.12.80 diff --git a/go.sum b/go.sum index 04679b80..4ec0e491 100644 --- a/go.sum +++ b/go.sum @@ -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.92.0 h1:3EeoPahTcGEo97BCbwT50gu8QJnawfL166z12hc8Ucg= -github.com/kernel/kernel-go-sdk v0.92.0/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= +github.com/kernel/kernel-go-sdk v0.92.1-0.20260819184853-796d4245c87a h1:VJcz+I1d/VTEHkKM4O7+Wf+ejSbXQtytUtDXnZ0+b+4= +github.com/kernel/kernel-go-sdk v0.92.1-0.20260819184853-796d4245c87a/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= From 31d2462fcbd0d9f53d417493761424714b81da23 Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Wed, 19 Aug 2026 20:39:11 +0000 Subject: [PATCH 5/8] CLI: Update Go SDK to 467fea7 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps kernel-go-sdk to v0.92.1-0.20260819203102-467fea72ee93, which adds the proxy_error browser telemetry event (BrowserProxyErrorEvent) to the telemetry event union. No CLI coverage gaps: a full enumeration of all 137 SDK methods in api.md found a corresponding CLI command for each, and the new event type needs no code change because the telemetry commands render category/type generically and accept --types values without a fixed allowlist. Tested: go build ./..., go vet ./..., go test ./... (all pass); browsers create --telemetry all, browsers curl, browsers telemetry events (table, --output json, --categories network --all, --types proxy_error), browsers telemetry stream --categories network --types proxy_error, browsers delete — all against the live API. Co-Authored-By: Claude Opus 5 --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 900e422a..59cb47a0 100644 --- a/go.mod +++ b/go.mod @@ -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.92.1-0.20260819184853-796d4245c87a + github.com/kernel/kernel-go-sdk v0.92.1-0.20260819203102-467fea72ee93 github.com/klauspost/compress v1.18.5 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/pterm/pterm v0.12.80 diff --git a/go.sum b/go.sum index 4ec0e491..662bb5e0 100644 --- a/go.sum +++ b/go.sum @@ -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.92.1-0.20260819184853-796d4245c87a h1:VJcz+I1d/VTEHkKM4O7+Wf+ejSbXQtytUtDXnZ0+b+4= -github.com/kernel/kernel-go-sdk v0.92.1-0.20260819184853-796d4245c87a/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= +github.com/kernel/kernel-go-sdk v0.92.1-0.20260819203102-467fea72ee93 h1:p+OWj+8b1iK+Bx/5gSSTP9itGLbN5w2hY/CVT0eBdRM= +github.com/kernel/kernel-go-sdk v0.92.1-0.20260819203102-467fea72ee93/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= From 16880f448f52ba874488156b50db24745f4685b3 Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Thu, 20 Aug 2026 00:06:00 +0000 Subject: [PATCH 6/8] CLI: Update Go SDK to v0.93.0 (0802326) Bumps github.com/kernel/kernel-go-sdk to 08023260493e4584c4d87638849ab4491b34ec49 (v0.93.0). The 0.93.0 release only changed version/changelog metadata relative to the SDK revision the CLI was already pinned to (467fea7); api.md and all generated Go sources are byte-identical, so there are no new methods, params, or fields to expose. Coverage analysis: full enumeration of all 140 SDK methods in api.md against the CLI command tree found no gaps. Every method has a command, and every param struct field is reachable via a flag, a positional arg, or a derived value. Tested: go build ./..., go vet ./..., go test ./... (all pass), plus live API smoke tests for browsers list/create/get/delete, browsers telemetry events, auth connections list, profiles list, telemetry destinations list. Co-Authored-By: Claude Opus 5 --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 59cb47a0..0e346401 100644 --- a/go.mod +++ b/go.mod @@ -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.92.1-0.20260819203102-467fea72ee93 + github.com/kernel/kernel-go-sdk v0.93.0 github.com/klauspost/compress v1.18.5 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/pterm/pterm v0.12.80 diff --git a/go.sum b/go.sum index 662bb5e0..78374443 100644 --- a/go.sum +++ b/go.sum @@ -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.92.1-0.20260819203102-467fea72ee93 h1:p+OWj+8b1iK+Bx/5gSSTP9itGLbN5w2hY/CVT0eBdRM= -github.com/kernel/kernel-go-sdk v0.92.1-0.20260819203102-467fea72ee93/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= +github.com/kernel/kernel-go-sdk v0.93.0 h1:mPsZKoQlLsgsC0TehWJ/Q5XqWwKu33bKfnuqnfNHtjs= +github.com/kernel/kernel-go-sdk v0.93.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= From f9b126f68e11d78fd112775a398e6ac03cce2c35 Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Fri, 21 Aug 2026 15:30:19 +0000 Subject: [PATCH 7/8] CLI: Update Go SDK to 9a36566 and cover the telemetry control/platform split Updates kernel-go-sdk to 9a36566d8999ca346a9eeccede0cbf88d651b93f, which mirrors the control/platform telemetry split into the public API. BrowserTelemetryCategories gains a `platform` category and `control` becomes its own config type carrying `cdp.excluded_methods`, so the previous `p.Control = on()` no longer compiled. New coverage: - `--telemetry=platform` is now a settable category on browsers create/update, browser-pools create/update/acquire, and auth connections create/update/login, and is reported by the telemetry summaries and details tables. - `--telemetry-cdp-exclude` (new flag, same eight commands) sets BrowserTelemetryCdpControlConfigParam.ExcludedMethods. Values are the 38 CDP methods the proxy reports, matched case-insensitively and canonicalized; `--telemetry-cdp-exclude=none` sends an empty list to report every method again. Combining it with `--telemetry=off` is rejected, and on auth connection update/login it requires `--telemetry` in the same command, since a connection stores its browser config as sent and exclusions alone would drop its category selection. - Excluded methods are surfaced in the create/update telemetry summary, the browser-pool details table, and the auth connection details table. A full enumeration of the 140 methods in api.md against the CLI command tree found no missing commands. The x-cli-skip endpoints (site-configs, auth connection exchange) remain excluded. Tested against the live API: browsers create/update/delete with --telemetry=control,platform --telemetry-cdp-exclude (set, replace, and =none clear); browser-pools create/get/update/acquire/delete; auth connections create/get/update/delete; browsers telemetry events --categories platform; and the unknown-method, --telemetry=off, and missing---telemetry error paths. Co-Authored-By: Claude Opus 5 --- cmd/auth_connections.go | 60 +++++++----- cmd/browser_pools.go | 87 ++++++++++------- cmd/browser_pools_test.go | 6 +- cmd/browsers.go | 126 +++++++++++++----------- cmd/browsers_telemetry.go | 173 ++++++++++++++++++++++++++++----- cmd/browsers_telemetry_test.go | 130 ++++++++++++++++++++----- go.mod | 2 +- go.sum | 4 +- 8 files changed, 417 insertions(+), 171 deletions(-) diff --git a/cmd/auth_connections.go b/cmd/auth_connections.go index c6aedcfa..d7fc1f9a 100644 --- a/cmd/auth_connections.go +++ b/cmd/auth_connections.go @@ -57,6 +57,7 @@ type AuthConnectionCreateInput struct { NoAutoReauth bool RecordSession BoolFlag Telemetry string + TelemetryCdpExclude string TelemetryExport string Output string } @@ -92,6 +93,7 @@ type AuthConnectionUpdateInput struct { AutoReauth BoolFlag RecordSession BoolFlag Telemetry string + TelemetryCdpExclude string TelemetryExport string Output string } @@ -111,15 +113,16 @@ type AuthConnectionDeleteInput struct { } type AuthConnectionLoginInput struct { - ID string - ProxyID string - ProxyName string - ProxyMode string - Stealth BoolFlag - RecordSession BoolFlag - Telemetry string - TelemetryExport string - Output string + ID string + ProxyID string + ProxyName string + ProxyMode string + Stealth BoolFlag + RecordSession BoolFlag + Telemetry string + TelemetryCdpExclude string + TelemetryExport string + Output string } type AuthConnectionSubmitInput struct { @@ -237,8 +240,8 @@ func (c AuthConnectionCmd) Create(ctx context.Context, in AuthConnectionCreateIn params.ManagedAuthCreateRequest.RecordSession = kernel.Opt(in.RecordSession.Value) } - if in.Telemetry != "" || in.TelemetryExport != "" { - t, err := buildManagedAuthTelemetryParam(in.Telemetry, in.TelemetryExport, true) + if in.Telemetry != "" || in.TelemetryCdpExclude != "" || in.TelemetryExport != "" { + t, err := buildManagedAuthTelemetryParam(in.Telemetry, in.TelemetryCdpExclude, in.TelemetryExport, true) if err != nil { return err } @@ -383,8 +386,8 @@ func (c AuthConnectionCmd) Update(ctx context.Context, in AuthConnectionUpdateIn hasChanges = true } - if in.Telemetry != "" || in.TelemetryExport != "" { - t, err := buildManagedAuthTelemetryParam(in.Telemetry, in.TelemetryExport, false) + if in.Telemetry != "" || in.TelemetryCdpExclude != "" || in.TelemetryExport != "" { + t, err := buildManagedAuthTelemetryParam(in.Telemetry, in.TelemetryCdpExclude, in.TelemetryExport, false) if err != nil { return err } @@ -781,8 +784,8 @@ func (c AuthConnectionCmd) Login(ctx context.Context, in AuthConnectionLoginInpu params.RecordSession = kernel.Opt(in.RecordSession.Value) } - if in.Telemetry != "" || in.TelemetryExport != "" { - t, err := buildManagedAuthTelemetryParam(in.Telemetry, in.TelemetryExport, false) + if in.Telemetry != "" || in.TelemetryCdpExclude != "" || in.TelemetryExport != "" { + t, err := buildManagedAuthTelemetryParam(in.Telemetry, in.TelemetryCdpExclude, in.TelemetryExport, false) if err != nil { return err } @@ -1279,6 +1282,7 @@ func init() { authConnectionsCreateCmd.Flags().Bool("record-session", false, "Record browser sessions for this connection by default (useful for debugging)") authConnectionsCreateCmd.Flags().String("telemetry", "", "Configure telemetry for this connection's browser sessions (opt-in): --telemetry=all (default set), --telemetry=off (disable), or --telemetry=console,network (capture exactly those categories)") authConnectionsCreateCmd.Flags().String("telemetry-export-otlp", "", "Export this connection's captured telemetry over OTLP to one of the org's configured destinations, by ID or name; --telemetry-export-otlp=off disables export. Implies --telemetry=all when --telemetry is not set, since export requires capture") + authConnectionsCreateCmd.Flags().String("telemetry-cdp-exclude", "", "Leave the named CDP methods out of control telemetry's cdp_command events, comma-separated (e.g. Input.dispatchMouseEvent,Page.captureScreenshot); --telemetry-cdp-exclude=none clears the list. Excluded commands are still relayed to the browser, they just produce no event") _ = authConnectionsCreateCmd.MarkFlagRequired("domain") _ = authConnectionsCreateCmd.MarkFlagRequired("profile-name") authConnectionsCreateCmd.MarkFlagsMutuallyExclusive("credential-name", "credential-provider") @@ -1308,6 +1312,7 @@ func init() { authConnectionsUpdateCmd.Flags().Bool("record-session", false, "Set whether browser sessions are recorded by default; use --record-session=false to disable") authConnectionsUpdateCmd.Flags().String("telemetry", "", "Update telemetry for future browser sessions: --telemetry=all (reset to default set), --telemetry=off (disable), or --telemetry=console,network (merge those categories into the current selection)") authConnectionsUpdateCmd.Flags().String("telemetry-export-otlp", "", "Update where future sessions export captured telemetry over OTLP, by destination ID or name; --telemetry-export-otlp=off disables export. Naming a destination requires passing --telemetry in the same command, since export and capture are validated together") + authConnectionsUpdateCmd.Flags().String("telemetry-cdp-exclude", "", "Leave the named CDP methods out of control telemetry's cdp_command events, comma-separated (e.g. Input.dispatchMouseEvent,Page.captureScreenshot); --telemetry-cdp-exclude=none clears the list. Excluded commands are still relayed to the browser, they just produce no event") authConnectionsUpdateCmd.MarkFlagsMutuallyExclusive("credential-name", "credential-provider") authConnectionsUpdateCmd.MarkFlagsMutuallyExclusive("save-credentials", "no-save-credentials") authConnectionsUpdateCmd.MarkFlagsMutuallyExclusive("health-checks", "no-health-checks") @@ -1333,6 +1338,7 @@ func init() { authConnectionsLoginCmd.Flags().Bool("record-session", false, "Override whether this login's browser session is recorded; use --record-session=false to disable") authConnectionsLoginCmd.Flags().String("telemetry", "", "Telemetry override for this login only, merged onto the connection's config: --telemetry=all, --telemetry=off, or --telemetry=console,network") authConnectionsLoginCmd.Flags().String("telemetry-export-otlp", "", "Export override for this login only: an OTLP destination ID or name; --telemetry-export-otlp=off disables export for this login. Naming a destination requires passing --telemetry in the same command, since export and capture are validated together") + authConnectionsLoginCmd.Flags().String("telemetry-cdp-exclude", "", "Leave the named CDP methods out of control telemetry's cdp_command events, comma-separated (e.g. Input.dispatchMouseEvent,Page.captureScreenshot); --telemetry-cdp-exclude=none clears the list. Excluded commands are still relayed to the browser, they just produce no event") // Submit flags addJSONOutputFlag(authConnectionsSubmitCmd) @@ -1387,6 +1393,7 @@ func runAuthConnectionsCreate(cmd *cobra.Command, args []string) error { noHealthChecks, _ := cmd.Flags().GetBool("no-health-checks") noAutoReauth, _ := cmd.Flags().GetBool("no-auto-reauth") telemetry, _ := cmd.Flags().GetString("telemetry") + telemetryCdpExclude, _ := cmd.Flags().GetString("telemetry-cdp-exclude") telemetryExport, _ := cmd.Flags().GetString("telemetry-export-otlp") svc := client.Auth.Connections @@ -1410,6 +1417,7 @@ func runAuthConnectionsCreate(cmd *cobra.Command, args []string) error { NoAutoReauth: noAutoReauth, RecordSession: readBoolFlag(cmd.Flags(), "record-session"), Telemetry: telemetry, + TelemetryCdpExclude: telemetryCdpExclude, TelemetryExport: telemetryExport, Output: output, }) @@ -1443,6 +1451,7 @@ func runAuthConnectionsUpdate(cmd *cobra.Command, args []string) error { noSaveCredentials, _ := cmd.Flags().GetBool("no-save-credentials") healthCheckInterval, _ := cmd.Flags().GetInt("health-check-interval") telemetry, _ := cmd.Flags().GetString("telemetry") + telemetryCdpExclude, _ := cmd.Flags().GetString("telemetry-cdp-exclude") telemetryExport, _ := cmd.Flags().GetString("telemetry-export-otlp") saveCredentialsFlag := BoolFlag{} @@ -1496,6 +1505,7 @@ func runAuthConnectionsUpdate(cmd *cobra.Command, args []string) error { AutoReauth: togglePair("auto-reauth", "no-auto-reauth"), RecordSession: readBoolFlag(cmd.Flags(), "record-session"), Telemetry: telemetry, + TelemetryCdpExclude: telemetryCdpExclude, TelemetryExport: telemetryExport, Output: output, }) @@ -1541,20 +1551,22 @@ func runAuthConnectionsLogin(cmd *cobra.Command, args []string) error { proxyName, _ := cmd.Flags().GetString("proxy-name") proxyMode, _ := cmd.Flags().GetString("proxy-mode") telemetry, _ := cmd.Flags().GetString("telemetry") + telemetryCdpExclude, _ := cmd.Flags().GetString("telemetry-cdp-exclude") telemetryExport, _ := cmd.Flags().GetString("telemetry-export-otlp") svc := client.Auth.Connections c := AuthConnectionCmd{svc: &svc} return c.Login(cmd.Context(), AuthConnectionLoginInput{ - ID: args[0], - ProxyID: proxyID, - ProxyName: proxyName, - ProxyMode: proxyMode, - Stealth: readBoolFlag(cmd.Flags(), "stealth"), - RecordSession: readBoolFlag(cmd.Flags(), "record-session"), - Telemetry: telemetry, - TelemetryExport: telemetryExport, - Output: output, + ID: args[0], + ProxyID: proxyID, + ProxyName: proxyName, + ProxyMode: proxyMode, + Stealth: readBoolFlag(cmd.Flags(), "stealth"), + RecordSession: readBoolFlag(cmd.Flags(), "record-session"), + Telemetry: telemetry, + TelemetryCdpExclude: telemetryCdpExclude, + TelemetryExport: telemetryExport, + Output: output, }) } diff --git a/cmd/browser_pools.go b/cmd/browser_pools.go index c6f7051b..276018d7 100644 --- a/cmd/browser_pools.go +++ b/cmd/browser_pools.go @@ -108,21 +108,24 @@ 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) +// buildPoolNewTelemetryParam converts --telemetry and --telemetry-cdp-exclude flag +// values to the pool create param. +func buildPoolNewTelemetryParam(s, cdpExclude string) (kernel.BrowserPoolNewParamsTelemetry, error) { + enabled, browser, err := resolveTelemetryFlag(s, cdpExclude) 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) +// buildPoolUpdateTelemetryParam converts --telemetry and --telemetry-cdp-exclude flag +// values to the pool update param. +func buildPoolUpdateTelemetryParam(s, cdpExclude string) (kernel.BrowserPoolUpdateParamsTelemetry, error) { + enabled, browser, err := resolveTelemetryFlag(s, cdpExclude) 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) +// buildPoolAcquireTelemetryParam converts --telemetry and --telemetry-cdp-exclude flag +// values to the acquire override param. +func buildPoolAcquireTelemetryParam(s, cdpExclude string) (kernel.BrowserPoolAcquireParamsTelemetry, error) { + enabled, browser, err := resolveTelemetryFlag(s, cdpExclude) return kernel.BrowserPoolAcquireParamsTelemetry{Enabled: enabled, Browser: browser}, err } @@ -132,7 +135,11 @@ func formatPoolTelemetry(cfg kernel.BrowserTelemetryConfig) string { if len(on) == 0 { return "disabled" } - return strings.Join(on, ", ") + base := strings.Join(on, ", ") + if ex := formatCdpExcludedMethods(cfg.Browser.Control.Cdp.ExcludedMethods); ex != "" { + return base + " (excluding CDP methods: " + ex + ")" + } + return base } type BrowserPoolsCreateInput struct { @@ -155,6 +162,7 @@ type BrowserPoolsCreateInput struct { ChromePolicy string ChromePolicyFile string Telemetry string + TelemetryCdpExclude string Output string } @@ -247,8 +255,8 @@ func (c BrowserPoolsCmd) Create(ctx context.Context, in BrowserPoolsCreateInput) params.ChromePolicy = chromePolicy } - if in.Telemetry != "" { - t, err := buildPoolNewTelemetryParam(in.Telemetry) + if in.Telemetry != "" || in.TelemetryCdpExclude != "" { + t, err := buildPoolNewTelemetryParam(in.Telemetry, in.TelemetryCdpExclude) if err != nil { return err } @@ -269,7 +277,7 @@ func (c BrowserPoolsCmd) Create(ctx context.Context, in BrowserPoolsCreateInput) } else { pterm.Success.Printf("Created browser pool %s\n", pool.ID) } - if in.Telemetry != "" { + if in.Telemetry != "" || in.TelemetryCdpExclude != "" { printTelemetrySummary(pool.BrowserPoolConfig.Telemetry) } return nil @@ -350,6 +358,7 @@ type BrowserPoolsUpdateInput struct { ChromePolicyFile string ClearChromePolicy bool Telemetry string + TelemetryCdpExclude string DiscardAllIdle BoolFlag Output string } @@ -488,8 +497,8 @@ func (c BrowserPoolsCmd) Update(ctx context.Context, in BrowserPoolsUpdateInput) params.SetExtraFields(extraFields) } - if in.Telemetry != "" { - t, err := buildPoolUpdateTelemetryParam(in.Telemetry) + if in.Telemetry != "" || in.TelemetryCdpExclude != "" { + t, err := buildPoolUpdateTelemetryParam(in.Telemetry, in.TelemetryCdpExclude) if err != nil { return err } @@ -510,7 +519,7 @@ func (c BrowserPoolsCmd) Update(ctx context.Context, in BrowserPoolsUpdateInput) } else { pterm.Success.Printf("Updated browser pool %s\n", pool.ID) } - if in.Telemetry != "" { + if in.Telemetry != "" || in.TelemetryCdpExclude != "" { printTelemetrySummary(pool.BrowserPoolConfig.Telemetry) } return nil @@ -535,13 +544,14 @@ func (c BrowserPoolsCmd) Delete(ctx context.Context, in BrowserPoolsDeleteInput) } type BrowserPoolsAcquireInput struct { - IDOrName string - TimeoutSeconds int64 - Name string - StartURL string - Tags map[string]string - Telemetry string - Output string + IDOrName string + TimeoutSeconds int64 + Name string + StartURL string + Tags map[string]string + Telemetry string + TelemetryCdpExclude string + Output string } // buildAcquireParams builds the SDK params for acquiring a browser from a pool. @@ -549,7 +559,7 @@ type BrowserPoolsAcquireInput struct { // path so the per-lease name/tags/start-url/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, startURL string) (kernel.BrowserPoolAcquireParams, error) { +func buildAcquireParams(name string, tags map[string]string, timeoutSeconds int64, telemetry, telemetryCdpExclude, startURL string) (kernel.BrowserPoolAcquireParams, error) { params := kernel.BrowserPoolAcquireParams{} if timeoutSeconds > 0 { params.AcquireTimeoutSeconds = kernel.Int(timeoutSeconds) @@ -563,8 +573,8 @@ func buildAcquireParams(name string, tags map[string]string, timeoutSeconds int6 if len(tags) > 0 { params.Tags = kernel.Tags(tags) } - if telemetry != "" { - t, err := buildPoolAcquireTelemetryParam(telemetry) + if telemetry != "" || telemetryCdpExclude != "" { + t, err := buildPoolAcquireTelemetryParam(telemetry, telemetryCdpExclude) if err != nil { return kernel.BrowserPoolAcquireParams{}, err } @@ -578,7 +588,7 @@ func (c BrowserPoolsCmd) Acquire(ctx context.Context, in BrowserPoolsAcquireInpu return err } - params, err := buildAcquireParams(in.Name, in.Tags, in.TimeoutSeconds, in.Telemetry, in.StartURL) + params, err := buildAcquireParams(in.Name, in.Tags, in.TimeoutSeconds, in.Telemetry, in.TelemetryCdpExclude, in.StartURL) if err != nil { return err } @@ -749,6 +759,7 @@ func init() { 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.Flags().String("telemetry-cdp-exclude", "", "Leave the named CDP methods out of control telemetry's cdp_command events, comma-separated (e.g. Input.dispatchMouseEvent,Page.captureScreenshot); --telemetry-cdp-exclude=none clears the list. Excluded commands are still relayed to the browser, they just produce no event") browserPoolsCreateCmd.MarkFlagsMutuallyExclusive("chrome-policy", "chrome-policy-file") addJSONOutputFlag(browserPoolsGetCmd) @@ -779,6 +790,7 @@ func init() { browserPoolsUpdateCmd.MarkFlagsMutuallyExclusive("chrome-policy", "chrome-policy-file") browserPoolsUpdateCmd.MarkFlagsMutuallyExclusive("private-host", "clear-private-hosts") 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().String("telemetry-cdp-exclude", "", "Leave the named CDP methods out of control telemetry's cdp_command events, comma-separated (e.g. Input.dispatchMouseEvent,Page.captureScreenshot); --telemetry-cdp-exclude=none clears the list. Excluded commands are still relayed to the browser, they just produce no event") browserPoolsUpdateCmd.Flags().Bool("discard-all-idle", false, "Discard all idle browsers") addJSONOutputFlag(browserPoolsUpdateCmd) @@ -789,6 +801,7 @@ func init() { browserPoolsAcquireCmd.Flags().String("start-url", "", "URL to navigate the acquired browser to, overriding the pool's start URL for this acquire only (best-effort)") 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") + browserPoolsAcquireCmd.Flags().String("telemetry-cdp-exclude", "", "Leave the named CDP methods out of control telemetry's cdp_command events, comma-separated (e.g. Input.dispatchMouseEvent,Page.captureScreenshot); --telemetry-cdp-exclude=none clears the list. Excluded commands are still relayed to the browser, they just produce no event") addJSONOutputFlag(browserPoolsAcquireCmd) browserPoolsReleaseCmd.Flags().String("session-id", "", "Browser session ID to release") @@ -845,6 +858,7 @@ func runBrowserPoolsCreate(cmd *cobra.Command, args []string) error { chromePolicy, _ := cmd.Flags().GetString("chrome-policy") chromePolicyFile, _ := cmd.Flags().GetString("chrome-policy-file") telemetry, _ := cmd.Flags().GetString("telemetry") + telemetryCdpExclude, _ := cmd.Flags().GetString("telemetry-cdp-exclude") output, _ := cmd.Flags().GetString("output") in := BrowserPoolsCreateInput{ @@ -867,6 +881,7 @@ func runBrowserPoolsCreate(cmd *cobra.Command, args []string) error { ChromePolicy: chromePolicy, ChromePolicyFile: chromePolicyFile, Telemetry: telemetry, + TelemetryCdpExclude: telemetryCdpExclude, Output: output, } @@ -908,6 +923,7 @@ func runBrowserPoolsUpdate(cmd *cobra.Command, args []string) error { chromePolicyFile, _ := cmd.Flags().GetString("chrome-policy-file") clearChromePolicy, _ := cmd.Flags().GetBool("clear-chrome-policy") telemetry, _ := cmd.Flags().GetString("telemetry") + telemetryCdpExclude, _ := cmd.Flags().GetString("telemetry-cdp-exclude") discardIdle, _ := cmd.Flags().GetBool("discard-all-idle") output, _ := cmd.Flags().GetString("output") @@ -937,6 +953,7 @@ func runBrowserPoolsUpdate(cmd *cobra.Command, args []string) error { ChromePolicyFile: chromePolicyFile, ClearChromePolicy: clearChromePolicy, Telemetry: telemetry, + TelemetryCdpExclude: telemetryCdpExclude, DiscardAllIdle: BoolFlag{Set: cmd.Flags().Changed("discard-all-idle"), Value: discardIdle}, Output: output, } @@ -959,16 +976,18 @@ func runBrowserPoolsAcquire(cmd *cobra.Command, args []string) error { startURL, _ := cmd.Flags().GetString("start-url") tags, _ := tagsFromFlag(cmd, "tag") telemetry, _ := cmd.Flags().GetString("telemetry") + telemetryCdpExclude, _ := cmd.Flags().GetString("telemetry-cdp-exclude") output, _ := cmd.Flags().GetString("output") c := BrowserPoolsCmd{client: &client.BrowserPools} return c.Acquire(cmd.Context(), BrowserPoolsAcquireInput{ - IDOrName: args[0], - TimeoutSeconds: timeout, - Name: name, - StartURL: startURL, - Tags: tags, - Telemetry: telemetry, - Output: output, + IDOrName: args[0], + TimeoutSeconds: timeout, + Name: name, + StartURL: startURL, + Tags: tags, + Telemetry: telemetry, + TelemetryCdpExclude: telemetryCdpExclude, + Output: output, }) } diff --git a/cmd/browser_pools_test.go b/cmd/browser_pools_test.go index e0a143ff..387f3f18 100644 --- a/cmd/browser_pools_test.go +++ b/cmd/browser_pools_test.go @@ -239,7 +239,7 @@ func TestBrowserPoolsCreate_PrivateHostNormalization(t *testing.T) { // forwarding used by both `browser-pools acquire` and the `browsers create // --pool-id` lease path. func TestBuildAcquireParams(t *testing.T) { - p, err := buildAcquireParams("lease", map[string]string{"env": "prod"}, 30, "console,network", "https://example.com") + p, err := buildAcquireParams("lease", map[string]string{"env": "prod"}, 30, "console,network", "", "https://example.com") assert.NoError(t, err) assert.True(t, p.Name.Valid()) assert.Equal(t, "lease", p.Name.Value) @@ -252,7 +252,7 @@ func TestBuildAcquireParams(t *testing.T) { assert.True(t, p.Telemetry.Browser.Network.Enabled.Value) // Unset inputs produce an empty params struct (nothing forwarded). - empty, err := buildAcquireParams("", nil, 0, "", "") + empty, err := buildAcquireParams("", nil, 0, "", "", "") assert.NoError(t, err) assert.False(t, empty.Name.Valid()) assert.Len(t, empty.Tags, 0) @@ -260,7 +260,7 @@ func TestBuildAcquireParams(t *testing.T) { assert.False(t, empty.StartURL.Valid()) // An invalid category surfaces an error rather than a partial param. - _, err = buildAcquireParams("", nil, 0, "bogus", "") + _, err = buildAcquireParams("", nil, 0, "bogus", "", "") assert.Error(t, err) } diff --git a/cmd/browsers.go b/cmd/browsers.go index 326f88d1..5e4c82b7 100644 --- a/cmd/browsers.go +++ b/cmd/browsers.go @@ -360,31 +360,32 @@ func formatTags(tags kernel.Tags) string { // Inputs for each command type BrowsersCreateInput struct { - TimeoutSeconds int - Stealth BoolFlag - Headless BoolFlag - GPU BoolFlag - Memory string - InvocationID string - Kiosk BoolFlag - ProfileID string - ProfileName string - ProfileSaveChanges BoolFlag - ProxyID string - ProxyName string - ProxyMode string - Region string - PrivateHosts []string - StartURL string - Extensions []string - Viewport string - Telemetry string - TelemetryExport string - ChromePolicy string - ChromePolicyFile string - Name string - Tags map[string]string - Output string + TimeoutSeconds int + Stealth BoolFlag + Headless BoolFlag + GPU BoolFlag + Memory string + InvocationID string + Kiosk BoolFlag + ProfileID string + ProfileName string + ProfileSaveChanges BoolFlag + ProxyID string + ProxyName string + ProxyMode string + Region string + PrivateHosts []string + StartURL string + Extensions []string + Viewport string + Telemetry string + TelemetryCdpExclude string + TelemetryExport string + ChromePolicy string + ChromePolicyFile string + Name string + Tags map[string]string + Output string } type BrowsersDeleteInput struct { @@ -415,6 +416,7 @@ type BrowsersUpdateInput struct { Viewport string Force bool Telemetry string + TelemetryCdpExclude string Name string SetName bool ClearName bool @@ -669,8 +671,8 @@ func (b BrowsersCmd) Create(ctx context.Context, in BrowsersCreateInput) error { } } - if in.Telemetry != "" || in.TelemetryExport != "" { - t, err := buildNewTelemetryParam(in.Telemetry, in.TelemetryExport) + if in.Telemetry != "" || in.TelemetryCdpExclude != "" || in.TelemetryExport != "" { + t, err := buildNewTelemetryParam(in.Telemetry, in.TelemetryCdpExclude, in.TelemetryExport) if err != nil { return err } @@ -705,7 +707,7 @@ func (b BrowsersCmd) Create(ctx context.Context, in BrowsersCreateInput) error { } printBrowserSessionResult(browser.SessionID, browser.CdpWsURL, browser.BrowserLiveViewURL, browser.Profile, browser.ProfileSaveChanges, browser.StartURL, browser.Name, browser.Tags) - if in.Telemetry != "" || in.TelemetryExport != "" { + if in.Telemetry != "" || in.TelemetryCdpExclude != "" || in.TelemetryExport != "" { printTelemetrySummary(browser.Telemetry) } return nil @@ -941,8 +943,8 @@ func (b BrowsersCmd) Update(ctx context.Context, in BrowsersUpdateInput) error { } // Validate that at least one update option is provided - if !hasProxyChange && !hasProfileChange && !hasViewportChange && in.Telemetry == "" && !hasNameChange && !hasTagsChange { - return fmt.Errorf("must specify at least one of: --proxy-id, --proxy-name, --proxy-mode, --clear-proxy, --disable-default-proxy, --profile-id, --profile-name, --viewport, --telemetry, --name, --clear-name, --tag, or --clear-tags") + if !hasProxyChange && !hasProfileChange && !hasViewportChange && in.Telemetry == "" && in.TelemetryCdpExclude == "" && !hasNameChange && !hasTagsChange { + return fmt.Errorf("must specify at least one of: --proxy-id, --proxy-name, --proxy-mode, --clear-proxy, --disable-default-proxy, --profile-id, --profile-name, --viewport, --telemetry, --telemetry-cdp-exclude, --name, --clear-name, --tag, or --clear-tags") } params := kernel.BrowserUpdateParams{} @@ -985,8 +987,8 @@ func (b BrowsersCmd) Update(ctx context.Context, in BrowsersUpdateInput) error { } // Handle telemetry changes - if in.Telemetry != "" { - t, err := buildUpdateTelemetryParam(in.Telemetry) + if in.Telemetry != "" || in.TelemetryCdpExclude != "" { + t, err := buildUpdateTelemetryParam(in.Telemetry, in.TelemetryCdpExclude) if err != nil { return err } @@ -1036,7 +1038,7 @@ func (b BrowsersCmd) Update(ctx context.Context, in BrowsersUpdateInput) error { if hasProfileChange { pterm.Info.Printf("Profile save changes: %t\n", browser.ProfileSaveChanges) } - if in.Telemetry != "" { + if in.Telemetry != "" || in.TelemetryCdpExclude != "" { printTelemetrySummary(browser.Telemetry) } return nil @@ -2682,6 +2684,7 @@ func init() { 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 (reset to default set), --telemetry=off (disable), or --telemetry=console,network (merge those categories into the current selection)") + browsersUpdateCmd.Flags().String("telemetry-cdp-exclude", "", "Leave the named CDP methods out of control telemetry's cdp_command events, comma-separated (e.g. Input.dispatchMouseEvent,Page.captureScreenshot); --telemetry-cdp-exclude=none clears the list. Excluded commands are still relayed to the browser, they just produce no event") browsersUpdateCmd.Flags().String("name", "", "Set a new unique name for the browser session (mutually exclusive with --clear-name)") browsersUpdateCmd.Flags().Bool("clear-name", false, "Clear the browser session name") browsersUpdateCmd.Flags().StringArray("tag", nil, "Set a tag KEY=VALUE (repeatable; up to 50 pairs). Replaces the entire tag set; mutually exclusive with --clear-tags") @@ -2963,6 +2966,7 @@ func init() { 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 (opt-in): --telemetry=all (default set), --telemetry=off (disable), or --telemetry=console,network (capture exactly those categories)") + browsersCreateCmd.Flags().String("telemetry-cdp-exclude", "", "Leave the named CDP methods out of control telemetry's cdp_command events, comma-separated (e.g. Input.dispatchMouseEvent,Page.captureScreenshot); --telemetry-cdp-exclude=none clears the list. Excluded commands are still relayed to the browser, they just produce no event") browsersCreateCmd.Flags().String("telemetry-export-otlp", "", "Export captured telemetry over OTLP to one of the org's configured destinations, by ID or name; --telemetry-export-otlp=off disables export. Implies --telemetry=all when --telemetry is not set, since export requires capture") browsersCreateCmd.Flags().String("name", "", "Optional unique name for the browser session (used to find it later; can be changed with 'browsers update --name')") browsersCreateCmd.Flags().StringArray("tag", nil, "Set a tag KEY=VALUE on the session (repeatable; up to 50 pairs)") @@ -3094,6 +3098,7 @@ func runBrowsersCreate(cmd *cobra.Command, args []string) error { poolID, _ := cmd.Flags().GetString("pool-id") poolName, _ := cmd.Flags().GetString("pool-name") telemetry, _ := cmd.Flags().GetString("telemetry") + telemetryCdpExclude, _ := cmd.Flags().GetString("telemetry-cdp-exclude") telemetryExport, _ := cmd.Flags().GetString("telemetry-export-otlp") name, _ := cmd.Flags().GetString("name") tags, _ := tagsFromFlag(cmd, "tag") @@ -3160,7 +3165,7 @@ func runBrowsersCreate(cmd *cobra.Command, args []string) error { if cmd.Flags().Changed("timeout") && timeout > 0 { acquireTimeout = int64(timeout) } - acquireParams, err := buildAcquireParams(name, tags, acquireTimeout, telemetry, startURL) + acquireParams, err := buildAcquireParams(name, tags, acquireTimeout, telemetry, telemetryCdpExclude, startURL) if err != nil { return err } @@ -3202,31 +3207,32 @@ func runBrowsersCreate(cmd *cobra.Command, args []string) error { } in := BrowsersCreateInput{ - TimeoutSeconds: timeout, - Stealth: BoolFlag{Set: cmd.Flags().Changed("stealth"), Value: stealthVal}, - Headless: BoolFlag{Set: cmd.Flags().Changed("headless"), Value: headlessVal}, - GPU: BoolFlag{Set: cmd.Flags().Changed("gpu"), Value: gpuVal}, - Memory: memory, - InvocationID: invocationID, - Kiosk: BoolFlag{Set: cmd.Flags().Changed("kiosk"), Value: kioskVal}, - ProfileID: profileID, - ProfileName: profileName, - ProfileSaveChanges: BoolFlag{Set: cmd.Flags().Changed("save-changes"), Value: saveChanges}, - ProxyID: proxyID, - ProxyName: proxyName, - ProxyMode: proxyMode, - Region: region, - PrivateHosts: privateHosts, - StartURL: startURL, - Extensions: extensions, - Viewport: viewport, - Telemetry: telemetry, - TelemetryExport: telemetryExport, - ChromePolicy: chromePolicy, - ChromePolicyFile: chromePolicyFile, - Name: name, - Tags: tags, - Output: output, + TimeoutSeconds: timeout, + Stealth: BoolFlag{Set: cmd.Flags().Changed("stealth"), Value: stealthVal}, + Headless: BoolFlag{Set: cmd.Flags().Changed("headless"), Value: headlessVal}, + GPU: BoolFlag{Set: cmd.Flags().Changed("gpu"), Value: gpuVal}, + Memory: memory, + InvocationID: invocationID, + Kiosk: BoolFlag{Set: cmd.Flags().Changed("kiosk"), Value: kioskVal}, + ProfileID: profileID, + ProfileName: profileName, + ProfileSaveChanges: BoolFlag{Set: cmd.Flags().Changed("save-changes"), Value: saveChanges}, + ProxyID: proxyID, + ProxyName: proxyName, + ProxyMode: proxyMode, + Region: region, + PrivateHosts: privateHosts, + StartURL: startURL, + Extensions: extensions, + Viewport: viewport, + Telemetry: telemetry, + TelemetryCdpExclude: telemetryCdpExclude, + TelemetryExport: telemetryExport, + ChromePolicy: chromePolicy, + ChromePolicyFile: chromePolicyFile, + Name: name, + Tags: tags, + Output: output, } svc := client.Browsers @@ -3288,6 +3294,7 @@ func runBrowsersUpdate(cmd *cobra.Command, args []string) error { viewport, _ := cmd.Flags().GetString("viewport") force, _ := cmd.Flags().GetBool("force") telemetry, _ := cmd.Flags().GetString("telemetry") + telemetryCdpExclude, _ := cmd.Flags().GetString("telemetry-cdp-exclude") name, _ := cmd.Flags().GetString("name") clearName, _ := cmd.Flags().GetBool("clear-name") tags, tagsProvided := tagsFromFlag(cmd, "tag") @@ -3308,6 +3315,7 @@ func runBrowsersUpdate(cmd *cobra.Command, args []string) error { Viewport: viewport, Force: force, Telemetry: telemetry, + TelemetryCdpExclude: telemetryCdpExclude, Name: name, SetName: cmd.Flags().Changed("name"), ClearName: clearName, diff --git a/cmd/browsers_telemetry.go b/cmd/browsers_telemetry.go index 9a37ef36..03699772 100644 --- a/cmd/browsers_telemetry.go +++ b/cmd/browsers_telemetry.go @@ -76,7 +76,9 @@ func parseTelemetryCategories(s string) (kernel.BrowserTelemetryCategoriesConfig case "interaction": p.Interaction = on() case "control": - p.Control = on() + p.Control = kernel.BrowserTelemetryControlConfigParam{Enabled: kernel.Opt(true)} + case "platform": + p.Platform = on() case "connection": p.Connection = on() case "system": @@ -92,20 +94,112 @@ func parseTelemetryCategories(s string) (kernel.BrowserTelemetryCategoriesConfig return p, nil } -// resolveTelemetryFlag interprets a --telemetry flag value shared by every browser -// and browser-pool command: "all" enables the default set, "off" disables capture, -// and a comma-separated list opts into exactly those categories. It returns the -// resolved (enabled, browser) pair so each endpoint can assemble its own param type. -func resolveTelemetryFlag(s string) (param.Opt[bool], kernel.BrowserTelemetryCategoriesConfigParam, error) { +// cdpCommandMethods are the browser-control commands the CDP proxy reports as +// cdp_command events, and so the values --telemetry-cdp-exclude accepts. +var cdpCommandMethods = []string{ + "Input.dispatchMouseEvent", + "Input.dispatchKeyEvent", + "Input.insertText", + "Input.imeSetComposition", + "Input.dispatchTouchEvent", + "Input.dispatchDragEvent", + "Input.cancelDragging", + "Input.emulateTouchFromMouseEvent", + "Input.synthesizePinchGesture", + "Input.synthesizeScrollGesture", + "Input.synthesizeTapGesture", + "DOM.setFileInputFiles", + "DOM.focus", + "DOM.scrollIntoViewIfNeeded", + "Page.bringToFront", + "Page.captureScreenshot", + "Page.captureSnapshot", + "Page.handleJavaScriptDialog", + "Page.navigate", + "Page.navigateToHistoryEntry", + "Page.reload", + "Page.printToPDF", + "Page.startScreencast", + "Page.stopScreencast", + "Page.stopLoading", + "Page.close", + "Page.setWebLifecycleState", + "Target.activateTarget", + "Target.closeTarget", + "Target.createTarget", + "Target.createBrowserContext", + "Target.disposeBrowserContext", + "Target.openDevTools", + "Browser.cancelDownload", + "Browser.close", + "Browser.setWindowBounds", + "Browser.setContentsSize", + "Autofill.trigger", +} + +// telemetryCdpExcludeNone is the --telemetry-cdp-exclude value that clears the +// exclusion list rather than naming methods to drop. +const telemetryCdpExcludeNone = "none" + +// parseTelemetryCdpExcludedMethods parses a --telemetry-cdp-exclude value into the +// exclusion list carried by the control category. "none" resolves to an empty list, +// which tells the API to report every supported method again. Method names are +// matched case-insensitively and returned in their canonical CDP spelling. +func parseTelemetryCdpExcludedMethods(s string) ([]kernel.BrowserCdpCommandMethod, error) { + methods := []kernel.BrowserCdpCommandMethod{} + if strings.TrimSpace(s) == telemetryCdpExcludeNone { + return methods, nil + } + for _, part := range strings.Split(s, ",") { + name := strings.TrimSpace(part) + if name == "" { + continue + } + i := slices.IndexFunc(cdpCommandMethods, func(m string) bool { return strings.EqualFold(m, name) }) + if i < 0 { + return nil, fmt.Errorf("unknown CDP method %q: must be one of %s, or %q to clear the exclusion list", name, strings.Join(cdpCommandMethods, ", "), telemetryCdpExcludeNone) + } + methods = append(methods, kernel.BrowserCdpCommandMethod(cdpCommandMethods[i])) + } + return methods, nil +} + +// resolveTelemetryFlag interprets the --telemetry and --telemetry-cdp-exclude flag +// values shared by every browser and browser-pool command: "all" enables the default +// set, "off" disables capture, and a comma-separated list opts into exactly those +// categories. Excluded CDP methods are merged into the control category independently +// of the selection, so they survive a later update that only names categories. It +// returns the resolved (enabled, browser) pair so each endpoint can assemble its own +// param type. +func resolveTelemetryFlag(s, cdpExclude string) (param.Opt[bool], kernel.BrowserTelemetryCategoriesConfigParam, error) { + var enabled param.Opt[bool] + var p kernel.BrowserTelemetryCategoriesConfigParam switch s { case "all": - return kernel.Opt(true), kernel.BrowserTelemetryCategoriesConfigParam{}, nil + enabled = kernel.Opt(true) case "off": - return kernel.Opt(false), kernel.BrowserTelemetryCategoriesConfigParam{}, nil + enabled = kernel.Opt(false) default: - p, err := parseTelemetryCategories(s) - return param.Opt[bool]{}, p, err + var err error + if p, err = parseTelemetryCategories(s); err != nil { + return enabled, p, err + } + } + if cdpExclude == "" { + return enabled, p, nil } + // Exclusion is a control-telemetry setting, so it has no meaning in a request + // that turns capture off. Error messages never lead with a flag token — the + // error style title-cases the first word. + if s == "off" { + return enabled, p, fmt.Errorf("cannot combine --telemetry=off with --telemetry-cdp-exclude: excluding CDP methods only applies while control telemetry is captured") + } + methods, err := parseTelemetryCdpExcludedMethods(cdpExclude) + if err != nil { + return enabled, p, err + } + p.Control.Cdp.ExcludedMethods = methods + return enabled, p, nil } // telemetryExportOff is the --telemetry-export-otlp value that turns export off @@ -167,10 +261,10 @@ func validateTelemetryExportCombo(telemetry, id, name string, canImply bool) err return nil } -// buildNewTelemetryParam converts --telemetry and --telemetry-export-otlp flag -// values to the create API param. -func buildNewTelemetryParam(s, export string) (kernel.BrowserNewParamsTelemetry, error) { - enabled, browser, err := resolveTelemetryFlag(s) +// buildNewTelemetryParam converts --telemetry, --telemetry-cdp-exclude and +// --telemetry-export-otlp flag values to the create API param. +func buildNewTelemetryParam(s, cdpExclude, export string) (kernel.BrowserNewParamsTelemetry, error) { + enabled, browser, err := resolveTelemetryFlag(s, cdpExclude) p := kernel.BrowserNewParamsTelemetry{Enabled: enabled, Browser: browser} if err != nil || export == "" { return p, err @@ -207,26 +301,37 @@ func optIfSet(s string) param.Opt[string] { return kernel.Opt(s) } -// buildUpdateTelemetryParam converts a --telemetry flag value to the update API param. -func buildUpdateTelemetryParam(s string) (kernel.BrowserUpdateParamsTelemetry, error) { - enabled, browser, err := resolveTelemetryFlag(s) +// buildUpdateTelemetryParam converts --telemetry and --telemetry-cdp-exclude flag +// values to the update API param. +func buildUpdateTelemetryParam(s, cdpExclude string) (kernel.BrowserUpdateParamsTelemetry, error) { + enabled, browser, err := resolveTelemetryFlag(s, cdpExclude) return kernel.BrowserUpdateParamsTelemetry{Enabled: enabled, Browser: browser}, err } -// buildManagedAuthTelemetryParam converts --telemetry and --telemetry-export-otlp -// flag values to the browser telemetry config carried by an auth connection's -// browser settings, shared by create, update, and login. +// buildManagedAuthTelemetryParam converts --telemetry, --telemetry-cdp-exclude and +// --telemetry-export-otlp flag values to the browser telemetry config carried by an +// auth connection's browser settings, shared by create, update, and login. // // canImply is true only on create, where there is no stored selection to clobber // and capture can safely be turned on for the user so a destination works on its // own. On update and login it is false: enabling capture there would replace the // connection's current category selection rather than merge onto it. -func buildManagedAuthTelemetryParam(s, export string, canImply bool) (kernel.ManagedAuthBrowserConfigTelemetryParam, error) { - enabled, browser, err := resolveTelemetryFlag(s) +func buildManagedAuthTelemetryParam(s, cdpExclude, export string, canImply bool) (kernel.ManagedAuthBrowserConfigTelemetryParam, error) { + enabled, browser, err := resolveTelemetryFlag(s, cdpExclude) p := kernel.ManagedAuthBrowserConfigTelemetryParam{Enabled: enabled, Browser: browser} - if err != nil || export == "" { + if err != nil { return p, err } + // A connection stores the browser config as sent rather than resolving it, so a + // request carrying only CDP exclusions would drop the connection's category + // selection. On update and login the user has to restate what to capture; on + // create there is nothing to lose. + if cdpExclude != "" && s == "" && !canImply { + return p, fmt.Errorf("setting --telemetry-cdp-exclude also requires --telemetry in the same command: the connection stores its browser config as sent, so exclusions on their own would drop its category selection") + } + if export == "" { + return p, nil + } exEnabled, id, name, err := resolveTelemetryExportFlag(export) if err != nil { return p, err @@ -264,6 +369,9 @@ func formatManagedAuthTelemetry(cfg kernel.ManagedAuthBrowserConfigTelemetry) st } return "disabled" }() + if ex := formatCdpExcludedMethods(cfg.Browser.Control.Cdp.ExcludedMethods); ex != "" { + base += " (excluding CDP methods: " + ex + ")" + } if dest := managedAuthExportDestination(cfg.Export); dest != "" { return base + " (exporting to " + dest + ")" } @@ -287,7 +395,7 @@ func managedAuthExportDestination(ex kernel.ManagedAuthBrowserConfigTelemetryExp // flows automatically whenever a CDP category is captured. var settableCategories = []string{ "console", "network", "page", "interaction", - "control", "connection", "system", "screenshot", "captcha", + "control", "connection", "system", "screenshot", "platform", "captcha", } // streamFilterCategories are the categories accepted by `telemetry stream --categories`. @@ -310,6 +418,7 @@ func telemetryEnabledCategories(cfg kernel.BrowserTelemetryConfig) []string { {"connection", b.Connection.Enabled}, {"system", b.System.Enabled}, {"screenshot", b.Screenshot.Enabled}, + {"platform", b.Platform.Enabled}, {"captcha", b.Captcha.Enabled}, } on := make([]string, 0, len(ordered)) @@ -330,6 +439,9 @@ func printTelemetrySummary(cfg kernel.BrowserTelemetryConfig) { return } pterm.Info.Printf("Telemetry capturing: %s\n", strings.Join(on, ", ")) + if ex := formatCdpExcludedMethods(cfg.Browser.Control.Cdp.ExcludedMethods); ex != "" { + pterm.Info.Printf("Telemetry excluding CDP methods: %s\n", ex) + } if cfg.Export.Otlp.Enabled { // The response reports the resolved destination by ID even when the request // selected it by name. @@ -341,6 +453,19 @@ func printTelemetrySummary(cfg kernel.BrowserTelemetryConfig) { } } +// formatCdpExcludedMethods renders the CDP methods left out of control +// telemetry's cdp_command stream, or "" when every supported method is reported. +func formatCdpExcludedMethods(methods []kernel.BrowserCdpCommandMethod) string { + if len(methods) == 0 { + return "" + } + names := make([]string, 0, len(methods)) + for _, m := range methods { + names = append(names, string(m)) + } + return strings.Join(names, ", ") +} + // shouldEmit applies client-side category/type filters to a telemetry event. func shouldEmit(category, eventType string, categories, types []string) bool { if len(categories) > 0 && !slices.Contains(categories, category) { diff --git a/cmd/browsers_telemetry_test.go b/cmd/browsers_telemetry_test.go index fe3b88e1..d9458bff 100644 --- a/cmd/browsers_telemetry_test.go +++ b/cmd/browsers_telemetry_test.go @@ -299,14 +299,17 @@ func TestShouldEmit(t *testing.T) { } func TestParseTelemetryCategories_OptInList(t *testing.T) { - p, err := parseTelemetryCategories("network,control,captcha") + p, err := parseTelemetryCategories("network,control,captcha,platform") assert.NoError(t, err) // Listed categories are enabled. - for _, c := range []kernel.BrowserTelemetryCategoryConfigParam{p.Network, p.Control, p.Captcha} { + for _, c := range []kernel.BrowserTelemetryCategoryConfigParam{p.Network, p.Captcha, p.Platform} { assert.True(t, c.Enabled.Valid()) assert.True(t, c.Enabled.Value) } + // Control carries its own config type, so it is checked separately. + assert.True(t, p.Control.Enabled.Valid()) + assert.True(t, p.Control.Enabled.Value) // Unlisted categories are omitted (opt-in: the instance treats them as off). assert.False(t, p.Console.Enabled.Valid()) assert.False(t, p.Page.Enabled.Valid()) @@ -336,21 +339,21 @@ func TestParseTelemetryCategories_WhitespaceTolerance(t *testing.T) { // listed categories enabled (Enabled unset). func TestBuildTelemetryParam_WireEncoding(t *testing.T) { t.Run("all", func(t *testing.T) { - p, err := buildNewTelemetryParam("all", "") + p, err := buildNewTelemetryParam("all", "", "") assert.NoError(t, err) assert.True(t, p.Enabled.Valid()) assert.True(t, p.Enabled.Value) assert.False(t, p.Browser.Network.Enabled.Valid()) }) t.Run("off", func(t *testing.T) { - p, err := buildNewTelemetryParam("off", "") + p, err := buildNewTelemetryParam("off", "", "") assert.NoError(t, err) assert.True(t, p.Enabled.Valid()) assert.False(t, p.Enabled.Value) assert.False(t, p.Browser.Network.Enabled.Valid()) }) t.Run("opt-in list sets only Browser", func(t *testing.T) { - p, err := buildNewTelemetryParam("network,control", "") + p, err := buildNewTelemetryParam("network,control", "", "") assert.NoError(t, err) assert.False(t, p.Enabled.Valid(), "Enabled must be unset for an opt-in selection") assert.True(t, p.Browser.Network.Enabled.Valid()) @@ -366,7 +369,7 @@ func TestBuildTelemetryParam_WireEncoding(t *testing.T) { // enabled=false combined with one. func TestBuildTelemetryParam_ExportWireEncoding(t *testing.T) { t.Run("destination by CUID sets id", func(t *testing.T) { - p, err := buildNewTelemetryParam("", "abcdefghijklmnopqrstuvwx") + p, err := buildNewTelemetryParam("", "", "abcdefghijklmnopqrstuvwx") assert.NoError(t, err) otlp := p.Export.Otlp assert.True(t, otlp.Destination.ID.Valid()) @@ -375,7 +378,7 @@ func TestBuildTelemetryParam_ExportWireEncoding(t *testing.T) { assert.False(t, otlp.Enabled.Valid(), "a destination implies enabled server-side") }) t.Run("destination by name sets name", func(t *testing.T) { - p, err := buildNewTelemetryParam("", "my-collector") + p, err := buildNewTelemetryParam("", "", "my-collector") assert.NoError(t, err) otlp := p.Export.Otlp assert.True(t, otlp.Destination.Name.Valid()) @@ -383,20 +386,20 @@ func TestBuildTelemetryParam_ExportWireEncoding(t *testing.T) { assert.False(t, otlp.Destination.ID.Valid(), "id must be unset when name is sent") }) t.Run("destination implies capture on create", func(t *testing.T) { - p, err := buildNewTelemetryParam("", "my-collector") + p, err := buildNewTelemetryParam("", "", "my-collector") assert.NoError(t, err) assert.True(t, p.Enabled.Valid(), "export requires capture, so create implies it") assert.True(t, p.Enabled.Value) }) t.Run("explicit --telemetry selection is preserved", func(t *testing.T) { - p, err := buildNewTelemetryParam("network,control", "my-collector") + p, err := buildNewTelemetryParam("network,control", "", "my-collector") assert.NoError(t, err) assert.False(t, p.Enabled.Valid(), "an opt-in selection must not be overridden") assert.True(t, p.Browser.Network.Enabled.Value) assert.Equal(t, "my-collector", p.Export.Otlp.Destination.Name.Value) }) t.Run("off disables export without a destination", func(t *testing.T) { - p, err := buildNewTelemetryParam("all", "off") + p, err := buildNewTelemetryParam("all", "", "off") assert.NoError(t, err) otlp := p.Export.Otlp assert.True(t, otlp.Enabled.Valid()) @@ -405,7 +408,7 @@ func TestBuildTelemetryParam_ExportWireEncoding(t *testing.T) { assert.False(t, otlp.Destination.Name.Valid()) }) t.Run("off does not imply capture", func(t *testing.T) { - p, err := buildNewTelemetryParam("", "off") + p, err := buildNewTelemetryParam("", "", "off") assert.NoError(t, err) assert.False(t, p.Enabled.Valid(), "disabling export must not turn capture on") }) @@ -414,44 +417,44 @@ func TestBuildTelemetryParam_ExportWireEncoding(t *testing.T) { // same request. Update and login refuse to supply one: doing so would replace // the connection's current category selection. t.Run("update requires an explicit --telemetry alongside a destination", func(t *testing.T) { - _, err := buildManagedAuthTelemetryParam("", "my-collector", false) + _, err := buildManagedAuthTelemetryParam("", "", "my-collector", false) assert.Error(t, err) assert.Contains(t, err.Error(), "also requires --telemetry") }) t.Run("login requires an explicit --telemetry alongside a destination", func(t *testing.T) { - _, err := buildManagedAuthTelemetryParam("", "my-collector", false) + _, err := buildManagedAuthTelemetryParam("", "", "my-collector", false) assert.Error(t, err) assert.Contains(t, err.Error(), "also requires --telemetry") }) t.Run("update never implies capture when --telemetry is given", func(t *testing.T) { - p, err := buildManagedAuthTelemetryParam("console", "my-collector", false) + p, err := buildManagedAuthTelemetryParam("console", "", "my-collector", false) assert.NoError(t, err) assert.False(t, p.Enabled.Valid(), "an opt-in selection must not be widened to the default set") assert.True(t, p.Browser.Console.Enabled.Value) assert.Equal(t, "my-collector", p.Export.Otlp.Destination.Name.Value) }) t.Run("login never implies capture when --telemetry is given", func(t *testing.T) { - p, err := buildManagedAuthTelemetryParam("console", "my-collector", false) + p, err := buildManagedAuthTelemetryParam("console", "", "my-collector", false) assert.NoError(t, err) assert.False(t, p.Enabled.Valid()) assert.Equal(t, "my-collector", p.Export.Otlp.Destination.Name.Value) }) t.Run("update and login allow export=off without --telemetry", func(t *testing.T) { - u, err := buildManagedAuthTelemetryParam("", "off", false) + u, err := buildManagedAuthTelemetryParam("", "", "off", false) assert.NoError(t, err) assert.False(t, u.Export.Otlp.Enabled.Value) - l, err := buildManagedAuthTelemetryParam("", "off", false) + l, err := buildManagedAuthTelemetryParam("", "", "off", false) assert.NoError(t, err) assert.False(t, l.Export.Otlp.Enabled.Value) }) t.Run("auth connection create implies capture", func(t *testing.T) { - p, err := buildManagedAuthTelemetryParam("", "my-collector", true) + p, err := buildManagedAuthTelemetryParam("", "", "my-collector", true) assert.NoError(t, err) assert.True(t, p.Enabled.Valid()) assert.True(t, p.Enabled.Value) }) t.Run("invalid category still errors with export set", func(t *testing.T) { - _, err := buildNewTelemetryParam("bogus", "my-collector") + _, err := buildNewTelemetryParam("bogus", "", "my-collector") assert.Error(t, err) }) t.Run("telemetry=off with a destination is rejected", func(t *testing.T) { @@ -459,9 +462,9 @@ func TestBuildTelemetryParam_ExportWireEncoding(t *testing.T) { name string fn func() error }{ - {"create", func() error { _, e := buildNewTelemetryParam("off", "my-collector"); return e }}, - {"auth create", func() error { _, e := buildManagedAuthTelemetryParam("off", "my-collector", true); return e }}, - {"auth update/login", func() error { _, e := buildManagedAuthTelemetryParam("off", "my-collector", false); return e }}, + {"create", func() error { _, e := buildNewTelemetryParam("off", "", "my-collector"); return e }}, + {"auth create", func() error { _, e := buildManagedAuthTelemetryParam("off", "", "my-collector", true); return e }}, + {"auth update/login", func() error { _, e := buildManagedAuthTelemetryParam("off", "", "my-collector", false); return e }}, } { err := tc.fn() assert.Error(t, err, tc.name) @@ -469,13 +472,13 @@ func TestBuildTelemetryParam_ExportWireEncoding(t *testing.T) { } }) t.Run("telemetry=off with export=off is allowed", func(t *testing.T) { - p, err := buildNewTelemetryParam("off", "off") + p, err := buildNewTelemetryParam("off", "", "off") assert.NoError(t, err) assert.False(t, p.Enabled.Value) assert.False(t, p.Export.Otlp.Enabled.Value) }) t.Run("empty export value errors", func(t *testing.T) { - _, err := buildNewTelemetryParam("all", " ") + _, err := buildNewTelemetryParam("all", "", " ") assert.Error(t, err) }) } @@ -718,3 +721,82 @@ func TestTelemetryEvents_FullScanIgnoresOffsetUsesSince(t *testing.T) { assert.Equal(t, "5m", gotQuery.Since.Value, "--all walks the window from --since") _ = buf } + +func TestParseTelemetryCategories_Platform(t *testing.T) { + p, err := parseTelemetryCategories("platform") + + assert.NoError(t, err) + assert.True(t, p.Platform.Enabled.Valid()) + assert.True(t, p.Platform.Enabled.Value) + // platform is opt-in only, so it must be offered by the flag's error message too. + _, err = parseTelemetryCategories("bogus") + assert.ErrorContains(t, err, "platform") +} + +func TestTelemetryEnabledCategories_Platform(t *testing.T) { + cfg := kernel.BrowserTelemetryConfig{Browser: kernel.BrowserTelemetryCategoriesConfig{}} + cfg.Browser.Platform.Enabled = true + + assert.Equal(t, []string{"platform"}, telemetryEnabledCategories(cfg)) +} + +func TestParseTelemetryCdpExcludedMethods(t *testing.T) { + t.Run("canonicalizes and trims", func(t *testing.T) { + got, err := parseTelemetryCdpExcludedMethods(" input.dispatchmouseevent , Page.captureScreenshot ") + assert.NoError(t, err) + assert.Equal(t, []kernel.BrowserCdpCommandMethod{ + kernel.BrowserCdpCommandMethodInputDispatchMouseEvent, + kernel.BrowserCdpCommandMethodPageCaptureScreenshot, + }, got) + }) + t.Run("none clears the list", func(t *testing.T) { + got, err := parseTelemetryCdpExcludedMethods("none") + assert.NoError(t, err) + assert.NotNil(t, got, "an empty list must still be sent, so the API reports every method again") + assert.Empty(t, got) + }) + t.Run("rejects unknown methods", func(t *testing.T) { + _, err := parseTelemetryCdpExcludedMethods("Page.doesNotExist") + assert.ErrorContains(t, err, "unknown CDP method") + }) +} + +func TestBuildTelemetryParam_CdpExclude(t *testing.T) { + t.Run("merges into control without enabling it", func(t *testing.T) { + p, err := buildNewTelemetryParam("", "Input.dispatchMouseEvent", "") + assert.NoError(t, err) + assert.False(t, p.Enabled.Valid()) + assert.False(t, p.Browser.Control.Enabled.Valid(), "exclusions must not silently flip the control category") + assert.Equal(t, []kernel.BrowserCdpCommandMethod{ + kernel.BrowserCdpCommandMethodInputDispatchMouseEvent, + }, p.Browser.Control.Cdp.ExcludedMethods) + }) + t.Run("combines with a category selection", func(t *testing.T) { + p, err := buildUpdateTelemetryParam("control,network", "Page.captureScreenshot") + assert.NoError(t, err) + assert.True(t, p.Browser.Control.Enabled.Value) + assert.Equal(t, []kernel.BrowserCdpCommandMethod{ + kernel.BrowserCdpCommandMethodPageCaptureScreenshot, + }, p.Browser.Control.Cdp.ExcludedMethods) + }) + t.Run("rejects combining with telemetry off", func(t *testing.T) { + _, err := buildNewTelemetryParam("off", "Page.captureScreenshot", "") + assert.ErrorContains(t, err, "cannot combine --telemetry=off with --telemetry-cdp-exclude") + }) +} + +func TestBuildManagedAuthTelemetryParam_CdpExcludeNeedsCategories(t *testing.T) { + // The connection stores the config verbatim, so exclusions on their own would + // replace its category selection — allowed on create, rejected on update/login. + _, err := buildManagedAuthTelemetryParam("", "Page.navigate", "", false) + assert.ErrorContains(t, err, "also requires --telemetry in the same command") + + p, err := buildManagedAuthTelemetryParam("", "Page.navigate", "", true) + assert.NoError(t, err) + assert.Equal(t, []kernel.BrowserCdpCommandMethod{ + kernel.BrowserCdpCommandMethodPageNavigate, + }, p.Browser.Control.Cdp.ExcludedMethods) + + _, err = buildManagedAuthTelemetryParam("control", "Page.navigate", "", false) + assert.NoError(t, err) +} diff --git a/go.mod b/go.mod index 0e346401..cbedb97c 100644 --- a/go.mod +++ b/go.mod @@ -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.93.0 + github.com/kernel/kernel-go-sdk v0.93.1-0.20260821151320-9a36566d8999 github.com/klauspost/compress v1.18.5 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/pterm/pterm v0.12.80 diff --git a/go.sum b/go.sum index 78374443..7b5172cd 100644 --- a/go.sum +++ b/go.sum @@ -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.93.0 h1:mPsZKoQlLsgsC0TehWJ/Q5XqWwKu33bKfnuqnfNHtjs= -github.com/kernel/kernel-go-sdk v0.93.0/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= +github.com/kernel/kernel-go-sdk v0.93.1-0.20260821151320-9a36566d8999 h1:+BIiUH4JK5tCi3P57exQgbyGEXCrFNY3OpMuuI931Xo= +github.com/kernel/kernel-go-sdk v0.93.1-0.20260821151320-9a36566d8999/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= From 063d7f5e4e0f7ca1986aee458495956a39fb3d2c Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Fri, 21 Aug 2026 17:44:24 +0000 Subject: [PATCH 8/8] CLI: Update Go SDK to c042837 and drop the telemetry control/platform split MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updates kernel-go-sdk to c0428370612f0ae242d9c4cbbf87e6a6436ff9d9. The previous update (f9b126f) targeted SDK commit 9a36566d8999, which is not reachable from the SDK's main branch — that telemetry control/platform split never landed. Against c042837 the CLI no longer compiled: BrowserTelemetryControlConfigParam, BrowserCdpCommandMethod, the `platform` category, and `control.cdp` do not exist. This reverts f9b126f's code changes, so the CLI is back to the nine categories the SDK actually ships (captcha, connection, console, control, interaction, network, page, screenshot, system) and the `--telemetry-cdp-exclude` flag is gone. The only API-surface change between the CLI's previous SDK and c042837 is browser_routing.go adding "computer" and "playwright" to the direct-to-VM routing allowlist — an internal default with no CLI-visible effect. Coverage analysis: a full enumeration of the 140 methods in api.md against the CLI command tree found no missing commands, and a field-by-field pass over every Params struct found no missing flags. The x-cli-skip endpoints (site-configs, auth connection exchange) remain excluded. Tested against the production API: - browsers create --telemetry=console,network / update --telemetry=page / telemetry events / get / delete - browser-pools create --telemetry=console / get / update --telemetry=network / delete - browsers create --telemetry-cdp-exclude now correctly rejects the removed flag - read-only sweep: auth context, auth connections list, browsers list, browser-pools list, app list, proxies list, profiles list, extensions list, org entitlements, telemetry destinations list, credentials list, projects list - go build ./..., go vet ./..., go test ./... all pass Triggered by: kernel/kernel-go-sdk@c0428370612f0ae242d9c4cbbf87e6a6436ff9d9 Co-Authored-By: Claude Opus 5 --- cmd/auth_connections.go | 60 +++++------- cmd/browser_pools.go | 87 +++++++---------- cmd/browser_pools_test.go | 6 +- cmd/browsers.go | 126 +++++++++++------------- cmd/browsers_telemetry.go | 173 +++++---------------------------- cmd/browsers_telemetry_test.go | 130 +++++-------------------- go.mod | 2 +- go.sum | 4 +- 8 files changed, 171 insertions(+), 417 deletions(-) diff --git a/cmd/auth_connections.go b/cmd/auth_connections.go index d7fc1f9a..c6aedcfa 100644 --- a/cmd/auth_connections.go +++ b/cmd/auth_connections.go @@ -57,7 +57,6 @@ type AuthConnectionCreateInput struct { NoAutoReauth bool RecordSession BoolFlag Telemetry string - TelemetryCdpExclude string TelemetryExport string Output string } @@ -93,7 +92,6 @@ type AuthConnectionUpdateInput struct { AutoReauth BoolFlag RecordSession BoolFlag Telemetry string - TelemetryCdpExclude string TelemetryExport string Output string } @@ -113,16 +111,15 @@ type AuthConnectionDeleteInput struct { } type AuthConnectionLoginInput struct { - ID string - ProxyID string - ProxyName string - ProxyMode string - Stealth BoolFlag - RecordSession BoolFlag - Telemetry string - TelemetryCdpExclude string - TelemetryExport string - Output string + ID string + ProxyID string + ProxyName string + ProxyMode string + Stealth BoolFlag + RecordSession BoolFlag + Telemetry string + TelemetryExport string + Output string } type AuthConnectionSubmitInput struct { @@ -240,8 +237,8 @@ func (c AuthConnectionCmd) Create(ctx context.Context, in AuthConnectionCreateIn params.ManagedAuthCreateRequest.RecordSession = kernel.Opt(in.RecordSession.Value) } - if in.Telemetry != "" || in.TelemetryCdpExclude != "" || in.TelemetryExport != "" { - t, err := buildManagedAuthTelemetryParam(in.Telemetry, in.TelemetryCdpExclude, in.TelemetryExport, true) + if in.Telemetry != "" || in.TelemetryExport != "" { + t, err := buildManagedAuthTelemetryParam(in.Telemetry, in.TelemetryExport, true) if err != nil { return err } @@ -386,8 +383,8 @@ func (c AuthConnectionCmd) Update(ctx context.Context, in AuthConnectionUpdateIn hasChanges = true } - if in.Telemetry != "" || in.TelemetryCdpExclude != "" || in.TelemetryExport != "" { - t, err := buildManagedAuthTelemetryParam(in.Telemetry, in.TelemetryCdpExclude, in.TelemetryExport, false) + if in.Telemetry != "" || in.TelemetryExport != "" { + t, err := buildManagedAuthTelemetryParam(in.Telemetry, in.TelemetryExport, false) if err != nil { return err } @@ -784,8 +781,8 @@ func (c AuthConnectionCmd) Login(ctx context.Context, in AuthConnectionLoginInpu params.RecordSession = kernel.Opt(in.RecordSession.Value) } - if in.Telemetry != "" || in.TelemetryCdpExclude != "" || in.TelemetryExport != "" { - t, err := buildManagedAuthTelemetryParam(in.Telemetry, in.TelemetryCdpExclude, in.TelemetryExport, false) + if in.Telemetry != "" || in.TelemetryExport != "" { + t, err := buildManagedAuthTelemetryParam(in.Telemetry, in.TelemetryExport, false) if err != nil { return err } @@ -1282,7 +1279,6 @@ func init() { authConnectionsCreateCmd.Flags().Bool("record-session", false, "Record browser sessions for this connection by default (useful for debugging)") authConnectionsCreateCmd.Flags().String("telemetry", "", "Configure telemetry for this connection's browser sessions (opt-in): --telemetry=all (default set), --telemetry=off (disable), or --telemetry=console,network (capture exactly those categories)") authConnectionsCreateCmd.Flags().String("telemetry-export-otlp", "", "Export this connection's captured telemetry over OTLP to one of the org's configured destinations, by ID or name; --telemetry-export-otlp=off disables export. Implies --telemetry=all when --telemetry is not set, since export requires capture") - authConnectionsCreateCmd.Flags().String("telemetry-cdp-exclude", "", "Leave the named CDP methods out of control telemetry's cdp_command events, comma-separated (e.g. Input.dispatchMouseEvent,Page.captureScreenshot); --telemetry-cdp-exclude=none clears the list. Excluded commands are still relayed to the browser, they just produce no event") _ = authConnectionsCreateCmd.MarkFlagRequired("domain") _ = authConnectionsCreateCmd.MarkFlagRequired("profile-name") authConnectionsCreateCmd.MarkFlagsMutuallyExclusive("credential-name", "credential-provider") @@ -1312,7 +1308,6 @@ func init() { authConnectionsUpdateCmd.Flags().Bool("record-session", false, "Set whether browser sessions are recorded by default; use --record-session=false to disable") authConnectionsUpdateCmd.Flags().String("telemetry", "", "Update telemetry for future browser sessions: --telemetry=all (reset to default set), --telemetry=off (disable), or --telemetry=console,network (merge those categories into the current selection)") authConnectionsUpdateCmd.Flags().String("telemetry-export-otlp", "", "Update where future sessions export captured telemetry over OTLP, by destination ID or name; --telemetry-export-otlp=off disables export. Naming a destination requires passing --telemetry in the same command, since export and capture are validated together") - authConnectionsUpdateCmd.Flags().String("telemetry-cdp-exclude", "", "Leave the named CDP methods out of control telemetry's cdp_command events, comma-separated (e.g. Input.dispatchMouseEvent,Page.captureScreenshot); --telemetry-cdp-exclude=none clears the list. Excluded commands are still relayed to the browser, they just produce no event") authConnectionsUpdateCmd.MarkFlagsMutuallyExclusive("credential-name", "credential-provider") authConnectionsUpdateCmd.MarkFlagsMutuallyExclusive("save-credentials", "no-save-credentials") authConnectionsUpdateCmd.MarkFlagsMutuallyExclusive("health-checks", "no-health-checks") @@ -1338,7 +1333,6 @@ func init() { authConnectionsLoginCmd.Flags().Bool("record-session", false, "Override whether this login's browser session is recorded; use --record-session=false to disable") authConnectionsLoginCmd.Flags().String("telemetry", "", "Telemetry override for this login only, merged onto the connection's config: --telemetry=all, --telemetry=off, or --telemetry=console,network") authConnectionsLoginCmd.Flags().String("telemetry-export-otlp", "", "Export override for this login only: an OTLP destination ID or name; --telemetry-export-otlp=off disables export for this login. Naming a destination requires passing --telemetry in the same command, since export and capture are validated together") - authConnectionsLoginCmd.Flags().String("telemetry-cdp-exclude", "", "Leave the named CDP methods out of control telemetry's cdp_command events, comma-separated (e.g. Input.dispatchMouseEvent,Page.captureScreenshot); --telemetry-cdp-exclude=none clears the list. Excluded commands are still relayed to the browser, they just produce no event") // Submit flags addJSONOutputFlag(authConnectionsSubmitCmd) @@ -1393,7 +1387,6 @@ func runAuthConnectionsCreate(cmd *cobra.Command, args []string) error { noHealthChecks, _ := cmd.Flags().GetBool("no-health-checks") noAutoReauth, _ := cmd.Flags().GetBool("no-auto-reauth") telemetry, _ := cmd.Flags().GetString("telemetry") - telemetryCdpExclude, _ := cmd.Flags().GetString("telemetry-cdp-exclude") telemetryExport, _ := cmd.Flags().GetString("telemetry-export-otlp") svc := client.Auth.Connections @@ -1417,7 +1410,6 @@ func runAuthConnectionsCreate(cmd *cobra.Command, args []string) error { NoAutoReauth: noAutoReauth, RecordSession: readBoolFlag(cmd.Flags(), "record-session"), Telemetry: telemetry, - TelemetryCdpExclude: telemetryCdpExclude, TelemetryExport: telemetryExport, Output: output, }) @@ -1451,7 +1443,6 @@ func runAuthConnectionsUpdate(cmd *cobra.Command, args []string) error { noSaveCredentials, _ := cmd.Flags().GetBool("no-save-credentials") healthCheckInterval, _ := cmd.Flags().GetInt("health-check-interval") telemetry, _ := cmd.Flags().GetString("telemetry") - telemetryCdpExclude, _ := cmd.Flags().GetString("telemetry-cdp-exclude") telemetryExport, _ := cmd.Flags().GetString("telemetry-export-otlp") saveCredentialsFlag := BoolFlag{} @@ -1505,7 +1496,6 @@ func runAuthConnectionsUpdate(cmd *cobra.Command, args []string) error { AutoReauth: togglePair("auto-reauth", "no-auto-reauth"), RecordSession: readBoolFlag(cmd.Flags(), "record-session"), Telemetry: telemetry, - TelemetryCdpExclude: telemetryCdpExclude, TelemetryExport: telemetryExport, Output: output, }) @@ -1551,22 +1541,20 @@ func runAuthConnectionsLogin(cmd *cobra.Command, args []string) error { proxyName, _ := cmd.Flags().GetString("proxy-name") proxyMode, _ := cmd.Flags().GetString("proxy-mode") telemetry, _ := cmd.Flags().GetString("telemetry") - telemetryCdpExclude, _ := cmd.Flags().GetString("telemetry-cdp-exclude") telemetryExport, _ := cmd.Flags().GetString("telemetry-export-otlp") svc := client.Auth.Connections c := AuthConnectionCmd{svc: &svc} return c.Login(cmd.Context(), AuthConnectionLoginInput{ - ID: args[0], - ProxyID: proxyID, - ProxyName: proxyName, - ProxyMode: proxyMode, - Stealth: readBoolFlag(cmd.Flags(), "stealth"), - RecordSession: readBoolFlag(cmd.Flags(), "record-session"), - Telemetry: telemetry, - TelemetryCdpExclude: telemetryCdpExclude, - TelemetryExport: telemetryExport, - Output: output, + ID: args[0], + ProxyID: proxyID, + ProxyName: proxyName, + ProxyMode: proxyMode, + Stealth: readBoolFlag(cmd.Flags(), "stealth"), + RecordSession: readBoolFlag(cmd.Flags(), "record-session"), + Telemetry: telemetry, + TelemetryExport: telemetryExport, + Output: output, }) } diff --git a/cmd/browser_pools.go b/cmd/browser_pools.go index 276018d7..c6f7051b 100644 --- a/cmd/browser_pools.go +++ b/cmd/browser_pools.go @@ -108,24 +108,21 @@ func (c BrowserPoolsCmd) List(ctx context.Context, in BrowserPoolsListInput) err return nil } -// buildPoolNewTelemetryParam converts --telemetry and --telemetry-cdp-exclude flag -// values to the pool create param. -func buildPoolNewTelemetryParam(s, cdpExclude string) (kernel.BrowserPoolNewParamsTelemetry, error) { - enabled, browser, err := resolveTelemetryFlag(s, cdpExclude) +// 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 --telemetry and --telemetry-cdp-exclude flag -// values to the pool update param. -func buildPoolUpdateTelemetryParam(s, cdpExclude string) (kernel.BrowserPoolUpdateParamsTelemetry, error) { - enabled, browser, err := resolveTelemetryFlag(s, cdpExclude) +// 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 --telemetry and --telemetry-cdp-exclude flag -// values to the acquire override param. -func buildPoolAcquireTelemetryParam(s, cdpExclude string) (kernel.BrowserPoolAcquireParamsTelemetry, error) { - enabled, browser, err := resolveTelemetryFlag(s, cdpExclude) +// 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 } @@ -135,11 +132,7 @@ func formatPoolTelemetry(cfg kernel.BrowserTelemetryConfig) string { if len(on) == 0 { return "disabled" } - base := strings.Join(on, ", ") - if ex := formatCdpExcludedMethods(cfg.Browser.Control.Cdp.ExcludedMethods); ex != "" { - return base + " (excluding CDP methods: " + ex + ")" - } - return base + return strings.Join(on, ", ") } type BrowserPoolsCreateInput struct { @@ -162,7 +155,6 @@ type BrowserPoolsCreateInput struct { ChromePolicy string ChromePolicyFile string Telemetry string - TelemetryCdpExclude string Output string } @@ -255,8 +247,8 @@ func (c BrowserPoolsCmd) Create(ctx context.Context, in BrowserPoolsCreateInput) params.ChromePolicy = chromePolicy } - if in.Telemetry != "" || in.TelemetryCdpExclude != "" { - t, err := buildPoolNewTelemetryParam(in.Telemetry, in.TelemetryCdpExclude) + if in.Telemetry != "" { + t, err := buildPoolNewTelemetryParam(in.Telemetry) if err != nil { return err } @@ -277,7 +269,7 @@ func (c BrowserPoolsCmd) Create(ctx context.Context, in BrowserPoolsCreateInput) } else { pterm.Success.Printf("Created browser pool %s\n", pool.ID) } - if in.Telemetry != "" || in.TelemetryCdpExclude != "" { + if in.Telemetry != "" { printTelemetrySummary(pool.BrowserPoolConfig.Telemetry) } return nil @@ -358,7 +350,6 @@ type BrowserPoolsUpdateInput struct { ChromePolicyFile string ClearChromePolicy bool Telemetry string - TelemetryCdpExclude string DiscardAllIdle BoolFlag Output string } @@ -497,8 +488,8 @@ func (c BrowserPoolsCmd) Update(ctx context.Context, in BrowserPoolsUpdateInput) params.SetExtraFields(extraFields) } - if in.Telemetry != "" || in.TelemetryCdpExclude != "" { - t, err := buildPoolUpdateTelemetryParam(in.Telemetry, in.TelemetryCdpExclude) + if in.Telemetry != "" { + t, err := buildPoolUpdateTelemetryParam(in.Telemetry) if err != nil { return err } @@ -519,7 +510,7 @@ func (c BrowserPoolsCmd) Update(ctx context.Context, in BrowserPoolsUpdateInput) } else { pterm.Success.Printf("Updated browser pool %s\n", pool.ID) } - if in.Telemetry != "" || in.TelemetryCdpExclude != "" { + if in.Telemetry != "" { printTelemetrySummary(pool.BrowserPoolConfig.Telemetry) } return nil @@ -544,14 +535,13 @@ func (c BrowserPoolsCmd) Delete(ctx context.Context, in BrowserPoolsDeleteInput) } type BrowserPoolsAcquireInput struct { - IDOrName string - TimeoutSeconds int64 - Name string - StartURL string - Tags map[string]string - Telemetry string - TelemetryCdpExclude string - Output string + IDOrName string + TimeoutSeconds int64 + Name string + StartURL string + Tags map[string]string + Telemetry string + Output string } // buildAcquireParams builds the SDK params for acquiring a browser from a pool. @@ -559,7 +549,7 @@ type BrowserPoolsAcquireInput struct { // path so the per-lease name/tags/start-url/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, telemetryCdpExclude, startURL string) (kernel.BrowserPoolAcquireParams, error) { +func buildAcquireParams(name string, tags map[string]string, timeoutSeconds int64, telemetry, startURL string) (kernel.BrowserPoolAcquireParams, error) { params := kernel.BrowserPoolAcquireParams{} if timeoutSeconds > 0 { params.AcquireTimeoutSeconds = kernel.Int(timeoutSeconds) @@ -573,8 +563,8 @@ func buildAcquireParams(name string, tags map[string]string, timeoutSeconds int6 if len(tags) > 0 { params.Tags = kernel.Tags(tags) } - if telemetry != "" || telemetryCdpExclude != "" { - t, err := buildPoolAcquireTelemetryParam(telemetry, telemetryCdpExclude) + if telemetry != "" { + t, err := buildPoolAcquireTelemetryParam(telemetry) if err != nil { return kernel.BrowserPoolAcquireParams{}, err } @@ -588,7 +578,7 @@ func (c BrowserPoolsCmd) Acquire(ctx context.Context, in BrowserPoolsAcquireInpu return err } - params, err := buildAcquireParams(in.Name, in.Tags, in.TimeoutSeconds, in.Telemetry, in.TelemetryCdpExclude, in.StartURL) + params, err := buildAcquireParams(in.Name, in.Tags, in.TimeoutSeconds, in.Telemetry, in.StartURL) if err != nil { return err } @@ -759,7 +749,6 @@ func init() { 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.Flags().String("telemetry-cdp-exclude", "", "Leave the named CDP methods out of control telemetry's cdp_command events, comma-separated (e.g. Input.dispatchMouseEvent,Page.captureScreenshot); --telemetry-cdp-exclude=none clears the list. Excluded commands are still relayed to the browser, they just produce no event") browserPoolsCreateCmd.MarkFlagsMutuallyExclusive("chrome-policy", "chrome-policy-file") addJSONOutputFlag(browserPoolsGetCmd) @@ -790,7 +779,6 @@ func init() { browserPoolsUpdateCmd.MarkFlagsMutuallyExclusive("chrome-policy", "chrome-policy-file") browserPoolsUpdateCmd.MarkFlagsMutuallyExclusive("private-host", "clear-private-hosts") 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().String("telemetry-cdp-exclude", "", "Leave the named CDP methods out of control telemetry's cdp_command events, comma-separated (e.g. Input.dispatchMouseEvent,Page.captureScreenshot); --telemetry-cdp-exclude=none clears the list. Excluded commands are still relayed to the browser, they just produce no event") browserPoolsUpdateCmd.Flags().Bool("discard-all-idle", false, "Discard all idle browsers") addJSONOutputFlag(browserPoolsUpdateCmd) @@ -801,7 +789,6 @@ func init() { browserPoolsAcquireCmd.Flags().String("start-url", "", "URL to navigate the acquired browser to, overriding the pool's start URL for this acquire only (best-effort)") 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") - browserPoolsAcquireCmd.Flags().String("telemetry-cdp-exclude", "", "Leave the named CDP methods out of control telemetry's cdp_command events, comma-separated (e.g. Input.dispatchMouseEvent,Page.captureScreenshot); --telemetry-cdp-exclude=none clears the list. Excluded commands are still relayed to the browser, they just produce no event") addJSONOutputFlag(browserPoolsAcquireCmd) browserPoolsReleaseCmd.Flags().String("session-id", "", "Browser session ID to release") @@ -858,7 +845,6 @@ func runBrowserPoolsCreate(cmd *cobra.Command, args []string) error { chromePolicy, _ := cmd.Flags().GetString("chrome-policy") chromePolicyFile, _ := cmd.Flags().GetString("chrome-policy-file") telemetry, _ := cmd.Flags().GetString("telemetry") - telemetryCdpExclude, _ := cmd.Flags().GetString("telemetry-cdp-exclude") output, _ := cmd.Flags().GetString("output") in := BrowserPoolsCreateInput{ @@ -881,7 +867,6 @@ func runBrowserPoolsCreate(cmd *cobra.Command, args []string) error { ChromePolicy: chromePolicy, ChromePolicyFile: chromePolicyFile, Telemetry: telemetry, - TelemetryCdpExclude: telemetryCdpExclude, Output: output, } @@ -923,7 +908,6 @@ func runBrowserPoolsUpdate(cmd *cobra.Command, args []string) error { chromePolicyFile, _ := cmd.Flags().GetString("chrome-policy-file") clearChromePolicy, _ := cmd.Flags().GetBool("clear-chrome-policy") telemetry, _ := cmd.Flags().GetString("telemetry") - telemetryCdpExclude, _ := cmd.Flags().GetString("telemetry-cdp-exclude") discardIdle, _ := cmd.Flags().GetBool("discard-all-idle") output, _ := cmd.Flags().GetString("output") @@ -953,7 +937,6 @@ func runBrowserPoolsUpdate(cmd *cobra.Command, args []string) error { ChromePolicyFile: chromePolicyFile, ClearChromePolicy: clearChromePolicy, Telemetry: telemetry, - TelemetryCdpExclude: telemetryCdpExclude, DiscardAllIdle: BoolFlag{Set: cmd.Flags().Changed("discard-all-idle"), Value: discardIdle}, Output: output, } @@ -976,18 +959,16 @@ func runBrowserPoolsAcquire(cmd *cobra.Command, args []string) error { startURL, _ := cmd.Flags().GetString("start-url") tags, _ := tagsFromFlag(cmd, "tag") telemetry, _ := cmd.Flags().GetString("telemetry") - telemetryCdpExclude, _ := cmd.Flags().GetString("telemetry-cdp-exclude") output, _ := cmd.Flags().GetString("output") c := BrowserPoolsCmd{client: &client.BrowserPools} return c.Acquire(cmd.Context(), BrowserPoolsAcquireInput{ - IDOrName: args[0], - TimeoutSeconds: timeout, - Name: name, - StartURL: startURL, - Tags: tags, - Telemetry: telemetry, - TelemetryCdpExclude: telemetryCdpExclude, - Output: output, + IDOrName: args[0], + TimeoutSeconds: timeout, + Name: name, + StartURL: startURL, + Tags: tags, + Telemetry: telemetry, + Output: output, }) } diff --git a/cmd/browser_pools_test.go b/cmd/browser_pools_test.go index 387f3f18..e0a143ff 100644 --- a/cmd/browser_pools_test.go +++ b/cmd/browser_pools_test.go @@ -239,7 +239,7 @@ func TestBrowserPoolsCreate_PrivateHostNormalization(t *testing.T) { // forwarding used by both `browser-pools acquire` and the `browsers create // --pool-id` lease path. func TestBuildAcquireParams(t *testing.T) { - p, err := buildAcquireParams("lease", map[string]string{"env": "prod"}, 30, "console,network", "", "https://example.com") + p, err := buildAcquireParams("lease", map[string]string{"env": "prod"}, 30, "console,network", "https://example.com") assert.NoError(t, err) assert.True(t, p.Name.Valid()) assert.Equal(t, "lease", p.Name.Value) @@ -252,7 +252,7 @@ func TestBuildAcquireParams(t *testing.T) { assert.True(t, p.Telemetry.Browser.Network.Enabled.Value) // Unset inputs produce an empty params struct (nothing forwarded). - empty, err := buildAcquireParams("", nil, 0, "", "", "") + empty, err := buildAcquireParams("", nil, 0, "", "") assert.NoError(t, err) assert.False(t, empty.Name.Valid()) assert.Len(t, empty.Tags, 0) @@ -260,7 +260,7 @@ func TestBuildAcquireParams(t *testing.T) { assert.False(t, empty.StartURL.Valid()) // An invalid category surfaces an error rather than a partial param. - _, err = buildAcquireParams("", nil, 0, "bogus", "", "") + _, err = buildAcquireParams("", nil, 0, "bogus", "") assert.Error(t, err) } diff --git a/cmd/browsers.go b/cmd/browsers.go index 5e4c82b7..326f88d1 100644 --- a/cmd/browsers.go +++ b/cmd/browsers.go @@ -360,32 +360,31 @@ func formatTags(tags kernel.Tags) string { // Inputs for each command type BrowsersCreateInput struct { - TimeoutSeconds int - Stealth BoolFlag - Headless BoolFlag - GPU BoolFlag - Memory string - InvocationID string - Kiosk BoolFlag - ProfileID string - ProfileName string - ProfileSaveChanges BoolFlag - ProxyID string - ProxyName string - ProxyMode string - Region string - PrivateHosts []string - StartURL string - Extensions []string - Viewport string - Telemetry string - TelemetryCdpExclude string - TelemetryExport string - ChromePolicy string - ChromePolicyFile string - Name string - Tags map[string]string - Output string + TimeoutSeconds int + Stealth BoolFlag + Headless BoolFlag + GPU BoolFlag + Memory string + InvocationID string + Kiosk BoolFlag + ProfileID string + ProfileName string + ProfileSaveChanges BoolFlag + ProxyID string + ProxyName string + ProxyMode string + Region string + PrivateHosts []string + StartURL string + Extensions []string + Viewport string + Telemetry string + TelemetryExport string + ChromePolicy string + ChromePolicyFile string + Name string + Tags map[string]string + Output string } type BrowsersDeleteInput struct { @@ -416,7 +415,6 @@ type BrowsersUpdateInput struct { Viewport string Force bool Telemetry string - TelemetryCdpExclude string Name string SetName bool ClearName bool @@ -671,8 +669,8 @@ func (b BrowsersCmd) Create(ctx context.Context, in BrowsersCreateInput) error { } } - if in.Telemetry != "" || in.TelemetryCdpExclude != "" || in.TelemetryExport != "" { - t, err := buildNewTelemetryParam(in.Telemetry, in.TelemetryCdpExclude, in.TelemetryExport) + if in.Telemetry != "" || in.TelemetryExport != "" { + t, err := buildNewTelemetryParam(in.Telemetry, in.TelemetryExport) if err != nil { return err } @@ -707,7 +705,7 @@ func (b BrowsersCmd) Create(ctx context.Context, in BrowsersCreateInput) error { } printBrowserSessionResult(browser.SessionID, browser.CdpWsURL, browser.BrowserLiveViewURL, browser.Profile, browser.ProfileSaveChanges, browser.StartURL, browser.Name, browser.Tags) - if in.Telemetry != "" || in.TelemetryCdpExclude != "" || in.TelemetryExport != "" { + if in.Telemetry != "" || in.TelemetryExport != "" { printTelemetrySummary(browser.Telemetry) } return nil @@ -943,8 +941,8 @@ func (b BrowsersCmd) Update(ctx context.Context, in BrowsersUpdateInput) error { } // Validate that at least one update option is provided - if !hasProxyChange && !hasProfileChange && !hasViewportChange && in.Telemetry == "" && in.TelemetryCdpExclude == "" && !hasNameChange && !hasTagsChange { - return fmt.Errorf("must specify at least one of: --proxy-id, --proxy-name, --proxy-mode, --clear-proxy, --disable-default-proxy, --profile-id, --profile-name, --viewport, --telemetry, --telemetry-cdp-exclude, --name, --clear-name, --tag, or --clear-tags") + if !hasProxyChange && !hasProfileChange && !hasViewportChange && in.Telemetry == "" && !hasNameChange && !hasTagsChange { + return fmt.Errorf("must specify at least one of: --proxy-id, --proxy-name, --proxy-mode, --clear-proxy, --disable-default-proxy, --profile-id, --profile-name, --viewport, --telemetry, --name, --clear-name, --tag, or --clear-tags") } params := kernel.BrowserUpdateParams{} @@ -987,8 +985,8 @@ func (b BrowsersCmd) Update(ctx context.Context, in BrowsersUpdateInput) error { } // Handle telemetry changes - if in.Telemetry != "" || in.TelemetryCdpExclude != "" { - t, err := buildUpdateTelemetryParam(in.Telemetry, in.TelemetryCdpExclude) + if in.Telemetry != "" { + t, err := buildUpdateTelemetryParam(in.Telemetry) if err != nil { return err } @@ -1038,7 +1036,7 @@ func (b BrowsersCmd) Update(ctx context.Context, in BrowsersUpdateInput) error { if hasProfileChange { pterm.Info.Printf("Profile save changes: %t\n", browser.ProfileSaveChanges) } - if in.Telemetry != "" || in.TelemetryCdpExclude != "" { + if in.Telemetry != "" { printTelemetrySummary(browser.Telemetry) } return nil @@ -2684,7 +2682,6 @@ func init() { 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 (reset to default set), --telemetry=off (disable), or --telemetry=console,network (merge those categories into the current selection)") - browsersUpdateCmd.Flags().String("telemetry-cdp-exclude", "", "Leave the named CDP methods out of control telemetry's cdp_command events, comma-separated (e.g. Input.dispatchMouseEvent,Page.captureScreenshot); --telemetry-cdp-exclude=none clears the list. Excluded commands are still relayed to the browser, they just produce no event") browsersUpdateCmd.Flags().String("name", "", "Set a new unique name for the browser session (mutually exclusive with --clear-name)") browsersUpdateCmd.Flags().Bool("clear-name", false, "Clear the browser session name") browsersUpdateCmd.Flags().StringArray("tag", nil, "Set a tag KEY=VALUE (repeatable; up to 50 pairs). Replaces the entire tag set; mutually exclusive with --clear-tags") @@ -2966,7 +2963,6 @@ func init() { 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 (opt-in): --telemetry=all (default set), --telemetry=off (disable), or --telemetry=console,network (capture exactly those categories)") - browsersCreateCmd.Flags().String("telemetry-cdp-exclude", "", "Leave the named CDP methods out of control telemetry's cdp_command events, comma-separated (e.g. Input.dispatchMouseEvent,Page.captureScreenshot); --telemetry-cdp-exclude=none clears the list. Excluded commands are still relayed to the browser, they just produce no event") browsersCreateCmd.Flags().String("telemetry-export-otlp", "", "Export captured telemetry over OTLP to one of the org's configured destinations, by ID or name; --telemetry-export-otlp=off disables export. Implies --telemetry=all when --telemetry is not set, since export requires capture") browsersCreateCmd.Flags().String("name", "", "Optional unique name for the browser session (used to find it later; can be changed with 'browsers update --name')") browsersCreateCmd.Flags().StringArray("tag", nil, "Set a tag KEY=VALUE on the session (repeatable; up to 50 pairs)") @@ -3098,7 +3094,6 @@ func runBrowsersCreate(cmd *cobra.Command, args []string) error { poolID, _ := cmd.Flags().GetString("pool-id") poolName, _ := cmd.Flags().GetString("pool-name") telemetry, _ := cmd.Flags().GetString("telemetry") - telemetryCdpExclude, _ := cmd.Flags().GetString("telemetry-cdp-exclude") telemetryExport, _ := cmd.Flags().GetString("telemetry-export-otlp") name, _ := cmd.Flags().GetString("name") tags, _ := tagsFromFlag(cmd, "tag") @@ -3165,7 +3160,7 @@ func runBrowsersCreate(cmd *cobra.Command, args []string) error { if cmd.Flags().Changed("timeout") && timeout > 0 { acquireTimeout = int64(timeout) } - acquireParams, err := buildAcquireParams(name, tags, acquireTimeout, telemetry, telemetryCdpExclude, startURL) + acquireParams, err := buildAcquireParams(name, tags, acquireTimeout, telemetry, startURL) if err != nil { return err } @@ -3207,32 +3202,31 @@ func runBrowsersCreate(cmd *cobra.Command, args []string) error { } in := BrowsersCreateInput{ - TimeoutSeconds: timeout, - Stealth: BoolFlag{Set: cmd.Flags().Changed("stealth"), Value: stealthVal}, - Headless: BoolFlag{Set: cmd.Flags().Changed("headless"), Value: headlessVal}, - GPU: BoolFlag{Set: cmd.Flags().Changed("gpu"), Value: gpuVal}, - Memory: memory, - InvocationID: invocationID, - Kiosk: BoolFlag{Set: cmd.Flags().Changed("kiosk"), Value: kioskVal}, - ProfileID: profileID, - ProfileName: profileName, - ProfileSaveChanges: BoolFlag{Set: cmd.Flags().Changed("save-changes"), Value: saveChanges}, - ProxyID: proxyID, - ProxyName: proxyName, - ProxyMode: proxyMode, - Region: region, - PrivateHosts: privateHosts, - StartURL: startURL, - Extensions: extensions, - Viewport: viewport, - Telemetry: telemetry, - TelemetryCdpExclude: telemetryCdpExclude, - TelemetryExport: telemetryExport, - ChromePolicy: chromePolicy, - ChromePolicyFile: chromePolicyFile, - Name: name, - Tags: tags, - Output: output, + TimeoutSeconds: timeout, + Stealth: BoolFlag{Set: cmd.Flags().Changed("stealth"), Value: stealthVal}, + Headless: BoolFlag{Set: cmd.Flags().Changed("headless"), Value: headlessVal}, + GPU: BoolFlag{Set: cmd.Flags().Changed("gpu"), Value: gpuVal}, + Memory: memory, + InvocationID: invocationID, + Kiosk: BoolFlag{Set: cmd.Flags().Changed("kiosk"), Value: kioskVal}, + ProfileID: profileID, + ProfileName: profileName, + ProfileSaveChanges: BoolFlag{Set: cmd.Flags().Changed("save-changes"), Value: saveChanges}, + ProxyID: proxyID, + ProxyName: proxyName, + ProxyMode: proxyMode, + Region: region, + PrivateHosts: privateHosts, + StartURL: startURL, + Extensions: extensions, + Viewport: viewport, + Telemetry: telemetry, + TelemetryExport: telemetryExport, + ChromePolicy: chromePolicy, + ChromePolicyFile: chromePolicyFile, + Name: name, + Tags: tags, + Output: output, } svc := client.Browsers @@ -3294,7 +3288,6 @@ func runBrowsersUpdate(cmd *cobra.Command, args []string) error { viewport, _ := cmd.Flags().GetString("viewport") force, _ := cmd.Flags().GetBool("force") telemetry, _ := cmd.Flags().GetString("telemetry") - telemetryCdpExclude, _ := cmd.Flags().GetString("telemetry-cdp-exclude") name, _ := cmd.Flags().GetString("name") clearName, _ := cmd.Flags().GetBool("clear-name") tags, tagsProvided := tagsFromFlag(cmd, "tag") @@ -3315,7 +3308,6 @@ func runBrowsersUpdate(cmd *cobra.Command, args []string) error { Viewport: viewport, Force: force, Telemetry: telemetry, - TelemetryCdpExclude: telemetryCdpExclude, Name: name, SetName: cmd.Flags().Changed("name"), ClearName: clearName, diff --git a/cmd/browsers_telemetry.go b/cmd/browsers_telemetry.go index 03699772..9a37ef36 100644 --- a/cmd/browsers_telemetry.go +++ b/cmd/browsers_telemetry.go @@ -76,9 +76,7 @@ func parseTelemetryCategories(s string) (kernel.BrowserTelemetryCategoriesConfig case "interaction": p.Interaction = on() case "control": - p.Control = kernel.BrowserTelemetryControlConfigParam{Enabled: kernel.Opt(true)} - case "platform": - p.Platform = on() + p.Control = on() case "connection": p.Connection = on() case "system": @@ -94,112 +92,20 @@ func parseTelemetryCategories(s string) (kernel.BrowserTelemetryCategoriesConfig return p, nil } -// cdpCommandMethods are the browser-control commands the CDP proxy reports as -// cdp_command events, and so the values --telemetry-cdp-exclude accepts. -var cdpCommandMethods = []string{ - "Input.dispatchMouseEvent", - "Input.dispatchKeyEvent", - "Input.insertText", - "Input.imeSetComposition", - "Input.dispatchTouchEvent", - "Input.dispatchDragEvent", - "Input.cancelDragging", - "Input.emulateTouchFromMouseEvent", - "Input.synthesizePinchGesture", - "Input.synthesizeScrollGesture", - "Input.synthesizeTapGesture", - "DOM.setFileInputFiles", - "DOM.focus", - "DOM.scrollIntoViewIfNeeded", - "Page.bringToFront", - "Page.captureScreenshot", - "Page.captureSnapshot", - "Page.handleJavaScriptDialog", - "Page.navigate", - "Page.navigateToHistoryEntry", - "Page.reload", - "Page.printToPDF", - "Page.startScreencast", - "Page.stopScreencast", - "Page.stopLoading", - "Page.close", - "Page.setWebLifecycleState", - "Target.activateTarget", - "Target.closeTarget", - "Target.createTarget", - "Target.createBrowserContext", - "Target.disposeBrowserContext", - "Target.openDevTools", - "Browser.cancelDownload", - "Browser.close", - "Browser.setWindowBounds", - "Browser.setContentsSize", - "Autofill.trigger", -} - -// telemetryCdpExcludeNone is the --telemetry-cdp-exclude value that clears the -// exclusion list rather than naming methods to drop. -const telemetryCdpExcludeNone = "none" - -// parseTelemetryCdpExcludedMethods parses a --telemetry-cdp-exclude value into the -// exclusion list carried by the control category. "none" resolves to an empty list, -// which tells the API to report every supported method again. Method names are -// matched case-insensitively and returned in their canonical CDP spelling. -func parseTelemetryCdpExcludedMethods(s string) ([]kernel.BrowserCdpCommandMethod, error) { - methods := []kernel.BrowserCdpCommandMethod{} - if strings.TrimSpace(s) == telemetryCdpExcludeNone { - return methods, nil - } - for _, part := range strings.Split(s, ",") { - name := strings.TrimSpace(part) - if name == "" { - continue - } - i := slices.IndexFunc(cdpCommandMethods, func(m string) bool { return strings.EqualFold(m, name) }) - if i < 0 { - return nil, fmt.Errorf("unknown CDP method %q: must be one of %s, or %q to clear the exclusion list", name, strings.Join(cdpCommandMethods, ", "), telemetryCdpExcludeNone) - } - methods = append(methods, kernel.BrowserCdpCommandMethod(cdpCommandMethods[i])) - } - return methods, nil -} - -// resolveTelemetryFlag interprets the --telemetry and --telemetry-cdp-exclude flag -// values shared by every browser and browser-pool command: "all" enables the default -// set, "off" disables capture, and a comma-separated list opts into exactly those -// categories. Excluded CDP methods are merged into the control category independently -// of the selection, so they survive a later update that only names categories. It -// returns the resolved (enabled, browser) pair so each endpoint can assemble its own -// param type. -func resolveTelemetryFlag(s, cdpExclude string) (param.Opt[bool], kernel.BrowserTelemetryCategoriesConfigParam, error) { - var enabled param.Opt[bool] - var p kernel.BrowserTelemetryCategoriesConfigParam +// resolveTelemetryFlag interprets a --telemetry flag value shared by every browser +// and browser-pool command: "all" enables the default set, "off" disables capture, +// and a comma-separated list opts into exactly those categories. It returns the +// resolved (enabled, browser) pair so each endpoint can assemble its own param type. +func resolveTelemetryFlag(s string) (param.Opt[bool], kernel.BrowserTelemetryCategoriesConfigParam, error) { switch s { case "all": - enabled = kernel.Opt(true) + return kernel.Opt(true), kernel.BrowserTelemetryCategoriesConfigParam{}, nil case "off": - enabled = kernel.Opt(false) + return kernel.Opt(false), kernel.BrowserTelemetryCategoriesConfigParam{}, nil default: - var err error - if p, err = parseTelemetryCategories(s); err != nil { - return enabled, p, err - } - } - if cdpExclude == "" { - return enabled, p, nil + p, err := parseTelemetryCategories(s) + return param.Opt[bool]{}, p, err } - // Exclusion is a control-telemetry setting, so it has no meaning in a request - // that turns capture off. Error messages never lead with a flag token — the - // error style title-cases the first word. - if s == "off" { - return enabled, p, fmt.Errorf("cannot combine --telemetry=off with --telemetry-cdp-exclude: excluding CDP methods only applies while control telemetry is captured") - } - methods, err := parseTelemetryCdpExcludedMethods(cdpExclude) - if err != nil { - return enabled, p, err - } - p.Control.Cdp.ExcludedMethods = methods - return enabled, p, nil } // telemetryExportOff is the --telemetry-export-otlp value that turns export off @@ -261,10 +167,10 @@ func validateTelemetryExportCombo(telemetry, id, name string, canImply bool) err return nil } -// buildNewTelemetryParam converts --telemetry, --telemetry-cdp-exclude and -// --telemetry-export-otlp flag values to the create API param. -func buildNewTelemetryParam(s, cdpExclude, export string) (kernel.BrowserNewParamsTelemetry, error) { - enabled, browser, err := resolveTelemetryFlag(s, cdpExclude) +// buildNewTelemetryParam converts --telemetry and --telemetry-export-otlp flag +// values to the create API param. +func buildNewTelemetryParam(s, export string) (kernel.BrowserNewParamsTelemetry, error) { + enabled, browser, err := resolveTelemetryFlag(s) p := kernel.BrowserNewParamsTelemetry{Enabled: enabled, Browser: browser} if err != nil || export == "" { return p, err @@ -301,37 +207,26 @@ func optIfSet(s string) param.Opt[string] { return kernel.Opt(s) } -// buildUpdateTelemetryParam converts --telemetry and --telemetry-cdp-exclude flag -// values to the update API param. -func buildUpdateTelemetryParam(s, cdpExclude string) (kernel.BrowserUpdateParamsTelemetry, error) { - enabled, browser, err := resolveTelemetryFlag(s, cdpExclude) +// buildUpdateTelemetryParam converts a --telemetry flag value to the update API param. +func buildUpdateTelemetryParam(s string) (kernel.BrowserUpdateParamsTelemetry, error) { + enabled, browser, err := resolveTelemetryFlag(s) return kernel.BrowserUpdateParamsTelemetry{Enabled: enabled, Browser: browser}, err } -// buildManagedAuthTelemetryParam converts --telemetry, --telemetry-cdp-exclude and -// --telemetry-export-otlp flag values to the browser telemetry config carried by an -// auth connection's browser settings, shared by create, update, and login. +// buildManagedAuthTelemetryParam converts --telemetry and --telemetry-export-otlp +// flag values to the browser telemetry config carried by an auth connection's +// browser settings, shared by create, update, and login. // // canImply is true only on create, where there is no stored selection to clobber // and capture can safely be turned on for the user so a destination works on its // own. On update and login it is false: enabling capture there would replace the // connection's current category selection rather than merge onto it. -func buildManagedAuthTelemetryParam(s, cdpExclude, export string, canImply bool) (kernel.ManagedAuthBrowserConfigTelemetryParam, error) { - enabled, browser, err := resolveTelemetryFlag(s, cdpExclude) +func buildManagedAuthTelemetryParam(s, export string, canImply bool) (kernel.ManagedAuthBrowserConfigTelemetryParam, error) { + enabled, browser, err := resolveTelemetryFlag(s) p := kernel.ManagedAuthBrowserConfigTelemetryParam{Enabled: enabled, Browser: browser} - if err != nil { + if err != nil || export == "" { return p, err } - // A connection stores the browser config as sent rather than resolving it, so a - // request carrying only CDP exclusions would drop the connection's category - // selection. On update and login the user has to restate what to capture; on - // create there is nothing to lose. - if cdpExclude != "" && s == "" && !canImply { - return p, fmt.Errorf("setting --telemetry-cdp-exclude also requires --telemetry in the same command: the connection stores its browser config as sent, so exclusions on their own would drop its category selection") - } - if export == "" { - return p, nil - } exEnabled, id, name, err := resolveTelemetryExportFlag(export) if err != nil { return p, err @@ -369,9 +264,6 @@ func formatManagedAuthTelemetry(cfg kernel.ManagedAuthBrowserConfigTelemetry) st } return "disabled" }() - if ex := formatCdpExcludedMethods(cfg.Browser.Control.Cdp.ExcludedMethods); ex != "" { - base += " (excluding CDP methods: " + ex + ")" - } if dest := managedAuthExportDestination(cfg.Export); dest != "" { return base + " (exporting to " + dest + ")" } @@ -395,7 +287,7 @@ func managedAuthExportDestination(ex kernel.ManagedAuthBrowserConfigTelemetryExp // flows automatically whenever a CDP category is captured. var settableCategories = []string{ "console", "network", "page", "interaction", - "control", "connection", "system", "screenshot", "platform", "captcha", + "control", "connection", "system", "screenshot", "captcha", } // streamFilterCategories are the categories accepted by `telemetry stream --categories`. @@ -418,7 +310,6 @@ func telemetryEnabledCategories(cfg kernel.BrowserTelemetryConfig) []string { {"connection", b.Connection.Enabled}, {"system", b.System.Enabled}, {"screenshot", b.Screenshot.Enabled}, - {"platform", b.Platform.Enabled}, {"captcha", b.Captcha.Enabled}, } on := make([]string, 0, len(ordered)) @@ -439,9 +330,6 @@ func printTelemetrySummary(cfg kernel.BrowserTelemetryConfig) { return } pterm.Info.Printf("Telemetry capturing: %s\n", strings.Join(on, ", ")) - if ex := formatCdpExcludedMethods(cfg.Browser.Control.Cdp.ExcludedMethods); ex != "" { - pterm.Info.Printf("Telemetry excluding CDP methods: %s\n", ex) - } if cfg.Export.Otlp.Enabled { // The response reports the resolved destination by ID even when the request // selected it by name. @@ -453,19 +341,6 @@ func printTelemetrySummary(cfg kernel.BrowserTelemetryConfig) { } } -// formatCdpExcludedMethods renders the CDP methods left out of control -// telemetry's cdp_command stream, or "" when every supported method is reported. -func formatCdpExcludedMethods(methods []kernel.BrowserCdpCommandMethod) string { - if len(methods) == 0 { - return "" - } - names := make([]string, 0, len(methods)) - for _, m := range methods { - names = append(names, string(m)) - } - return strings.Join(names, ", ") -} - // shouldEmit applies client-side category/type filters to a telemetry event. func shouldEmit(category, eventType string, categories, types []string) bool { if len(categories) > 0 && !slices.Contains(categories, category) { diff --git a/cmd/browsers_telemetry_test.go b/cmd/browsers_telemetry_test.go index d9458bff..fe3b88e1 100644 --- a/cmd/browsers_telemetry_test.go +++ b/cmd/browsers_telemetry_test.go @@ -299,17 +299,14 @@ func TestShouldEmit(t *testing.T) { } func TestParseTelemetryCategories_OptInList(t *testing.T) { - p, err := parseTelemetryCategories("network,control,captcha,platform") + p, err := parseTelemetryCategories("network,control,captcha") assert.NoError(t, err) // Listed categories are enabled. - for _, c := range []kernel.BrowserTelemetryCategoryConfigParam{p.Network, p.Captcha, p.Platform} { + for _, c := range []kernel.BrowserTelemetryCategoryConfigParam{p.Network, p.Control, p.Captcha} { assert.True(t, c.Enabled.Valid()) assert.True(t, c.Enabled.Value) } - // Control carries its own config type, so it is checked separately. - assert.True(t, p.Control.Enabled.Valid()) - assert.True(t, p.Control.Enabled.Value) // Unlisted categories are omitted (opt-in: the instance treats them as off). assert.False(t, p.Console.Enabled.Valid()) assert.False(t, p.Page.Enabled.Valid()) @@ -339,21 +336,21 @@ func TestParseTelemetryCategories_WhitespaceTolerance(t *testing.T) { // listed categories enabled (Enabled unset). func TestBuildTelemetryParam_WireEncoding(t *testing.T) { t.Run("all", func(t *testing.T) { - p, err := buildNewTelemetryParam("all", "", "") + p, err := buildNewTelemetryParam("all", "") assert.NoError(t, err) assert.True(t, p.Enabled.Valid()) assert.True(t, p.Enabled.Value) assert.False(t, p.Browser.Network.Enabled.Valid()) }) t.Run("off", func(t *testing.T) { - p, err := buildNewTelemetryParam("off", "", "") + p, err := buildNewTelemetryParam("off", "") assert.NoError(t, err) assert.True(t, p.Enabled.Valid()) assert.False(t, p.Enabled.Value) assert.False(t, p.Browser.Network.Enabled.Valid()) }) t.Run("opt-in list sets only Browser", func(t *testing.T) { - p, err := buildNewTelemetryParam("network,control", "", "") + p, err := buildNewTelemetryParam("network,control", "") assert.NoError(t, err) assert.False(t, p.Enabled.Valid(), "Enabled must be unset for an opt-in selection") assert.True(t, p.Browser.Network.Enabled.Valid()) @@ -369,7 +366,7 @@ func TestBuildTelemetryParam_WireEncoding(t *testing.T) { // enabled=false combined with one. func TestBuildTelemetryParam_ExportWireEncoding(t *testing.T) { t.Run("destination by CUID sets id", func(t *testing.T) { - p, err := buildNewTelemetryParam("", "", "abcdefghijklmnopqrstuvwx") + p, err := buildNewTelemetryParam("", "abcdefghijklmnopqrstuvwx") assert.NoError(t, err) otlp := p.Export.Otlp assert.True(t, otlp.Destination.ID.Valid()) @@ -378,7 +375,7 @@ func TestBuildTelemetryParam_ExportWireEncoding(t *testing.T) { assert.False(t, otlp.Enabled.Valid(), "a destination implies enabled server-side") }) t.Run("destination by name sets name", func(t *testing.T) { - p, err := buildNewTelemetryParam("", "", "my-collector") + p, err := buildNewTelemetryParam("", "my-collector") assert.NoError(t, err) otlp := p.Export.Otlp assert.True(t, otlp.Destination.Name.Valid()) @@ -386,20 +383,20 @@ func TestBuildTelemetryParam_ExportWireEncoding(t *testing.T) { assert.False(t, otlp.Destination.ID.Valid(), "id must be unset when name is sent") }) t.Run("destination implies capture on create", func(t *testing.T) { - p, err := buildNewTelemetryParam("", "", "my-collector") + p, err := buildNewTelemetryParam("", "my-collector") assert.NoError(t, err) assert.True(t, p.Enabled.Valid(), "export requires capture, so create implies it") assert.True(t, p.Enabled.Value) }) t.Run("explicit --telemetry selection is preserved", func(t *testing.T) { - p, err := buildNewTelemetryParam("network,control", "", "my-collector") + p, err := buildNewTelemetryParam("network,control", "my-collector") assert.NoError(t, err) assert.False(t, p.Enabled.Valid(), "an opt-in selection must not be overridden") assert.True(t, p.Browser.Network.Enabled.Value) assert.Equal(t, "my-collector", p.Export.Otlp.Destination.Name.Value) }) t.Run("off disables export without a destination", func(t *testing.T) { - p, err := buildNewTelemetryParam("all", "", "off") + p, err := buildNewTelemetryParam("all", "off") assert.NoError(t, err) otlp := p.Export.Otlp assert.True(t, otlp.Enabled.Valid()) @@ -408,7 +405,7 @@ func TestBuildTelemetryParam_ExportWireEncoding(t *testing.T) { assert.False(t, otlp.Destination.Name.Valid()) }) t.Run("off does not imply capture", func(t *testing.T) { - p, err := buildNewTelemetryParam("", "", "off") + p, err := buildNewTelemetryParam("", "off") assert.NoError(t, err) assert.False(t, p.Enabled.Valid(), "disabling export must not turn capture on") }) @@ -417,44 +414,44 @@ func TestBuildTelemetryParam_ExportWireEncoding(t *testing.T) { // same request. Update and login refuse to supply one: doing so would replace // the connection's current category selection. t.Run("update requires an explicit --telemetry alongside a destination", func(t *testing.T) { - _, err := buildManagedAuthTelemetryParam("", "", "my-collector", false) + _, err := buildManagedAuthTelemetryParam("", "my-collector", false) assert.Error(t, err) assert.Contains(t, err.Error(), "also requires --telemetry") }) t.Run("login requires an explicit --telemetry alongside a destination", func(t *testing.T) { - _, err := buildManagedAuthTelemetryParam("", "", "my-collector", false) + _, err := buildManagedAuthTelemetryParam("", "my-collector", false) assert.Error(t, err) assert.Contains(t, err.Error(), "also requires --telemetry") }) t.Run("update never implies capture when --telemetry is given", func(t *testing.T) { - p, err := buildManagedAuthTelemetryParam("console", "", "my-collector", false) + p, err := buildManagedAuthTelemetryParam("console", "my-collector", false) assert.NoError(t, err) assert.False(t, p.Enabled.Valid(), "an opt-in selection must not be widened to the default set") assert.True(t, p.Browser.Console.Enabled.Value) assert.Equal(t, "my-collector", p.Export.Otlp.Destination.Name.Value) }) t.Run("login never implies capture when --telemetry is given", func(t *testing.T) { - p, err := buildManagedAuthTelemetryParam("console", "", "my-collector", false) + p, err := buildManagedAuthTelemetryParam("console", "my-collector", false) assert.NoError(t, err) assert.False(t, p.Enabled.Valid()) assert.Equal(t, "my-collector", p.Export.Otlp.Destination.Name.Value) }) t.Run("update and login allow export=off without --telemetry", func(t *testing.T) { - u, err := buildManagedAuthTelemetryParam("", "", "off", false) + u, err := buildManagedAuthTelemetryParam("", "off", false) assert.NoError(t, err) assert.False(t, u.Export.Otlp.Enabled.Value) - l, err := buildManagedAuthTelemetryParam("", "", "off", false) + l, err := buildManagedAuthTelemetryParam("", "off", false) assert.NoError(t, err) assert.False(t, l.Export.Otlp.Enabled.Value) }) t.Run("auth connection create implies capture", func(t *testing.T) { - p, err := buildManagedAuthTelemetryParam("", "", "my-collector", true) + p, err := buildManagedAuthTelemetryParam("", "my-collector", true) assert.NoError(t, err) assert.True(t, p.Enabled.Valid()) assert.True(t, p.Enabled.Value) }) t.Run("invalid category still errors with export set", func(t *testing.T) { - _, err := buildNewTelemetryParam("bogus", "", "my-collector") + _, err := buildNewTelemetryParam("bogus", "my-collector") assert.Error(t, err) }) t.Run("telemetry=off with a destination is rejected", func(t *testing.T) { @@ -462,9 +459,9 @@ func TestBuildTelemetryParam_ExportWireEncoding(t *testing.T) { name string fn func() error }{ - {"create", func() error { _, e := buildNewTelemetryParam("off", "", "my-collector"); return e }}, - {"auth create", func() error { _, e := buildManagedAuthTelemetryParam("off", "", "my-collector", true); return e }}, - {"auth update/login", func() error { _, e := buildManagedAuthTelemetryParam("off", "", "my-collector", false); return e }}, + {"create", func() error { _, e := buildNewTelemetryParam("off", "my-collector"); return e }}, + {"auth create", func() error { _, e := buildManagedAuthTelemetryParam("off", "my-collector", true); return e }}, + {"auth update/login", func() error { _, e := buildManagedAuthTelemetryParam("off", "my-collector", false); return e }}, } { err := tc.fn() assert.Error(t, err, tc.name) @@ -472,13 +469,13 @@ func TestBuildTelemetryParam_ExportWireEncoding(t *testing.T) { } }) t.Run("telemetry=off with export=off is allowed", func(t *testing.T) { - p, err := buildNewTelemetryParam("off", "", "off") + p, err := buildNewTelemetryParam("off", "off") assert.NoError(t, err) assert.False(t, p.Enabled.Value) assert.False(t, p.Export.Otlp.Enabled.Value) }) t.Run("empty export value errors", func(t *testing.T) { - _, err := buildNewTelemetryParam("all", "", " ") + _, err := buildNewTelemetryParam("all", " ") assert.Error(t, err) }) } @@ -721,82 +718,3 @@ func TestTelemetryEvents_FullScanIgnoresOffsetUsesSince(t *testing.T) { assert.Equal(t, "5m", gotQuery.Since.Value, "--all walks the window from --since") _ = buf } - -func TestParseTelemetryCategories_Platform(t *testing.T) { - p, err := parseTelemetryCategories("platform") - - assert.NoError(t, err) - assert.True(t, p.Platform.Enabled.Valid()) - assert.True(t, p.Platform.Enabled.Value) - // platform is opt-in only, so it must be offered by the flag's error message too. - _, err = parseTelemetryCategories("bogus") - assert.ErrorContains(t, err, "platform") -} - -func TestTelemetryEnabledCategories_Platform(t *testing.T) { - cfg := kernel.BrowserTelemetryConfig{Browser: kernel.BrowserTelemetryCategoriesConfig{}} - cfg.Browser.Platform.Enabled = true - - assert.Equal(t, []string{"platform"}, telemetryEnabledCategories(cfg)) -} - -func TestParseTelemetryCdpExcludedMethods(t *testing.T) { - t.Run("canonicalizes and trims", func(t *testing.T) { - got, err := parseTelemetryCdpExcludedMethods(" input.dispatchmouseevent , Page.captureScreenshot ") - assert.NoError(t, err) - assert.Equal(t, []kernel.BrowserCdpCommandMethod{ - kernel.BrowserCdpCommandMethodInputDispatchMouseEvent, - kernel.BrowserCdpCommandMethodPageCaptureScreenshot, - }, got) - }) - t.Run("none clears the list", func(t *testing.T) { - got, err := parseTelemetryCdpExcludedMethods("none") - assert.NoError(t, err) - assert.NotNil(t, got, "an empty list must still be sent, so the API reports every method again") - assert.Empty(t, got) - }) - t.Run("rejects unknown methods", func(t *testing.T) { - _, err := parseTelemetryCdpExcludedMethods("Page.doesNotExist") - assert.ErrorContains(t, err, "unknown CDP method") - }) -} - -func TestBuildTelemetryParam_CdpExclude(t *testing.T) { - t.Run("merges into control without enabling it", func(t *testing.T) { - p, err := buildNewTelemetryParam("", "Input.dispatchMouseEvent", "") - assert.NoError(t, err) - assert.False(t, p.Enabled.Valid()) - assert.False(t, p.Browser.Control.Enabled.Valid(), "exclusions must not silently flip the control category") - assert.Equal(t, []kernel.BrowserCdpCommandMethod{ - kernel.BrowserCdpCommandMethodInputDispatchMouseEvent, - }, p.Browser.Control.Cdp.ExcludedMethods) - }) - t.Run("combines with a category selection", func(t *testing.T) { - p, err := buildUpdateTelemetryParam("control,network", "Page.captureScreenshot") - assert.NoError(t, err) - assert.True(t, p.Browser.Control.Enabled.Value) - assert.Equal(t, []kernel.BrowserCdpCommandMethod{ - kernel.BrowserCdpCommandMethodPageCaptureScreenshot, - }, p.Browser.Control.Cdp.ExcludedMethods) - }) - t.Run("rejects combining with telemetry off", func(t *testing.T) { - _, err := buildNewTelemetryParam("off", "Page.captureScreenshot", "") - assert.ErrorContains(t, err, "cannot combine --telemetry=off with --telemetry-cdp-exclude") - }) -} - -func TestBuildManagedAuthTelemetryParam_CdpExcludeNeedsCategories(t *testing.T) { - // The connection stores the config verbatim, so exclusions on their own would - // replace its category selection — allowed on create, rejected on update/login. - _, err := buildManagedAuthTelemetryParam("", "Page.navigate", "", false) - assert.ErrorContains(t, err, "also requires --telemetry in the same command") - - p, err := buildManagedAuthTelemetryParam("", "Page.navigate", "", true) - assert.NoError(t, err) - assert.Equal(t, []kernel.BrowserCdpCommandMethod{ - kernel.BrowserCdpCommandMethodPageNavigate, - }, p.Browser.Control.Cdp.ExcludedMethods) - - _, err = buildManagedAuthTelemetryParam("control", "Page.navigate", "", false) - assert.NoError(t, err) -} diff --git a/go.mod b/go.mod index cbedb97c..6b00cac9 100644 --- a/go.mod +++ b/go.mod @@ -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.93.1-0.20260821151320-9a36566d8999 + github.com/kernel/kernel-go-sdk v0.93.1-0.20260821173629-c0428370612f github.com/klauspost/compress v1.18.5 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/pterm/pterm v0.12.80 diff --git a/go.sum b/go.sum index 7b5172cd..50e70bb4 100644 --- a/go.sum +++ b/go.sum @@ -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.93.1-0.20260821151320-9a36566d8999 h1:+BIiUH4JK5tCi3P57exQgbyGEXCrFNY3OpMuuI931Xo= -github.com/kernel/kernel-go-sdk v0.93.1-0.20260821151320-9a36566d8999/go.mod h1:EeZzSuHZVeHKxKCPUzxou2bovNGhXaz0RXrSqKNf1AQ= +github.com/kernel/kernel-go-sdk v0.93.1-0.20260821173629-c0428370612f h1:Nqwb7HXCMYBvltbuGbiD1Ms86aJs9JH46Q9aDNU/Oc8= +github.com/kernel/kernel-go-sdk v0.93.1-0.20260821173629-c0428370612f/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=