From 5cb4f4f35986beb2b28f2d07d17dbb63f0540583 Mon Sep 17 00:00:00 2001 From: Sayan Samanta Date: Tue, 2 Jun 2026 20:16:53 -0700 Subject: [PATCH 1/4] Rework telemetry event categories and make category server-authoritative Split the overloaded `system` category into intent-based categories so the set a caller configures matches the set they see on events. `system` now means VM health only (oom kills, service crashes); client attach/detach lifecycle moves to `connection`, periodic screenshots to `screenshot`, captcha outcomes to `captcha`, CDP-collector health to the auto-managed `monitor`, and `api` is renamed `control`. kernel-images-api is now authoritative on category: a known event type is assigned its canonical category from a generated `CategoryForType` lookup (derived from openapi.yaml via `go:generate`), and any caller-supplied value is ignored. Unknown custom types must carry an explicit category. Behavior changes: - Nothing is force-always-on. The publish filter is "you get the categories you enabled", with `monitor` riding along automatically whenever a CDP category is captured. - Default (empty config / enabled:true) captures every category except `screenshot`, which is heavy base64 data and opt-in. - The CDP collector starts only when a CDP category is enabled, so a system-only subscriber pays no collector cost. Co-authored-by: Cursor --- server/Makefile | 1 + server/cmd/api/api/events.go | 11 +- server/cmd/api/api/events_test.go | 53 +- server/cmd/api/api/middleware.go | 2 +- server/cmd/api/api/middleware_test.go | 2 +- server/cmd/api/api/telemetry.go | 269 ++++---- server/cmd/api/api/telemetry_test.go | 124 ++-- server/go.mod | 2 +- server/lib/cdpmonitor/README.md | 2 +- server/lib/cdpmonitor/monitor.go | 8 +- server/lib/cdpmonitor/monitor_test.go | 2 +- server/lib/cdpmonitor/screenshot.go | 2 +- server/lib/devtoolsproxy/proxy.go | 4 +- server/lib/devtoolsproxy/proxy_test.go | 4 +- server/lib/events/category_gen.go | 45 ++ server/lib/events/event.go | 56 +- server/lib/oapi/oapi.go | 868 ++++++++++++++----------- server/lib/sysmon/README.md | 9 +- server/lib/telemetry/telemetry.go | 53 +- server/lib/telemetry/telemetry_test.go | 19 +- server/openapi.yaml | 100 ++- server/scripts/categorygen/main.go | 88 +++ 22 files changed, 1051 insertions(+), 673 deletions(-) create mode 100644 server/lib/events/category_gen.go create mode 100644 server/scripts/categorygen/main.go diff --git a/server/Makefile b/server/Makefile index 6f98d35b..f51ef0b9 100644 --- a/server/Makefile +++ b/server/Makefile @@ -21,6 +21,7 @@ oapi-generate: @echo "Fixing oapi-codegen issue https://github.com/oapi-codegen/oapi-codegen/issues/1764..." go run ./scripts/oapi/patch_sse_methods -file ./lib/oapi/oapi.go -expected-replacements 4 go fmt ./lib/oapi/oapi.go + go generate ./lib/events/... go mod tidy build: | $(BIN_DIR) diff --git a/server/cmd/api/api/events.go b/server/cmd/api/api/events.go index ac567719..c37e7cd3 100644 --- a/server/cmd/api/api/events.go +++ b/server/cmd/api/api/events.go @@ -27,16 +27,21 @@ func (s *ApiService) PublishTelemetryEvent(_ context.Context, req oapi.PublishTe return oapi.PublishTelemetryEvent400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "type is required"}}, nil } ev := events.Event{Type: body.Type} - ev.Ts = time.Now().UnixMicro() - if body.Category != nil { + + // Category is server-authoritative. A known event type is assigned its + // canonical category and any caller-supplied value is ignored; an unknown + // custom type must carry a valid category from the caller. + if cat, ok := events.CategoryForType(body.Type); ok { + ev.Category = cat + } else if body.Category != nil { cat := oapi.TelemetryEventCategory(*body.Category) if !cat.Valid() { return oapi.PublishTelemetryEvent400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "invalid category"}}, nil } ev.Category = cat } else { - ev.Category = events.System + return oapi.PublishTelemetryEvent400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "category is required for unknown event type"}}, nil } if body.Source != nil { diff --git a/server/cmd/api/api/events_test.go b/server/cmd/api/api/events_test.go index ae5c44f0..6ed0fed3 100644 --- a/server/cmd/api/api/events_test.go +++ b/server/cmd/api/api/events_test.go @@ -54,9 +54,10 @@ func TestEventLifecycle(t *testing.T) { } }() - // Publish an event. + // Publish a custom event. Unknown types must carry an explicit category. + sys := oapi.PublishEventRequestCategorySystem resp, err := svc.PublishTelemetryEvent(ctx, oapi.PublishTelemetryEventRequestObject{ - Body: &oapi.PublishEventRequest{Type: "test.event"}, + Body: &oapi.PublishEventRequest{Type: "test.event", Category: &sys}, }) require.NoError(t, err) r200pub, ok := resp.(publishTelemetryEventOKResponse) @@ -73,17 +74,9 @@ func TestEventLifecycle(t *testing.T) { t.Fatal("timed out waiting for test.event") } - // Stop telemetry by disabling all categories. - f := false + // Stop telemetry by disabling every category. stopResp, err := svc.PatchTelemetry(ctx, oapi.PatchTelemetryRequestObject{ - Body: &oapi.BrowserTelemetryConfig{ - Browser: &oapi.BrowserTelemetryCategoriesConfig{ - Console: &oapi.BrowserTelemetryCategoryConfig{Enabled: &f}, - Network: &oapi.BrowserTelemetryCategoryConfig{Enabled: &f}, - Page: &oapi.BrowserTelemetryCategoryConfig{Enabled: &f}, - Interaction: &oapi.BrowserTelemetryCategoryConfig{Enabled: &f}, - }, - }, + Body: &oapi.BrowserTelemetryConfig{Browser: allCategoriesDisabled()}, }) require.NoError(t, err) assert.IsType(t, oapi.PatchTelemetry200JSONResponse{}, stopResp) @@ -94,13 +87,47 @@ func TestPublishDroppedWhenTelemetryInactive(t *testing.T) { ctx := context.Background() svc := newTestService(t, newMockRecordManager()) + sys := oapi.PublishEventRequestCategorySystem resp, err := svc.PublishTelemetryEvent(ctx, oapi.PublishTelemetryEventRequestObject{ - Body: &oapi.PublishEventRequest{Type: "test.event"}, + Body: &oapi.PublishEventRequest{Type: "test.event", Category: &sys}, }) require.NoError(t, err) assert.IsType(t, oapi.PublishTelemetryEvent204Response{}, resp, "filtered events should return 204") } +func TestPublishRequiresCategoryForUnknownType(t *testing.T) { + t.Parallel() + ctx := context.Background() + svc := newTestService(t, newMockRecordManager()) + _, err := svc.PutTelemetry(ctx, oapi.PutTelemetryRequestObject{}) + require.NoError(t, err) + + resp, err := svc.PublishTelemetryEvent(ctx, oapi.PublishTelemetryEventRequestObject{ + Body: &oapi.PublishEventRequest{Type: "custom.unknown"}, + }) + require.NoError(t, err) + assert.IsType(t, oapi.PublishTelemetryEvent400JSONResponse{}, resp, "unknown type without a category must 400") +} + +func TestPublishKnownTypeCategoryIsServerAuthoritative(t *testing.T) { + t.Parallel() + ctx := context.Background() + svc := newTestService(t, newMockRecordManager()) + _, err := svc.PutTelemetry(ctx, oapi.PutTelemetryRequestObject{}) + require.NoError(t, err) + + // api_call is a known type that maps to the control category. A caller + // supplying a different category must be overridden by the server. + console := oapi.PublishEventRequestCategoryConsole + resp, err := svc.PublishTelemetryEvent(ctx, oapi.PublishTelemetryEventRequestObject{ + Body: &oapi.PublishEventRequest{Type: "api_call", Category: &console}, + }) + require.NoError(t, err) + okResp, ok := resp.(publishTelemetryEventOKResponse) + require.True(t, ok, "expected 200, got %T", resp) + assert.Equal(t, events.Control, okResp.env.Event.Category) +} + func TestPublishDroppedWhenCategoryDisabled(t *testing.T) { t.Parallel() ctx := context.Background() diff --git a/server/cmd/api/api/middleware.go b/server/cmd/api/api/middleware.go index a33fb361..a558199a 100644 --- a/server/cmd/api/api/middleware.go +++ b/server/cmd/api/api/middleware.go @@ -64,7 +64,7 @@ func TelemetryHTTPMiddleware(publish func(events.Event) (events.Envelope, bool)) publish(events.Event{ Ts: time.Now().UnixMicro(), Type: "api_call", - Category: events.Api, + Category: events.Control, Source: oapi.BrowserEventSource{Kind: oapi.KernelApi}, Data: data, }) diff --git a/server/cmd/api/api/middleware_test.go b/server/cmd/api/api/middleware_test.go index f49739da..972967f4 100644 --- a/server/cmd/api/api/middleware_test.go +++ b/server/cmd/api/api/middleware_test.go @@ -80,7 +80,7 @@ func TestTelemetryMiddleware_EmitsApiCallEventOnDocumentedRoute(t *testing.T) { require.Len(t, captured, 1) ev := captured[0] assert.Equal(t, "api_call", ev.Type) - assert.Equal(t, events.Api, ev.Category) + assert.Equal(t, events.Control, ev.Category) assert.Equal(t, oapi.KernelApi, ev.Source.Kind) var data struct { diff --git a/server/cmd/api/api/telemetry.go b/server/cmd/api/api/telemetry.go index fd013b09..f8b09f22 100644 --- a/server/cmd/api/api/telemetry.go +++ b/server/cmd/api/api/telemetry.go @@ -26,7 +26,7 @@ func (s *ApiService) GetTelemetry(_ context.Context, _ oapi.GetTelemetryRequestO // PutTelemetry handles PUT /telemetry. // Sets the telemetry configuration. Returns 201 if not previously configured, 200 if it was. -// Setting all five categories to enabled:false clears the configuration (200). +// Setting every configurable category to enabled:false clears the configuration (200). func (s *ApiService) PutTelemetry(ctx context.Context, req oapi.PutTelemetryRequestObject) (oapi.PutTelemetryResponseObject, error) { s.monitorMu.Lock() defer s.monitorMu.Unlock() @@ -39,44 +39,39 @@ func (s *ApiService) PutTelemetry(ctx context.Context, req oapi.PutTelemetryRequ wasActive := s.telemetrySession.Active() if allDisabled { - if !wasActive { - // Already cleared; all-disabled is idempotent. - return oapi.PutTelemetry200JSONResponse(oapi.TelemetryState{Config: disabledConfig(), Seq: int64(s.telemetrySession.Seq())}), nil + if wasActive { + s.telemetrySession.Stop() + _ = s.applyTelemetryState() } - // All categories disabled: clear the configuration. - s.cdpMonitor.Stop() - s.telemetrySession.Stop() - s.applyTelemetryMiddlewareState() return oapi.PutTelemetry200JSONResponse(oapi.TelemetryState{Config: disabledConfig(), Seq: int64(s.telemetrySession.Seq())}), nil } if wasActive { - // Replace config on the running session. s.telemetrySession.UpdateConfig(cfg) - s.applyTelemetryMiddlewareState() - return oapi.PutTelemetry200JSONResponse(s.buildTelemetryResponse()), nil + } else { + s.telemetrySession.Start(cuid2.Generate(), cfg) } - // Start a new telemetry session. - id := cuid2.Generate() - s.telemetrySession.Start(id, cfg) - - if err := s.cdpMonitor.Start(s.lifecycleCtx); err != nil { - // Roll back: clear the session so a retry can succeed. - s.telemetrySession.Stop() - s.applyTelemetryMiddlewareState() - logger.FromContext(ctx).Error("failed to start telemetry monitor", "err", err) + if err := s.applyTelemetryState(); err != nil { + if !wasActive { + // Roll back the freshly started session so a retry can succeed. + s.telemetrySession.Stop() + _ = s.applyTelemetryState() + } + logger.FromContext(ctx).Error("failed to apply telemetry state", "err", err) return oapi.PutTelemetry500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to start telemetry"}}, nil } - s.applyTelemetryMiddlewareState() + if wasActive { + return oapi.PutTelemetry200JSONResponse(s.buildTelemetryResponse()), nil + } return oapi.PutTelemetry201JSONResponse(s.buildTelemetryResponse()), nil } // PatchTelemetry handles PATCH /telemetry. // Partially updates the telemetry configuration. Returns 404 if not configured. -// Setting all five categories to enabled:false clears the configuration (200). -func (s *ApiService) PatchTelemetry(_ context.Context, req oapi.PatchTelemetryRequestObject) (oapi.PatchTelemetryResponseObject, error) { +// Setting every configurable category to enabled:false clears the configuration (200). +func (s *ApiService) PatchTelemetry(ctx context.Context, req oapi.PatchTelemetryRequestObject) (oapi.PatchTelemetryResponseObject, error) { s.monitorMu.Lock() defer s.monitorMu.Unlock() @@ -84,40 +79,52 @@ func (s *ApiService) PatchTelemetry(_ context.Context, req oapi.PatchTelemetryRe return oapi.PatchTelemetry404JSONResponse{NotFoundErrorJSONResponse: oapi.NotFoundErrorJSONResponse{Message: "telemetry is not configured"}}, nil } - if req.Body != nil && req.Body.Browser != nil { - // PATCH merges: only categories explicitly set in the request are updated; - // omitted categories retain their current enabled/disabled state. - current := s.telemetrySession.Config() - cfg, allDisabled := mergeTelemetryConfig(current, req.Body.Browser) - if allDisabled { - // All categories disabled: clear the configuration. - s.cdpMonitor.Stop() - s.telemetrySession.Stop() - s.applyTelemetryMiddlewareState() - return oapi.PatchTelemetry200JSONResponse(oapi.TelemetryState{Config: disabledConfig(), Seq: int64(s.telemetrySession.Seq())}), nil - } - s.telemetrySession.UpdateConfig(cfg) - s.applyTelemetryMiddlewareState() + if req.Body == nil || req.Body.Browser == nil { + return oapi.PatchTelemetry200JSONResponse(s.buildTelemetryResponse()), nil + } + + cfg, allDisabled := mergeTelemetryConfig(s.telemetrySession.Config(), req.Body.Browser) + if allDisabled { + s.telemetrySession.Stop() + _ = s.applyTelemetryState() + return oapi.PatchTelemetry200JSONResponse(oapi.TelemetryState{Config: disabledConfig(), Seq: int64(s.telemetrySession.Seq())}), nil } + s.telemetrySession.UpdateConfig(cfg) + if err := s.applyTelemetryState(); err != nil { + logger.FromContext(ctx).Error("failed to apply telemetry state", "err", err) + return oapi.PatchTelemetry500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to apply telemetry"}}, nil + } return oapi.PatchTelemetry200JSONResponse(s.buildTelemetryResponse()), nil } -// applyTelemetryMiddlewareState turns the api_call middleware on iff the -// session is active and the api category is enabled. Call after any config -// change. -func (s *ApiService) applyTelemetryMiddlewareState() { +// applyTelemetryState reconciles the CDP collector and the api_call (control) +// middleware with the active telemetry config. The CDP collector runs iff a CDP +// category is captured; the middleware emits iff the control category is. Call +// after any session change. +func (s *ApiService) applyTelemetryState() error { if !s.telemetrySession.Active() { + if s.cdpMonitor.IsRunning() { + s.cdpMonitor.Stop() + } DisableTelemetryMiddleware() - return + return nil } - for _, c := range s.telemetrySession.Config().Categories { - if c == events.Api { - EnableTelemetryMiddleware() - return - } + + cats := s.telemetrySession.Config().Categories + if containsCategory(cats, events.Control) { + EnableTelemetryMiddleware() + } else { + DisableTelemetryMiddleware() } - DisableTelemetryMiddleware() + + switch { + case events.HasCDPCategory(cats) && !s.cdpMonitor.IsRunning(): + return s.cdpMonitor.Start(s.lifecycleCtx) + case !events.HasCDPCategory(cats) && s.cdpMonitor.IsRunning(): + s.cdpMonitor.Stop() + } + return nil } // buildTelemetryResponse constructs a TelemetryState response from the current configuration. @@ -132,101 +139,96 @@ func (s *ApiService) buildTelemetryResponse() oapi.TelemetryState { return resp } -// telemetryConfigFromOAPI converts an *oapi.BrowserTelemetryConfig to a telemetry.TelemetryConfig. -// Returns the config, a boolean indicating whether all user-facing categories are explicitly -// disabled (stop signal), and any validation error. -func telemetryConfigFromOAPI(cfg *oapi.BrowserTelemetryConfig) (telemetry.TelemetryConfig, bool, error) { - if cfg == nil || cfg.Browser == nil { - // No config provided: capture all categories. - return telemetry.TelemetryConfig{}, false, nil - } +// categoryField pairs a category with its config field so the helpers can walk +// the configurable categories without enumerating them inline. +type categoryField struct { + category oapi.TelemetryEventCategory + config *oapi.BrowserTelemetryCategoryConfig +} - b := cfg.Browser - // A nil or omitted Enabled field defaults to true (capture the category). - isEnabled := func(c *oapi.BrowserTelemetryCategoryConfig) bool { - return c == nil || c.Enabled == nil || *c.Enabled +func categoryFields(b *oapi.BrowserTelemetryCategoriesConfig) []categoryField { + return []categoryField{ + {events.Console, b.Console}, + {events.Network, b.Network}, + {events.Page, b.Page}, + {events.Interaction, b.Interaction}, + {events.Control, b.Control}, + {events.Connection, b.Connection}, + {events.System, b.System}, + {events.Screenshot, b.Screenshot}, + {events.Captcha, b.Captcha}, } +} - consoleOn := isEnabled(b.Console) - networkOn := isEnabled(b.Network) - pageOn := isEnabled(b.Page) - interactionOn := isEnabled(b.Interaction) - apiOn := isEnabled(b.Api) - - allDisabled := !consoleOn && !networkOn && !pageOn && !interactionOn && !apiOn - if allDisabled { - return telemetry.TelemetryConfig{}, true, nil +func categorySetOf(cats []oapi.TelemetryEventCategory) map[oapi.TelemetryEventCategory]bool { + set := make(map[oapi.TelemetryEventCategory]bool, len(cats)) + for _, c := range cats { + set[c] = true } + return set +} - cats := make([]oapi.TelemetryEventCategory, 0, 6) - if consoleOn { - cats = append(cats, events.Console) - } - if networkOn { - cats = append(cats, events.Network) +func containsCategory(cats []oapi.TelemetryEventCategory, target oapi.TelemetryEventCategory) bool { + for _, c := range cats { + if c == target { + return true + } } - if pageOn { - cats = append(cats, events.Page) + return false +} + +// telemetryConfigFromOAPI converts an *oapi.BrowserTelemetryConfig to a telemetry.TelemetryConfig. +// An omitted category resolves to its default state (events.DefaultCategories). Returns the +// config, whether every configurable category ended up disabled (stop signal), and any error. +func telemetryConfigFromOAPI(cfg *oapi.BrowserTelemetryConfig) (telemetry.TelemetryConfig, bool, error) { + if cfg == nil || cfg.Browser == nil { + // No config provided: the default set is applied downstream. + return telemetry.TelemetryConfig{}, false, nil } - if interactionOn { - cats = append(cats, events.Interaction) + + defaultOn := categorySetOf(events.DefaultCategories) + cats := make([]oapi.TelemetryEventCategory, 0, len(events.UserCategories)) + for _, f := range categoryFields(cfg.Browser) { + on := defaultOn[f.category] + if f.config != nil && f.config.Enabled != nil { + on = *f.config.Enabled + } + if on { + cats = append(cats, f.category) + } } - if apiOn { - cats = append(cats, events.Api) + if len(cats) == 0 { + return telemetry.TelemetryConfig{}, true, nil } - // CategorySystem is always appended by TelemetrySession.Start/UpdateConfig; - // no need to include it here. return telemetry.TelemetryConfig{Categories: cats}, false, nil } // mergeTelemetryConfig applies patch overrides onto current, returning the merged config and -// whether all user-facing categories ended up disabled (stop signal). Only categories with an +// whether every configurable category ended up disabled (stop signal). Only categories with an // explicit Enabled field in patch are changed; omitted categories keep their current state. func mergeTelemetryConfig(current telemetry.TelemetryConfig, patch *oapi.BrowserTelemetryCategoriesConfig) (telemetry.TelemetryConfig, bool) { + userCat := categorySetOf(events.UserCategories) active := make(map[oapi.TelemetryEventCategory]struct{}, len(current.Categories)) for _, c := range current.Categories { - if c != events.System { // system is managed internally by TelemetrySession + if userCat[c] { // ignore the auto-managed Monitor category active[c] = struct{}{} } } - override := func(cat oapi.TelemetryEventCategory, field *oapi.BrowserTelemetryCategoryConfig) { - if field == nil || field.Enabled == nil { - return // not mentioned in patch — keep current state + for _, f := range categoryFields(patch) { + if f.config == nil || f.config.Enabled == nil { + continue // not mentioned in patch — keep current state } - if *field.Enabled { - active[cat] = struct{}{} + if *f.config.Enabled { + active[f.category] = struct{}{} } else { - delete(active, cat) + delete(active, f.category) } } - override(events.Console, patch.Console) - override(events.Network, patch.Network) - override(events.Page, patch.Page) - override(events.Interaction, patch.Interaction) - override(events.Api, patch.Api) - - // CategorySystem is managed internally by TelemetrySession; exclude from the - // user-facing allDisabled check. - userCats := []oapi.TelemetryEventCategory{ - events.Console, - events.Network, - events.Page, - events.Interaction, - events.Api, - } - allDisabled := true - for _, c := range userCats { - if _, ok := active[c]; ok { - allDisabled = false - break - } - } - if allDisabled { + if len(active) == 0 { return telemetry.TelemetryConfig{}, true } - cats := make([]oapi.TelemetryEventCategory, 0, len(active)) for c := range active { cats = append(cats, c) @@ -234,40 +236,45 @@ func mergeTelemetryConfig(current telemetry.TelemetryConfig, patch *oapi.Browser return telemetry.TelemetryConfig{Categories: cats}, false } -// disabledConfig returns a BrowserTelemetryConfig with all five user-facing categories explicitly disabled. +// disabledConfig returns a BrowserTelemetryConfig with every configurable category explicitly disabled. func disabledConfig() oapi.BrowserTelemetryConfig { + off := func() *oapi.BrowserTelemetryCategoryConfig { + return &oapi.BrowserTelemetryCategoryConfig{Enabled: lo.ToPtr(false)} + } return oapi.BrowserTelemetryConfig{ Browser: &oapi.BrowserTelemetryCategoriesConfig{ - Console: &oapi.BrowserTelemetryCategoryConfig{Enabled: lo.ToPtr(false)}, - Network: &oapi.BrowserTelemetryCategoryConfig{Enabled: lo.ToPtr(false)}, - Page: &oapi.BrowserTelemetryCategoryConfig{Enabled: lo.ToPtr(false)}, - Interaction: &oapi.BrowserTelemetryCategoryConfig{Enabled: lo.ToPtr(false)}, - Api: &oapi.BrowserTelemetryCategoryConfig{Enabled: lo.ToPtr(false)}, + Console: off(), + Network: off(), + Page: off(), + Interaction: off(), + Control: off(), + Connection: off(), + System: off(), + Screenshot: off(), + Captcha: off(), }, } } // telemetryConfigToOAPI converts a telemetry.TelemetryConfig to an oapi.BrowserTelemetryConfig -// suitable for API responses. +// suitable for API responses. The auto-managed Monitor category is not represented. func telemetryConfigToOAPI(cfg telemetry.TelemetryConfig) oapi.BrowserTelemetryConfig { - // Build a set of active categories for O(1) lookup. - active := make(map[oapi.TelemetryEventCategory]struct{}, len(cfg.Categories)) - for _, c := range cfg.Categories { - active[c] = struct{}{} - } - + active := categorySetOf(cfg.Categories) enabled := func(cat oapi.TelemetryEventCategory) *oapi.BrowserTelemetryCategoryConfig { - _, on := active[cat] + on := active[cat] return &oapi.BrowserTelemetryCategoryConfig{Enabled: &on} } - return oapi.BrowserTelemetryConfig{ Browser: &oapi.BrowserTelemetryCategoriesConfig{ Console: enabled(events.Console), Network: enabled(events.Network), Page: enabled(events.Page), Interaction: enabled(events.Interaction), - Api: enabled(events.Api), + Control: enabled(events.Control), + Connection: enabled(events.Connection), + System: enabled(events.System), + Screenshot: enabled(events.Screenshot), + Captcha: enabled(events.Captcha), }, } } diff --git a/server/cmd/api/api/telemetry_test.go b/server/cmd/api/api/telemetry_test.go index 21975da9..acb215b3 100644 --- a/server/cmd/api/api/telemetry_test.go +++ b/server/cmd/api/api/telemetry_test.go @@ -12,8 +12,28 @@ import ( "github.com/stretchr/testify/require" ) +// allCategoriesDisabled returns a config with every configurable category set +// to enabled:false (the clear signal). +func allCategoriesDisabled() *oapi.BrowserTelemetryCategoriesConfig { + off := func() *oapi.BrowserTelemetryCategoryConfig { + f := false + return &oapi.BrowserTelemetryCategoryConfig{Enabled: &f} + } + return &oapi.BrowserTelemetryCategoriesConfig{ + Console: off(), + Network: off(), + Page: off(), + Interaction: off(), + Control: off(), + Connection: off(), + System: off(), + Screenshot: off(), + Captcha: off(), + } +} + func TestTelemetryConfigFromOAPI(t *testing.T) { - t.Run("nil body returns defaults (all categories)", func(t *testing.T) { + t.Run("nil body returns defaults", func(t *testing.T) { cfg, allDisabled, err := telemetryConfigFromOAPI(nil) require.NoError(t, err) assert.False(t, allDisabled) @@ -27,18 +47,39 @@ func TestTelemetryConfigFromOAPI(t *testing.T) { assert.Empty(t, cfg.Categories) }) - t.Run("omitted enabled defaults to true", func(t *testing.T) { + t.Run("omitted enabled resolves to default state", func(t *testing.T) { cfg, allDisabled, err := telemetryConfigFromOAPI(&oapi.BrowserTelemetryConfig{ Browser: &oapi.BrowserTelemetryCategoriesConfig{ - Console: &oapi.BrowserTelemetryCategoryConfig{}, // Enabled is nil → defaults to true + Console: &oapi.BrowserTelemetryCategoryConfig{}, // Enabled nil → default state (on) }, }) require.NoError(t, err) assert.False(t, allDisabled) assert.Contains(t, cfg.Categories, events.Console) + // Screenshot is off by default and must stay off when unspecified. + assert.NotContains(t, cfg.Categories, events.Screenshot) + }) + + t.Run("screenshot is opt-in", func(t *testing.T) { + tr := true + cfg, _, err := telemetryConfigFromOAPI(&oapi.BrowserTelemetryConfig{ + Browser: &oapi.BrowserTelemetryCategoriesConfig{ + Screenshot: &oapi.BrowserTelemetryCategoryConfig{Enabled: &tr}, + }, + }) + require.NoError(t, err) + assert.Contains(t, cfg.Categories, events.Screenshot) + }) + + t.Run("all configurable categories false returns allDisabled=true", func(t *testing.T) { + _, allDisabled, err := telemetryConfigFromOAPI(&oapi.BrowserTelemetryConfig{ + Browser: allCategoriesDisabled(), + }) + require.NoError(t, err) + assert.True(t, allDisabled) }) - t.Run("all false returns allDisabled=true", func(t *testing.T) { + t.Run("disabling only the default-on categories does not clear", func(t *testing.T) { f := false _, allDisabled, err := telemetryConfigFromOAPI(&oapi.BrowserTelemetryConfig{ Browser: &oapi.BrowserTelemetryCategoriesConfig{ @@ -46,14 +87,14 @@ func TestTelemetryConfigFromOAPI(t *testing.T) { Network: &oapi.BrowserTelemetryCategoryConfig{Enabled: &f}, Page: &oapi.BrowserTelemetryCategoryConfig{Enabled: &f}, Interaction: &oapi.BrowserTelemetryCategoryConfig{Enabled: &f}, - Api: &oapi.BrowserTelemetryCategoryConfig{Enabled: &f}, }, }) require.NoError(t, err) - assert.True(t, allDisabled) + // control/connection/system/captcha remain at their default-on state. + assert.False(t, allDisabled) }) - t.Run("mixed enabled flags", func(t *testing.T) { + t.Run("mixed enabled flags resolve unspecified to default", func(t *testing.T) { tr, f := true, false cfg, allDisabled, err := telemetryConfigFromOAPI(&oapi.BrowserTelemetryConfig{ Browser: &oapi.BrowserTelemetryCategoriesConfig{ @@ -63,7 +104,11 @@ func TestTelemetryConfigFromOAPI(t *testing.T) { }) require.NoError(t, err) assert.False(t, allDisabled) - assert.Len(t, cfg.Categories, 4) // console + page + interaction + api (network=false, others default true) + // network off; screenshot default off; the other 7 default-on categories remain. + assert.Contains(t, cfg.Categories, events.Console) + assert.NotContains(t, cfg.Categories, events.Network) + assert.NotContains(t, cfg.Categories, events.Screenshot) + assert.Len(t, cfg.Categories, len(events.DefaultCategories)-1) }) } @@ -119,17 +164,8 @@ func TestPutTelemetry(t *testing.T) { _, err := svc.PutTelemetry(ctx, oapi.PutTelemetryRequestObject{}) require.NoError(t, err) - f := false resp, err := svc.PutTelemetry(ctx, oapi.PutTelemetryRequestObject{ - Body: &oapi.BrowserTelemetryConfig{ - Browser: &oapi.BrowserTelemetryCategoriesConfig{ - Console: &oapi.BrowserTelemetryCategoryConfig{Enabled: &f}, - Network: &oapi.BrowserTelemetryCategoryConfig{Enabled: &f}, - Page: &oapi.BrowserTelemetryCategoryConfig{Enabled: &f}, - Interaction: &oapi.BrowserTelemetryCategoryConfig{Enabled: &f}, - Api: &oapi.BrowserTelemetryCategoryConfig{Enabled: &f}, - }, - }, + Body: &oapi.BrowserTelemetryConfig{Browser: allCategoriesDisabled()}, }) require.NoError(t, err) r200, ok := resp.(oapi.PutTelemetry200JSONResponse) @@ -137,10 +173,8 @@ func TestPutTelemetry(t *testing.T) { require.NotNil(t, r200.Config.Browser) require.NotNil(t, r200.Config.Browser.Console) assert.False(t, *r200.Config.Browser.Console.Enabled) - assert.False(t, *r200.Config.Browser.Network.Enabled) - assert.False(t, *r200.Config.Browser.Page.Enabled) - assert.False(t, *r200.Config.Browser.Interaction.Enabled) - assert.False(t, *r200.Config.Browser.Api.Enabled) + assert.False(t, *r200.Config.Browser.Control.Enabled) + assert.False(t, *r200.Config.Browser.System.Enabled) assert.Nil(t, r200.AppliedAt, "applied_at must be omitted when telemetry is unconfigured") }) } @@ -156,33 +190,25 @@ func TestTelemetryHandlersDriveMiddlewareToggle(t *testing.T) { _, err := svc.PutTelemetry(ctx, oapi.PutTelemetryRequestObject{ Body: &oapi.BrowserTelemetryConfig{ Browser: &oapi.BrowserTelemetryCategoriesConfig{ - Api: &oapi.BrowserTelemetryCategoryConfig{Enabled: &tr}, + Control: &oapi.BrowserTelemetryCategoryConfig{Enabled: &tr}, }, }, }) require.NoError(t, err) - assert.True(t, TelemetryMiddlewareEnabled(), "PUT with api=true should enable middleware") + assert.True(t, TelemetryMiddlewareEnabled(), "PUT with control=true should enable middleware") _, err = svc.PatchTelemetry(ctx, oapi.PatchTelemetryRequestObject{ Body: &oapi.BrowserTelemetryConfig{ Browser: &oapi.BrowserTelemetryCategoriesConfig{ - Api: &oapi.BrowserTelemetryCategoryConfig{Enabled: &f}, + Control: &oapi.BrowserTelemetryCategoryConfig{Enabled: &f}, }, }, }) require.NoError(t, err) - assert.False(t, TelemetryMiddlewareEnabled(), "PATCH api=false should disable middleware (other categories still active)") + assert.False(t, TelemetryMiddlewareEnabled(), "PATCH control=false should disable middleware (other categories still active)") _, err = svc.PutTelemetry(ctx, oapi.PutTelemetryRequestObject{ - Body: &oapi.BrowserTelemetryConfig{ - Browser: &oapi.BrowserTelemetryCategoriesConfig{ - Console: &oapi.BrowserTelemetryCategoryConfig{Enabled: &f}, - Network: &oapi.BrowserTelemetryCategoryConfig{Enabled: &f}, - Page: &oapi.BrowserTelemetryCategoryConfig{Enabled: &f}, - Interaction: &oapi.BrowserTelemetryCategoryConfig{Enabled: &f}, - Api: &oapi.BrowserTelemetryCategoryConfig{Enabled: &f}, - }, - }, + Body: &oapi.BrowserTelemetryConfig{Browser: allCategoriesDisabled()}, }) require.NoError(t, err) assert.False(t, TelemetryMiddlewareEnabled(), "all-disabled PUT should leave middleware off") @@ -266,17 +292,8 @@ func TestPatchTelemetry(t *testing.T) { _, err := svc.PutTelemetry(ctx, oapi.PutTelemetryRequestObject{}) require.NoError(t, err) - f := false resp, err := svc.PatchTelemetry(ctx, oapi.PatchTelemetryRequestObject{ - Body: &oapi.BrowserTelemetryConfig{ - Browser: &oapi.BrowserTelemetryCategoriesConfig{ - Console: &oapi.BrowserTelemetryCategoryConfig{Enabled: &f}, - Network: &oapi.BrowserTelemetryCategoryConfig{Enabled: &f}, - Page: &oapi.BrowserTelemetryCategoryConfig{Enabled: &f}, - Interaction: &oapi.BrowserTelemetryCategoryConfig{Enabled: &f}, - Api: &oapi.BrowserTelemetryCategoryConfig{Enabled: &f}, - }, - }, + Body: &oapi.BrowserTelemetryConfig{Browser: allCategoriesDisabled()}, }) require.NoError(t, err) r200, ok := resp.(oapi.PatchTelemetry200JSONResponse) @@ -284,10 +301,8 @@ func TestPatchTelemetry(t *testing.T) { require.NotNil(t, r200.Config.Browser) require.NotNil(t, r200.Config.Browser.Console) assert.False(t, *r200.Config.Browser.Console.Enabled) - assert.False(t, *r200.Config.Browser.Network.Enabled) - assert.False(t, *r200.Config.Browser.Page.Enabled) - assert.False(t, *r200.Config.Browser.Interaction.Enabled) - assert.False(t, *r200.Config.Browser.Api.Enabled) + assert.False(t, *r200.Config.Browser.Control.Enabled) + assert.False(t, *r200.Config.Browser.System.Enabled) }) t.Run("put returns 201 after patch clears configuration", func(t *testing.T) { @@ -295,17 +310,8 @@ func TestPatchTelemetry(t *testing.T) { _, err := svc.PutTelemetry(ctx, oapi.PutTelemetryRequestObject{}) require.NoError(t, err) - f := false _, err = svc.PatchTelemetry(ctx, oapi.PatchTelemetryRequestObject{ - Body: &oapi.BrowserTelemetryConfig{ - Browser: &oapi.BrowserTelemetryCategoriesConfig{ - Console: &oapi.BrowserTelemetryCategoryConfig{Enabled: &f}, - Network: &oapi.BrowserTelemetryCategoryConfig{Enabled: &f}, - Page: &oapi.BrowserTelemetryCategoryConfig{Enabled: &f}, - Interaction: &oapi.BrowserTelemetryCategoryConfig{Enabled: &f}, - Api: &oapi.BrowserTelemetryCategoryConfig{Enabled: &f}, - }, - }, + Body: &oapi.BrowserTelemetryConfig{Browser: allCategoriesDisabled()}, }) require.NoError(t, err) diff --git a/server/go.mod b/server/go.mod index d7ae2a36..fd8296e9 100644 --- a/server/go.mod +++ b/server/go.mod @@ -29,6 +29,7 @@ require ( golang.org/x/sync v0.17.0 golang.org/x/sys v0.39.0 golang.org/x/term v0.37.0 + gopkg.in/yaml.v3 v3.0.1 ) require ( @@ -106,7 +107,6 @@ require ( golang.org/x/tools v0.37.0 // indirect google.golang.org/protobuf v1.36.10 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect gorm.io/gorm v1.25.7 // indirect modernc.org/libc v1.22.5 // indirect modernc.org/mathutil v1.5.0 // indirect diff --git a/server/lib/cdpmonitor/README.md b/server/lib/cdpmonitor/README.md index 4c972e1d..e03142b3 100644 --- a/server/lib/cdpmonitor/README.md +++ b/server/lib/cdpmonitor/README.md @@ -115,7 +115,7 @@ Every event arrives as an `Envelope`: | `seq` | uint64 | Process-monotonic sequence number; does not reset across telemetry config changes. | | `event.ts` | int64 | Wall-clock time the monitor emitted the event, as **Unix microseconds** (µs since epoch). | | `event.type` | string | See [Event taxonomy](#event-taxonomy). | -| `event.category` | string | One of: `console`, `network`, `page`, `interaction`, `system`. | +| `event.category` | string | Emitted by this monitor: `console`, `network`, `page`, `interaction`, `screenshot`, `monitor` (collector health). | | `event.truncated` | bool | `true` if `data` was nulled to fit the 1 MB pipeline limit. | | `event.source.metadata.telemetry_session_id` | string | Pipeline-assigned ID for the telemetry session, stamped by the telemetry layer. | diff --git a/server/lib/cdpmonitor/monitor.go b/server/lib/cdpmonitor/monitor.go index dbc15dbb..5a1b764f 100644 --- a/server/lib/cdpmonitor/monitor.go +++ b/server/lib/cdpmonitor/monitor.go @@ -398,7 +398,7 @@ func (m *Monitor) initSession(ctx context.Context) { m.publish(events.Event{ Ts: time.Now().UnixMicro(), Type: EventMonitorInitFailed, - Category: events.System, + Category: events.Monitor, Source: oapi.BrowserEventSource{Kind: oapi.LocalProcess}, Data: initFailedData, }) @@ -499,7 +499,7 @@ func (m *Monitor) handleUpstreamRestart(ctx context.Context, newURL string) { m.publish(events.Event{ Ts: time.Now().UnixMicro(), Type: EventMonitorDisconnected, - Category: events.System, + Category: events.Monitor, Source: oapi.BrowserEventSource{Kind: oapi.LocalProcess}, Data: disconnectedData, }) @@ -533,7 +533,7 @@ func (m *Monitor) handleUpstreamRestart(ctx context.Context, newURL string) { m.publish(events.Event{ Ts: time.Now().UnixMicro(), Type: EventMonitorReconnectFailed, - Category: events.System, + Category: events.Monitor, Source: oapi.BrowserEventSource{Kind: oapi.LocalProcess}, Data: reconnectFailedData, }) @@ -558,7 +558,7 @@ func (m *Monitor) handleUpstreamRestart(ctx context.Context, newURL string) { m.publish(events.Event{ Ts: time.Now().UnixMicro(), Type: EventMonitorReconnected, - Category: events.System, + Category: events.Monitor, Source: oapi.BrowserEventSource{Kind: oapi.LocalProcess}, Data: reconnectedData, }) diff --git a/server/lib/cdpmonitor/monitor_test.go b/server/lib/cdpmonitor/monitor_test.go index 33c277fa..daa5d6ff 100644 --- a/server/lib/cdpmonitor/monitor_test.go +++ b/server/lib/cdpmonitor/monitor_test.go @@ -88,7 +88,7 @@ func TestScreenshot(t *testing.T) { require.Eventually(t, func() bool { return captureCount.Load() == 1 }, 2*time.Second, 20*time.Millisecond) ev := ec.waitFor(t, "monitor_screenshot", 2*time.Second) - assert.Equal(t, events.System, ev.Category) + assert.Equal(t, events.Screenshot, ev.Category) assert.Equal(t, oapi.LocalProcess, ev.Source.Kind) require.NotNil(t, ev.Source.Event) assert.Equal(t, "Page.loadEventFired", *ev.Source.Event) diff --git a/server/lib/cdpmonitor/screenshot.go b/server/lib/cdpmonitor/screenshot.go index 72b7b007..71f795b0 100644 --- a/server/lib/cdpmonitor/screenshot.go +++ b/server/lib/cdpmonitor/screenshot.go @@ -85,7 +85,7 @@ func (m *Monitor) captureScreenshot(parentCtx context.Context, sourceEvent strin m.publish(events.Event{ Ts: time.Now().UnixMicro(), Type: EventScreenshot, - Category: events.System, + Category: events.Screenshot, Source: src, Data: data, }) diff --git a/server/lib/devtoolsproxy/proxy.go b/server/lib/devtoolsproxy/proxy.go index 8fc4d643..aded45a8 100644 --- a/server/lib/devtoolsproxy/proxy.go +++ b/server/lib/devtoolsproxy/proxy.go @@ -471,7 +471,7 @@ func publishCdpConnect(publish EventPublisher) { publish(events.Event{ Ts: time.Now().UnixMicro(), Type: "cdp_connect", - Category: events.System, + Category: events.Connection, Source: oapi.BrowserEventSource{Kind: oapi.KernelApi}, }) } @@ -488,7 +488,7 @@ func publishCdpDisconnect(publish EventPublisher, reason oapi.BrowserCdpDisconne publish(events.Event{ Ts: disconnectedAt.UnixMicro(), Type: "cdp_disconnect", - Category: events.System, + Category: events.Connection, Source: oapi.BrowserEventSource{Kind: oapi.KernelApi}, Data: data, }) diff --git a/server/lib/devtoolsproxy/proxy_test.go b/server/lib/devtoolsproxy/proxy_test.go index d5e4dcc0..8d2d7130 100644 --- a/server/lib/devtoolsproxy/proxy_test.go +++ b/server/lib/devtoolsproxy/proxy_test.go @@ -484,8 +484,8 @@ func TestWebSocketProxyHandler_EmitsConnectAndDisconnect(t *testing.T) { if got := captured[0].Type; got != "cdp_connect" { t.Fatalf("first event type = %q, want cdp_connect", got) } - if got := captured[0].Category; got != events.System { - t.Fatalf("first event category = %q, want system", got) + if got := captured[0].Category; got != events.Connection { + t.Fatalf("first event category = %q, want connection", got) } if got := captured[1].Type; got != "cdp_disconnect" { diff --git a/server/lib/events/category_gen.go b/server/lib/events/category_gen.go new file mode 100644 index 00000000..06d641fb --- /dev/null +++ b/server/lib/events/category_gen.go @@ -0,0 +1,45 @@ +// Code generated by scripts/categorygen; DO NOT EDIT. + +package events + +import oapi "github.com/kernel/kernel-images/server/lib/oapi" + +var categoryByType = map[string]oapi.TelemetryEventCategory{ + "api_call": oapi.TelemetryEventCategory("control"), + "captcha_solve_result": oapi.TelemetryEventCategory("captcha"), + "cdp_connect": oapi.TelemetryEventCategory("connection"), + "cdp_disconnect": oapi.TelemetryEventCategory("connection"), + "console_error": oapi.TelemetryEventCategory("console"), + "console_log": oapi.TelemetryEventCategory("console"), + "interaction_click": oapi.TelemetryEventCategory("interaction"), + "interaction_key": oapi.TelemetryEventCategory("interaction"), + "interaction_scroll_settled": oapi.TelemetryEventCategory("interaction"), + "live_view_connect": oapi.TelemetryEventCategory("connection"), + "live_view_disconnect": oapi.TelemetryEventCategory("connection"), + "monitor_disconnected": oapi.TelemetryEventCategory("monitor"), + "monitor_init_failed": oapi.TelemetryEventCategory("monitor"), + "monitor_reconnect_failed": oapi.TelemetryEventCategory("monitor"), + "monitor_reconnected": oapi.TelemetryEventCategory("monitor"), + "monitor_screenshot": oapi.TelemetryEventCategory("screenshot"), + "network_idle": oapi.TelemetryEventCategory("network"), + "network_loading_failed": oapi.TelemetryEventCategory("network"), + "network_request": oapi.TelemetryEventCategory("network"), + "network_response": oapi.TelemetryEventCategory("network"), + "page_dom_content_loaded": oapi.TelemetryEventCategory("page"), + "page_layout_settled": oapi.TelemetryEventCategory("page"), + "page_layout_shift": oapi.TelemetryEventCategory("page"), + "page_lcp": oapi.TelemetryEventCategory("page"), + "page_load": oapi.TelemetryEventCategory("page"), + "page_navigation": oapi.TelemetryEventCategory("page"), + "page_navigation_settled": oapi.TelemetryEventCategory("page"), + "page_tab_opened": oapi.TelemetryEventCategory("page"), + "service_crashed": oapi.TelemetryEventCategory("system"), + "system_oom_kill": oapi.TelemetryEventCategory("system"), +} + +// CategoryForType returns the authoritative category for a known event +// type. ok is false for an unknown type. +func CategoryForType(eventType string) (oapi.TelemetryEventCategory, bool) { + c, ok := categoryByType[eventType] + return c, ok +} diff --git a/server/lib/events/event.go b/server/lib/events/event.go index d331084a..cdffc74c 100644 --- a/server/lib/events/event.go +++ b/server/lib/events/event.go @@ -1,5 +1,7 @@ package events +//go:generate go run github.com/kernel/kernel-images/server/scripts/categorygen -openapi ../../openapi.yaml -out category_gen.go + import ( "encoding/json" "log/slog" @@ -15,19 +17,63 @@ const ( Network = oapi.TelemetryEventCategory("network") Page = oapi.TelemetryEventCategory("page") Interaction = oapi.TelemetryEventCategory("interaction") - Api = oapi.TelemetryEventCategory("api") + Control = oapi.TelemetryEventCategory("control") + Connection = oapi.TelemetryEventCategory("connection") System = oapi.TelemetryEventCategory("system") + Screenshot = oapi.TelemetryEventCategory("screenshot") + Captcha = oapi.TelemetryEventCategory("captcha") + Monitor = oapi.TelemetryEventCategory("monitor") ) -// AllCategories is the canonical list of all configurable event categories. -// System events are always captured regardless of telemetry config. -var AllCategories = []oapi.TelemetryEventCategory{ +// UserCategories are the categories a caller can configure via the telemetry +// config. Monitor is excluded: it is CDP-collector health metadata that flows +// automatically whenever a CDP category is captured, not a configurable knob. +var UserCategories = []oapi.TelemetryEventCategory{ Console, Network, Page, Interaction, - Api, + Control, + Connection, System, + Screenshot, + Captcha, +} + +// DefaultCategories is captured when the caller enables telemetry without +// per-category settings: every configurable category except Screenshot, which +// is high-volume base64 image data and therefore opt-in. +var DefaultCategories = []oapi.TelemetryEventCategory{ + Console, + Network, + Page, + Interaction, + Control, + Connection, + System, + Captcha, +} + +// cdpCategories are produced by the CDP collector. Enabling any of them starts +// the collector, and Monitor (collector health) rides along while it runs. +var cdpCategories = map[oapi.TelemetryEventCategory]struct{}{ + Console: {}, + Network: {}, + Page: {}, + Interaction: {}, + Screenshot: {}, +} + +// HasCDPCategory reports whether the set contains any CDP-collector category. +// It is the single predicate gating both the collector start and Monitor +// inclusion, so the two can never diverge. +func HasCDPCategory(cats []oapi.TelemetryEventCategory) bool { + for _, c := range cats { + if _, ok := cdpCategories[c]; ok { + return true + } + } + return false } // Event is the portable event schema. It contains only producer-emitted content; diff --git a/server/lib/oapi/oapi.go b/server/lib/oapi/oapi.go index 1efa4827..e5a1fa64 100644 --- a/server/lib/oapi/oapi.go +++ b/server/lib/oapi/oapi.go @@ -28,13 +28,13 @@ import ( // Defines values for BrowserApiCallEventCategory. const ( - BrowserApiCallEventCategoryApi BrowserApiCallEventCategory = "api" + BrowserApiCallEventCategoryControl BrowserApiCallEventCategory = "control" ) // Valid indicates whether the value is a known member of the BrowserApiCallEventCategory enum. func (e BrowserApiCallEventCategory) Valid() bool { switch e { - case BrowserApiCallEventCategoryApi: + case BrowserApiCallEventCategoryControl: return true default: return false @@ -56,6 +56,21 @@ func (e BrowserApiCallEventType) Valid() bool { } } +// Defines values for BrowserCaptchaSolveResultEventCategory. +const ( + BrowserCaptchaSolveResultEventCategoryCaptcha BrowserCaptchaSolveResultEventCategory = "captcha" +) + +// Valid indicates whether the value is a known member of the BrowserCaptchaSolveResultEventCategory enum. +func (e BrowserCaptchaSolveResultEventCategory) Valid() bool { + switch e { + case BrowserCaptchaSolveResultEventCategoryCaptcha: + return true + default: + return false + } +} + // Defines values for BrowserCaptchaSolveResultEventType. const ( CaptchaSolveResult BrowserCaptchaSolveResultEventType = "captcha_solve_result" @@ -127,13 +142,13 @@ func (e BrowserCaptchaSolveResultEventDataStatus) Valid() bool { // Defines values for BrowserCdpConnectEventCategory. const ( - BrowserCdpConnectEventCategorySystem BrowserCdpConnectEventCategory = "system" + BrowserCdpConnectEventCategoryConnection BrowserCdpConnectEventCategory = "connection" ) // Valid indicates whether the value is a known member of the BrowserCdpConnectEventCategory enum. func (e BrowserCdpConnectEventCategory) Valid() bool { switch e { - case BrowserCdpConnectEventCategorySystem: + case BrowserCdpConnectEventCategoryConnection: return true default: return false @@ -157,13 +172,13 @@ func (e BrowserCdpConnectEventType) Valid() bool { // Defines values for BrowserCdpDisconnectEventCategory. const ( - BrowserCdpDisconnectEventCategorySystem BrowserCdpDisconnectEventCategory = "system" + BrowserCdpDisconnectEventCategoryConnection BrowserCdpDisconnectEventCategory = "connection" ) // Valid indicates whether the value is a known member of the BrowserCdpDisconnectEventCategory enum. func (e BrowserCdpDisconnectEventCategory) Valid() bool { switch e { - case BrowserCdpDisconnectEventCategorySystem: + case BrowserCdpDisconnectEventCategoryConnection: return true default: return false @@ -355,13 +370,13 @@ func (e BrowserInteractionKeyEventType) Valid() bool { // Defines values for BrowserInteractionScrollSettledEventCategory. const ( - BrowserInteractionScrollSettledEventCategoryInteraction BrowserInteractionScrollSettledEventCategory = "interaction" + Interaction BrowserInteractionScrollSettledEventCategory = "interaction" ) // Valid indicates whether the value is a known member of the BrowserInteractionScrollSettledEventCategory enum. func (e BrowserInteractionScrollSettledEventCategory) Valid() bool { switch e { - case BrowserInteractionScrollSettledEventCategoryInteraction: + case Interaction: return true default: return false @@ -383,6 +398,21 @@ func (e BrowserInteractionScrollSettledEventType) Valid() bool { } } +// Defines values for BrowserLiveViewConnectEventCategory. +const ( + BrowserLiveViewConnectEventCategoryConnection BrowserLiveViewConnectEventCategory = "connection" +) + +// Valid indicates whether the value is a known member of the BrowserLiveViewConnectEventCategory enum. +func (e BrowserLiveViewConnectEventCategory) Valid() bool { + switch e { + case BrowserLiveViewConnectEventCategoryConnection: + return true + default: + return false + } +} + // Defines values for BrowserLiveViewConnectEventType. const ( LiveViewConnect BrowserLiveViewConnectEventType = "live_view_connect" @@ -398,6 +428,21 @@ func (e BrowserLiveViewConnectEventType) Valid() bool { } } +// Defines values for BrowserLiveViewDisconnectEventCategory. +const ( + BrowserLiveViewDisconnectEventCategoryConnection BrowserLiveViewDisconnectEventCategory = "connection" +) + +// Valid indicates whether the value is a known member of the BrowserLiveViewDisconnectEventCategory enum. +func (e BrowserLiveViewDisconnectEventCategory) Valid() bool { + switch e { + case BrowserLiveViewDisconnectEventCategoryConnection: + return true + default: + return false + } +} + // Defines values for BrowserLiveViewDisconnectEventType. const ( LiveViewDisconnect BrowserLiveViewDisconnectEventType = "live_view_disconnect" @@ -415,13 +460,13 @@ func (e BrowserLiveViewDisconnectEventType) Valid() bool { // Defines values for BrowserMonitorDisconnectedEventCategory. const ( - BrowserMonitorDisconnectedEventCategorySystem BrowserMonitorDisconnectedEventCategory = "system" + BrowserMonitorDisconnectedEventCategoryMonitor BrowserMonitorDisconnectedEventCategory = "monitor" ) // Valid indicates whether the value is a known member of the BrowserMonitorDisconnectedEventCategory enum. func (e BrowserMonitorDisconnectedEventCategory) Valid() bool { switch e { - case BrowserMonitorDisconnectedEventCategorySystem: + case BrowserMonitorDisconnectedEventCategoryMonitor: return true default: return false @@ -460,13 +505,13 @@ func (e BrowserMonitorDisconnectedEventDataReason) Valid() bool { // Defines values for BrowserMonitorInitFailedEventCategory. const ( - BrowserMonitorInitFailedEventCategorySystem BrowserMonitorInitFailedEventCategory = "system" + BrowserMonitorInitFailedEventCategoryMonitor BrowserMonitorInitFailedEventCategory = "monitor" ) // Valid indicates whether the value is a known member of the BrowserMonitorInitFailedEventCategory enum. func (e BrowserMonitorInitFailedEventCategory) Valid() bool { switch e { - case BrowserMonitorInitFailedEventCategorySystem: + case BrowserMonitorInitFailedEventCategoryMonitor: return true default: return false @@ -490,13 +535,13 @@ func (e BrowserMonitorInitFailedEventType) Valid() bool { // Defines values for BrowserMonitorReconnectFailedEventCategory. const ( - BrowserMonitorReconnectFailedEventCategorySystem BrowserMonitorReconnectFailedEventCategory = "system" + BrowserMonitorReconnectFailedEventCategoryMonitor BrowserMonitorReconnectFailedEventCategory = "monitor" ) // Valid indicates whether the value is a known member of the BrowserMonitorReconnectFailedEventCategory enum. func (e BrowserMonitorReconnectFailedEventCategory) Valid() bool { switch e { - case BrowserMonitorReconnectFailedEventCategorySystem: + case BrowserMonitorReconnectFailedEventCategoryMonitor: return true default: return false @@ -535,13 +580,13 @@ func (e BrowserMonitorReconnectFailedEventDataReason) Valid() bool { // Defines values for BrowserMonitorReconnectedEventCategory. const ( - BrowserMonitorReconnectedEventCategorySystem BrowserMonitorReconnectedEventCategory = "system" + BrowserMonitorReconnectedEventCategoryMonitor BrowserMonitorReconnectedEventCategory = "monitor" ) // Valid indicates whether the value is a known member of the BrowserMonitorReconnectedEventCategory enum. func (e BrowserMonitorReconnectedEventCategory) Valid() bool { switch e { - case BrowserMonitorReconnectedEventCategorySystem: + case BrowserMonitorReconnectedEventCategoryMonitor: return true default: return false @@ -565,13 +610,13 @@ func (e BrowserMonitorReconnectedEventType) Valid() bool { // Defines values for BrowserMonitorScreenshotEventCategory. const ( - BrowserMonitorScreenshotEventCategorySystem BrowserMonitorScreenshotEventCategory = "system" + Screenshot BrowserMonitorScreenshotEventCategory = "screenshot" ) // Valid indicates whether the value is a known member of the BrowserMonitorScreenshotEventCategory enum. func (e BrowserMonitorScreenshotEventCategory) Valid() bool { switch e { - case BrowserMonitorScreenshotEventCategorySystem: + case Screenshot: return true default: return false @@ -1006,13 +1051,13 @@ func (e BrowserServiceCrashedEventDataPhase) Valid() bool { // Defines values for BrowserSystemOomKillEventCategory. const ( - System BrowserSystemOomKillEventCategory = "system" + BrowserSystemOomKillEventCategorySystem BrowserSystemOomKillEventCategory = "system" ) // Valid indicates whether the value is a known member of the BrowserSystemOomKillEventCategory enum. func (e BrowserSystemOomKillEventCategory) Valid() bool { switch e { - case System: + case BrowserSystemOomKillEventCategorySystem: return true default: return false @@ -1366,27 +1411,39 @@ func (e ProcessStreamEventStream) Valid() bool { // Defines values for PublishEventRequestCategory. const ( - PublishEventRequestCategoryApi PublishEventRequestCategory = "api" + PublishEventRequestCategoryCaptcha PublishEventRequestCategory = "captcha" + PublishEventRequestCategoryConnection PublishEventRequestCategory = "connection" PublishEventRequestCategoryConsole PublishEventRequestCategory = "console" + PublishEventRequestCategoryControl PublishEventRequestCategory = "control" PublishEventRequestCategoryInteraction PublishEventRequestCategory = "interaction" + PublishEventRequestCategoryMonitor PublishEventRequestCategory = "monitor" PublishEventRequestCategoryNetwork PublishEventRequestCategory = "network" PublishEventRequestCategoryPage PublishEventRequestCategory = "page" + PublishEventRequestCategoryScreenshot PublishEventRequestCategory = "screenshot" PublishEventRequestCategorySystem PublishEventRequestCategory = "system" ) // Valid indicates whether the value is a known member of the PublishEventRequestCategory enum. func (e PublishEventRequestCategory) Valid() bool { switch e { - case PublishEventRequestCategoryApi: + case PublishEventRequestCategoryCaptcha: + return true + case PublishEventRequestCategoryConnection: return true case PublishEventRequestCategoryConsole: return true + case PublishEventRequestCategoryControl: + return true case PublishEventRequestCategoryInteraction: return true + case PublishEventRequestCategoryMonitor: + return true case PublishEventRequestCategoryNetwork: return true case PublishEventRequestCategoryPage: return true + case PublishEventRequestCategoryScreenshot: + return true case PublishEventRequestCategorySystem: return true default: @@ -1396,27 +1453,39 @@ func (e PublishEventRequestCategory) Valid() bool { // Defines values for TelemetryEventCategory. const ( - TelemetryEventCategoryApi TelemetryEventCategory = "api" + TelemetryEventCategoryCaptcha TelemetryEventCategory = "captcha" + TelemetryEventCategoryConnection TelemetryEventCategory = "connection" TelemetryEventCategoryConsole TelemetryEventCategory = "console" + TelemetryEventCategoryControl TelemetryEventCategory = "control" TelemetryEventCategoryInteraction TelemetryEventCategory = "interaction" + TelemetryEventCategoryMonitor TelemetryEventCategory = "monitor" TelemetryEventCategoryNetwork TelemetryEventCategory = "network" TelemetryEventCategoryPage TelemetryEventCategory = "page" + TelemetryEventCategoryScreenshot TelemetryEventCategory = "screenshot" TelemetryEventCategorySystem TelemetryEventCategory = "system" ) // Valid indicates whether the value is a known member of the TelemetryEventCategory enum. func (e TelemetryEventCategory) Valid() bool { switch e { - case TelemetryEventCategoryApi: + case TelemetryEventCategoryCaptcha: + return true + case TelemetryEventCategoryConnection: return true case TelemetryEventCategoryConsole: return true + case TelemetryEventCategoryControl: + return true case TelemetryEventCategoryInteraction: return true + case TelemetryEventCategoryMonitor: + return true case TelemetryEventCategoryNetwork: return true case TelemetryEventCategoryPage: return true + case TelemetryEventCategoryScreenshot: + return true case TelemetryEventCategorySystem: return true default: @@ -1540,6 +1609,8 @@ type BrowserCallStack struct { // BrowserCaptchaSolveResultEvent A captcha solve attempt reached a terminal outcome. type BrowserCaptchaSolveResultEvent struct { + Category BrowserCaptchaSolveResultEventCategory `json:"category"` + // Data Per-attempt payload for `captcha_solve_result` events. Data *BrowserCaptchaSolveResultEventData `json:"data,omitempty"` @@ -1554,6 +1625,9 @@ type BrowserCaptchaSolveResultEvent struct { Type BrowserCaptchaSolveResultEventType `json:"type"` } +// BrowserCaptchaSolveResultEventCategory defines model for BrowserCaptchaSolveResultEvent.Category. +type BrowserCaptchaSolveResultEventCategory string + // BrowserCaptchaSolveResultEventType defines model for BrowserCaptchaSolveResultEvent.Type. type BrowserCaptchaSolveResultEventType string @@ -1993,6 +2067,8 @@ type BrowserInteractionScrollSettledEventData struct { // BrowserLiveViewConnectEvent A live view client connected to the headful browser's WebRTC server (Neko). Headful only; not emitted for headless images. type BrowserLiveViewConnectEvent struct { + Category BrowserLiveViewConnectEventCategory `json:"category"` + // Data Per-session payload for `live_view_connect` events. Data *BrowserLiveViewConnectEventData `json:"data,omitempty"` @@ -2007,6 +2083,9 @@ type BrowserLiveViewConnectEvent struct { Type BrowserLiveViewConnectEventType `json:"type"` } +// BrowserLiveViewConnectEventCategory defines model for BrowserLiveViewConnectEvent.Category. +type BrowserLiveViewConnectEventCategory string + // BrowserLiveViewConnectEventType defines model for BrowserLiveViewConnectEvent.Type. type BrowserLiveViewConnectEventType string @@ -2018,6 +2097,8 @@ type BrowserLiveViewConnectEventData struct { // BrowserLiveViewDisconnectEvent A live view client disconnected from the headful browser's WebRTC server (Neko). Pair with `live_view_connect` by `session_id`. type BrowserLiveViewDisconnectEvent struct { + Category BrowserLiveViewDisconnectEventCategory `json:"category"` + // Data Per-session payload for `live_view_disconnect` events. Data *BrowserLiveViewDisconnectEventData `json:"data,omitempty"` @@ -2032,6 +2113,9 @@ type BrowserLiveViewDisconnectEvent struct { Type BrowserLiveViewDisconnectEventType `json:"type"` } +// BrowserLiveViewDisconnectEventCategory defines model for BrowserLiveViewDisconnectEvent.Category. +type BrowserLiveViewDisconnectEventCategory string + // BrowserLiveViewDisconnectEventType defines model for BrowserLiveViewDisconnectEvent.Type. type BrowserLiveViewDisconnectEventType string @@ -2897,12 +2981,18 @@ type BrowserTargetType string // BrowserTelemetryCategoriesConfig Per-category telemetry capture settings for browser events. type BrowserTelemetryCategoriesConfig struct { - // Api Kernel-image-layer activity that the customer drives: inbound API calls to the kernel-images-api server and extension-mediated captcha solve attempts. CDP proxy and live view session lifecycle events are infrastructure and live in the always-on `system` category. - Api *BrowserTelemetryCategoryConfig `json:"api,omitempty"` + // Captcha Captcha solve attempt outcomes. + Captcha *BrowserTelemetryCategoryConfig `json:"captcha,omitempty"` + + // Connection Client attach/detach lifecycle for the CDP proxy and live view. + Connection *BrowserTelemetryCategoryConfig `json:"connection,omitempty"` // Console Console output (log, warn, error) and uncaught exceptions. Console *BrowserTelemetryCategoryConfig `json:"console,omitempty"` + // Control Agent-driven actions against the browser, such as inbound calls to the kernel-images-api server. + Control *BrowserTelemetryCategoryConfig `json:"control,omitempty"` + // Interaction User interaction events (clicks, keydowns, scroll). Interaction *BrowserTelemetryCategoryConfig `json:"interaction,omitempty"` @@ -2911,15 +3001,21 @@ type BrowserTelemetryCategoriesConfig struct { // Page Page lifecycle events (navigation, load, layout shifts, LCP). Page *BrowserTelemetryCategoryConfig `json:"page,omitempty"` + + // Screenshot Periodic base64-encoded viewport screenshots. High volume; off by default and opt-in. + Screenshot *BrowserTelemetryCategoryConfig `json:"screenshot,omitempty"` + + // System Browser VM health, such as out-of-memory kills and managed-service crashes. + System *BrowserTelemetryCategoryConfig `json:"system,omitempty"` } // BrowserTelemetryCategoryConfig Configuration for a single telemetry category. type BrowserTelemetryCategoryConfig struct { - // Enabled Whether this category is captured. In PUT requests, omitting this field defaults to true (category enabled). In PATCH requests, omitting this field (or sending an empty object `{}`) is a no-op; the category retains its current state. To enable or disable a category via PATCH, you must send an explicit `true` or `false`. + // Enabled Whether this category is captured. In PUT requests, omitting this field leaves the category at its default state (every category on except `screenshot`). In PATCH requests, omitting this field (or sending an empty object `{}`) is a no-op; the category retains its current state. To enable or disable a category via PATCH, you must send an explicit `true` or `false`. Enabled *bool `json:"enabled,omitempty"` } -// BrowserTelemetryConfig Telemetry configuration for a browser. Per-category capture settings. Omit a category or set enabled: true to capture it. Set enabled: false to exclude it. Omit the browser key entirely to capture all categories. Set all five categories to enabled: false to clear the telemetry configuration. +// BrowserTelemetryConfig Telemetry configuration for a browser. Per-category capture settings. Omit the browser key (or send an empty object) to capture the default set: every category except `screenshot`, which is heavy and opt-in. Within `browser`, omit a category to leave it at its default state, or set enabled true/false to override. Set every configurable category to enabled: false to clear the telemetry configuration. The `monitor` category (CDP collector health) is not configurable here; it flows automatically whenever a CDP category is captured. type BrowserTelemetryConfig struct { // Browser Per-category telemetry capture settings for browser events. Browser *BrowserTelemetryCategoriesConfig `json:"browser,omitempty"` @@ -3381,7 +3477,7 @@ type ProcessStreamEventStream string // PublishEventRequest Request body for publishing an event into the telemetry stream. type PublishEventRequest struct { - // Category Event category. + // Category Event category. Optional and advisory: for a known event `type` the server assigns the category authoritatively and ignores this field. It is only used for unknown custom types, where it is required. Category *PublishEventRequestCategory `json:"category,omitempty"` // Data Telemetry event payload. @@ -3394,7 +3490,7 @@ type PublishEventRequest struct { Type string `json:"type"` } -// PublishEventRequestCategory Event category. +// PublishEventRequestCategory Event category. Optional and advisory: for a known event `type` the server assigns the category authoritatively and ignores this field. It is only used for unknown custom types, where it is required. type PublishEventRequestCategory string // RecorderInfo defines model for RecorderInfo. @@ -3547,7 +3643,7 @@ type TelemetryState struct { // AppliedAt Wall-clock time at which the current configuration was applied. Omitted when telemetry is not configured. AppliedAt *time.Time `json:"applied_at,omitempty"` - // Config Telemetry configuration for a browser. Per-category capture settings. Omit a category or set enabled: true to capture it. Set enabled: false to exclude it. Omit the browser key entirely to capture all categories. Set all five categories to enabled: false to clear the telemetry configuration. + // Config Telemetry configuration for a browser. Per-category capture settings. Omit the browser key (or send an empty object) to capture the default set: every category except `screenshot`, which is heavy and opt-in. Within `browser`, omit a category to leave it at its default state, or set enabled true/false to override. Set every configurable category to enabled: false to clear the telemetry configuration. The `monitor` category (CDP collector health) is not configurable here; it flows automatically whenever a CDP category is captured. Config BrowserTelemetryConfig `json:"config"` // Seq Process-monotonic sequence number of the last published event. Does not reset across configuration changes. @@ -9796,6 +9892,7 @@ type PatchTelemetryResponse struct { JSON200 *TelemetryState JSON400 *BadRequestError JSON404 *NotFoundError + JSON500 *InternalError } // Status returns HTTPResponse.Status @@ -12596,6 +12693,13 @@ func ParsePatchTelemetryResponse(rsp *http.Response) (*PatchTelemetryResponse, e } response.JSON404 = &dest + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: + var dest InternalError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON500 = &dest + } return response, nil @@ -16723,6 +16827,15 @@ func (response PatchTelemetry404JSONResponse) VisitPatchTelemetryResponse(w http return json.NewEncoder(w).Encode(response) } +type PatchTelemetry500JSONResponse struct{ InternalErrorJSONResponse } + +func (response PatchTelemetry500JSONResponse) VisitPatchTelemetryResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + type PutTelemetryRequestObject struct { Body *PutTelemetryJSONRequestBody } @@ -18681,355 +18794,360 @@ func (sh *strictHandler) StreamTelemetryEvents(w http.ResponseWriter, r *http.Re var swaggerSpec = []string{ "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", + "ovYuHZF4snSj/98AAAD///KQFumjvQEA", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/server/lib/sysmon/README.md b/server/lib/sysmon/README.md index 7c85f5b8..790c7297 100644 --- a/server/lib/sysmon/README.md +++ b/server/lib/sysmon/README.md @@ -77,10 +77,11 @@ docker run -d --rm --name chromium-headless-test \ # Wait for the API. sleep 10 && curl -sf http://localhost:444/spec.json >/dev/null && echo "API up" -# Configure telemetry so a session is active. The empty body captures every -# browser category — system events flow regardless because Start force-includes -# them. (Setting all browser categories to enabled:false is treated as -# "clear the configuration" and tears the session down — don't do that here.) +# Configure telemetry so a session is active. The empty body captures the +# default set (every category except screenshot), which includes system, so +# OOM/crash events flow. (Setting all browser categories to enabled:false is +# treated as "clear the configuration" and tears the session down — don't do +# that here.) curl -sf -X PUT http://localhost:444/telemetry \ -H 'content-type: application/json' -d '{}' diff --git a/server/lib/telemetry/telemetry.go b/server/lib/telemetry/telemetry.go index 36cb90ec..2e668f84 100644 --- a/server/lib/telemetry/telemetry.go +++ b/server/lib/telemetry/telemetry.go @@ -9,11 +9,11 @@ import ( ) // TelemetryConfig holds caller-supplied telemetry preferences. All fields are -// optional; zero values mean "use server defaults" (all user-facing categories -// plus system events). +// optional; zero values mean "use server defaults" (events.DefaultCategories). type TelemetryConfig struct { - // Categories limits which event categories are captured. - // nil or empty captures all user-facing categories plus system events. + // Categories limits which event categories are captured. nil or empty + // captures events.DefaultCategories. Monitor is added automatically when + // any CDP category is present and is not configurable here. Categories []oapi.TelemetryEventCategory } @@ -38,11 +38,24 @@ func NewTelemetrySession(es *events.EventStream) *TelemetrySession { if es == nil { panic("telemetry: NewTelemetrySession requires a non-nil EventStream") } - cats := make(map[oapi.TelemetryEventCategory]struct{}, len(events.AllCategories)) - for _, c := range events.AllCategories { - cats[c] = struct{}{} + return &TelemetrySession{es: es, categories: categorySet(nil)} +} + +// categorySet builds the active filter set from the configured categories. An +// empty config falls back to the default set. Monitor is included whenever any +// CDP category is present, since collector-health rides along with CDP data. +func categorySet(cats []oapi.TelemetryEventCategory) map[oapi.TelemetryEventCategory]struct{} { + if len(cats) == 0 { + cats = events.DefaultCategories + } + set := make(map[oapi.TelemetryEventCategory]struct{}, len(cats)+1) + for _, c := range cats { + set[c] = struct{}{} + } + if events.HasCDPCategory(cats) { + set[events.Monitor] = struct{}{} } - return &TelemetrySession{es: es, categories: cats} + return set } // Start begins a new telemetry session with the given ID and config. Sequence @@ -54,18 +67,7 @@ func (s *TelemetrySession) Start(telemetrySessionID string, cfg TelemetryConfig) s.id = telemetrySessionID s.sessionStartSeq = s.es.Seq() s.appliedAt = time.Now() - - // Build the category filter. CategorySystem is always included so - // kernel_api events (e.g. monitor_disconnected) are never dropped. - cats := cfg.Categories - if len(cats) == 0 { - cats = events.AllCategories - } - s.categories = make(map[oapi.TelemetryEventCategory]struct{}, len(cats)+1) - for _, c := range cats { - s.categories[c] = struct{}{} - } - s.categories[events.System] = struct{}{} + s.categories = categorySet(cfg.Categories) } // publishLocked stamps telemetry_session_id into ev.Source.Metadata and forwards to the bus. @@ -146,16 +148,7 @@ func (s *TelemetrySession) AppliedAt() time.Time { func (s *TelemetrySession) UpdateConfig(cfg TelemetryConfig) { s.mu.Lock() defer s.mu.Unlock() - // CategorySystem is always included so kernel_api events are never dropped. - cats := cfg.Categories - if len(cats) == 0 { - cats = events.AllCategories - } - s.categories = make(map[oapi.TelemetryEventCategory]struct{}, len(cats)+1) - for _, c := range cats { - s.categories[c] = struct{}{} - } - s.categories[events.System] = struct{}{} + s.categories = categorySet(cfg.Categories) } // Active reports whether a telemetry session is currently running. diff --git a/server/lib/telemetry/telemetry_test.go b/server/lib/telemetry/telemetry_test.go index 126d8a82..9258c2ef 100644 --- a/server/lib/telemetry/telemetry_test.go +++ b/server/lib/telemetry/telemetry_test.go @@ -182,19 +182,28 @@ func TestTelemetrySession(t *testing.T) { assert.JSONEq(t, `{"url":"https://example.com"}`, string(env.Event.Data)) }) - t.Run("system_events_always_captured_regardless_of_config", func(t *testing.T) { + t.Run("monitor events ride along when a CDP category is enabled", func(t *testing.T) { ts := NewTelemetrySession(newTestEventStream(t, 10)) - // Start with only console category — system should still pass through. - ts.Start("sys-test", TelemetryConfig{Categories: []oapi.TelemetryEventCategory{events.Console}}) + // Console is a CDP category, so collector-health (monitor) rides along. + ts.Start("mon-test", TelemetryConfig{Categories: []oapi.TelemetryEventCategory{events.Console}}) reader := ts.NewReader(0) - ts.Publish(events.Event{Type: "monitor.disconnected", Category: events.System, Source: oapi.BrowserEventSource{Kind: oapi.KernelApi}, Ts: 1}) + ts.Publish(events.Event{Type: "monitor_disconnected", Category: events.Monitor, Source: oapi.BrowserEventSource{Kind: oapi.KernelApi}, Ts: 1}) ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() env := readEnvelope(t, reader, ctx) - assert.Equal(t, events.System, env.Event.Category) + assert.Equal(t, events.Monitor, env.Event.Category) + }) + + t.Run("monitor events dropped without a CDP category", func(t *testing.T) { + ts := NewTelemetrySession(newTestEventStream(t, 10)) + // System-only: the CDP collector never runs, so monitor must not flow. + ts.Start("no-cdp", TelemetryConfig{Categories: []oapi.TelemetryEventCategory{events.System}}) + + _, ok := ts.Publish(events.Event{Type: "monitor_disconnected", Category: events.Monitor, Source: oapi.BrowserEventSource{Kind: oapi.KernelApi}, Ts: 1}) + assert.False(t, ok, "monitor event should be dropped when no CDP category is enabled") }) t.Run("truncation_applied", func(t *testing.T) { diff --git a/server/openapi.yaml b/server/openapi.yaml index b9d0a6f8..ee2d4f5d 100644 --- a/server/openapi.yaml +++ b/server/openapi.yaml @@ -1298,9 +1298,9 @@ paths: summary: Set telemetry configuration description: > Sets the telemetry configuration. Returns 201 if telemetry was not - previously configured; returns 200 if it was. Setting all five categories - to enabled: false clears the configuration; this is idempotent when - telemetry is not configured. + previously configured; returns 200 if it was. Setting every configurable + category to enabled: false clears the configuration; this is idempotent + when telemetry is not configured. operationId: putTelemetry requestBody: required: false @@ -1331,7 +1331,7 @@ paths: Partially updates the telemetry configuration. Only categories explicitly set in the request body are changed; omitted categories retain their current settings. Returns 404 if telemetry is not configured. Setting - all five categories to enabled: false clears the configuration. + every configurable category to enabled: false clears the configuration. operationId: patchTelemetry requestBody: required: true @@ -1350,6 +1350,8 @@ paths: $ref: "#/components/responses/BadRequestError" "404": $ref: "#/components/responses/NotFoundError" + "500": + $ref: "#/components/responses/InternalError" /telemetry/events: post: summary: Publish an event into the telemetry stream @@ -1469,8 +1471,12 @@ components: - network - page - interaction - - api + - control + - connection - system + - screenshot + - captcha + - monitor source: $ref: "#/components/schemas/BrowserEventSource" data: @@ -1518,15 +1524,21 @@ components: description: Event type identifier. category: type: string - description: Event category. + description: > + Event category. Optional and advisory: for a known event `type` the + server assigns the category authoritatively and ignores this field. + It is only used for unknown custom types, where it is required. enum: - console - network - page - interaction - - api + - control + - connection - system - default: system + - screenshot + - captcha + - monitor source: $ref: "#/components/schemas/BrowserEventSource" data: @@ -1554,10 +1566,13 @@ components: type: object description: > Telemetry configuration for a browser. Per-category capture settings. - Omit a category or set enabled: true to capture it. - Set enabled: false to exclude it. - Omit the browser key entirely to capture all categories. - Set all five categories to enabled: false to clear the telemetry configuration. + Omit the browser key (or send an empty object) to capture the default + set: every category except `screenshot`, which is heavy and opt-in. + Within `browser`, omit a category to leave it at its default state, or + set enabled true/false to override. Set every configurable category to + enabled: false to clear the telemetry configuration. The `monitor` + category (CDP collector health) is not configurable here; it flows + automatically whenever a CDP category is captured. properties: browser: $ref: "#/components/schemas/BrowserTelemetryCategoriesConfig" @@ -1578,14 +1593,21 @@ components: network: $ref: "#/components/schemas/BrowserTelemetryCategoryConfig" description: HTTP request/response metadata. - api: + control: $ref: "#/components/schemas/BrowserTelemetryCategoryConfig" - description: > - Kernel-image-layer activity that the customer drives: inbound - API calls to the kernel-images-api server and - extension-mediated captcha solve attempts. CDP proxy and live - view session lifecycle events are infrastructure and live in - the always-on `system` category. + description: Agent-driven actions against the browser, such as inbound calls to the kernel-images-api server. + connection: + $ref: "#/components/schemas/BrowserTelemetryCategoryConfig" + description: Client attach/detach lifecycle for the CDP proxy and live view. + system: + $ref: "#/components/schemas/BrowserTelemetryCategoryConfig" + description: Browser VM health, such as out-of-memory kills and managed-service crashes. + screenshot: + $ref: "#/components/schemas/BrowserTelemetryCategoryConfig" + description: Periodic base64-encoded viewport screenshots. High volume; off by default and opt-in. + captcha: + $ref: "#/components/schemas/BrowserTelemetryCategoryConfig" + description: Captcha solve attempt outcomes. additionalProperties: false BrowserTelemetryCategoryConfig: type: object @@ -1595,10 +1617,11 @@ components: type: boolean description: > Whether this category is captured. In PUT requests, omitting this field - defaults to true (category enabled). In PATCH requests, omitting this field - (or sending an empty object `{}`) is a no-op; the category retains its - current state. To enable or disable a category via PATCH, you must send - an explicit `true` or `false`. + leaves the category at its default state (every category on except + `screenshot`). In PATCH requests, omitting this field (or sending an + empty object `{}`) is a no-op; the category retains its current state. + To enable or disable a category via PATCH, you must send an explicit + `true` or `false`. additionalProperties: false BrowserCallStack: type: object @@ -2403,7 +2426,7 @@ components: const: monitor_screenshot category: type: string - const: system + const: screenshot source: $ref: "#/components/schemas/BrowserEventSource" data: @@ -2434,7 +2457,7 @@ components: const: monitor_disconnected category: type: string - const: system + const: monitor source: $ref: "#/components/schemas/BrowserEventSource" data: @@ -2465,7 +2488,7 @@ components: const: monitor_reconnected category: type: string - const: system + const: monitor source: $ref: "#/components/schemas/BrowserEventSource" data: @@ -2496,7 +2519,7 @@ components: const: monitor_reconnect_failed category: type: string - const: system + const: monitor source: $ref: "#/components/schemas/BrowserEventSource" data: @@ -2526,7 +2549,7 @@ components: const: monitor_init_failed category: type: string - const: system + const: monitor source: $ref: "#/components/schemas/BrowserEventSource" data: @@ -2566,7 +2589,7 @@ components: const: api_call category: type: string - const: api + const: control source: $ref: "#/components/schemas/BrowserEventSource" data: @@ -2588,7 +2611,7 @@ components: const: cdp_connect category: type: string - const: system + const: connection source: $ref: "#/components/schemas/BrowserEventSource" truncated: @@ -2633,7 +2656,7 @@ components: const: cdp_disconnect category: type: string - const: system + const: connection source: $ref: "#/components/schemas/BrowserEventSource" data: @@ -2653,7 +2676,7 @@ components: BrowserLiveViewConnectEvent: type: object description: A live view client connected to the headful browser's WebRTC server (Neko). Headful only; not emitted for headless images. - required: [ts, type, source] + required: [ts, type, category, source] properties: ts: type: integer @@ -2662,6 +2685,9 @@ components: type: type: string const: live_view_connect + category: + type: string + const: connection source: $ref: "#/components/schemas/BrowserEventSource" data: @@ -2684,7 +2710,7 @@ components: BrowserLiveViewDisconnectEvent: type: object description: A live view client disconnected from the headful browser's WebRTC server (Neko). Pair with `live_view_connect` by `session_id`. - required: [ts, type, source] + required: [ts, type, category, source] properties: ts: type: integer @@ -2693,6 +2719,9 @@ components: type: type: string const: live_view_disconnect + category: + type: string + const: connection source: $ref: "#/components/schemas/BrowserEventSource" data: @@ -2750,7 +2779,7 @@ components: BrowserCaptchaSolveResultEvent: type: object description: A captcha solve attempt reached a terminal outcome. - required: [ts, type, source] + required: [ts, type, category, source] properties: ts: type: integer @@ -2759,6 +2788,9 @@ components: type: type: string const: captcha_solve_result + category: + type: string + const: captcha source: $ref: "#/components/schemas/BrowserEventSource" data: diff --git a/server/scripts/categorygen/main.go b/server/scripts/categorygen/main.go new file mode 100644 index 00000000..0d054018 --- /dev/null +++ b/server/scripts/categorygen/main.go @@ -0,0 +1,88 @@ +// Command categorygen derives the authoritative event-type -> category lookup +// from openapi.yaml. Every schema that pins both a `type` const and a +// `category` const contributes one entry. openapi.yaml is the single source of +// truth; this generator just surfaces it to Go. +package main + +import ( + "bytes" + "flag" + "fmt" + "go/format" + "os" + "sort" + + "gopkg.in/yaml.v3" +) + +type property struct { + Const string `yaml:"const"` +} + +type schema struct { + Properties map[string]property `yaml:"properties"` +} + +type document struct { + Components struct { + Schemas map[string]schema `yaml:"schemas"` + } `yaml:"components"` +} + +func main() { + openapiPath := flag.String("openapi", "", "path to openapi.yaml") + outPath := flag.String("out", "", "path to the generated Go file") + flag.Parse() + if *openapiPath == "" || *outPath == "" { + fmt.Fprintln(os.Stderr, "categorygen: -openapi and -out are required") + os.Exit(2) + } + + raw, err := os.ReadFile(*openapiPath) + if err != nil { + fmt.Fprintf(os.Stderr, "categorygen: read openapi: %v\n", err) + os.Exit(1) + } + var doc document + if err := yaml.Unmarshal(raw, &doc); err != nil { + fmt.Fprintf(os.Stderr, "categorygen: parse openapi: %v\n", err) + os.Exit(1) + } + + type entry struct{ typ, category string } + entries := make([]entry, 0, len(doc.Components.Schemas)) + for _, s := range doc.Components.Schemas { + typ := s.Properties["type"].Const + category := s.Properties["category"].Const + if typ != "" && category != "" { + entries = append(entries, entry{typ, category}) + } + } + sort.Slice(entries, func(i, j int) bool { return entries[i].typ < entries[j].typ }) + + var buf bytes.Buffer + buf.WriteString("// Code generated by scripts/categorygen; DO NOT EDIT.\n\n") + buf.WriteString("package events\n\n") + buf.WriteString(`import oapi "github.com/kernel/kernel-images/server/lib/oapi"` + "\n\n") + buf.WriteString("var categoryByType = map[string]oapi.TelemetryEventCategory{\n") + for _, e := range entries { + fmt.Fprintf(&buf, "\t%q: oapi.TelemetryEventCategory(%q),\n", e.typ, e.category) + } + buf.WriteString("}\n\n") + buf.WriteString("// CategoryForType returns the authoritative category for a known event\n") + buf.WriteString("// type. ok is false for an unknown type.\n") + buf.WriteString("func CategoryForType(eventType string) (oapi.TelemetryEventCategory, bool) {\n") + buf.WriteString("\tc, ok := categoryByType[eventType]\n") + buf.WriteString("\treturn c, ok\n") + buf.WriteString("}\n") + + formatted, err := format.Source(buf.Bytes()) + if err != nil { + fmt.Fprintf(os.Stderr, "categorygen: format: %v\n", err) + os.Exit(1) + } + if err := os.WriteFile(*outPath, formatted, 0o644); err != nil { + fmt.Fprintf(os.Stderr, "categorygen: write %s: %v\n", *outPath, err) + os.Exit(1) + } +} From e45981ff01171689d625e3340b880895f5a55b87 Mon Sep 17 00:00:00 2001 From: Sayan Samanta Date: Tue, 2 Jun 2026 20:55:00 -0700 Subject: [PATCH 2/4] Make telemetry config apply atomic on collector start failure Reconcile the CDP collector before committing the new config instead of after. The collector start is the only fallible step, so doing it first means a failure returns 500 without mutating the session config or middleware, for both fresh and already-active PUT/PATCH. Resolves the default category set in telemetryConfigFromOAPI so the effective categories are known at reconcile time. Addresses Bugbot review on #268. Co-authored-by: Cursor --- server/cmd/api/api/telemetry.go | 69 ++++++++++++++-------------- server/cmd/api/api/telemetry_test.go | 59 ++++++++++++++++++++++-- 2 files changed, 90 insertions(+), 38 deletions(-) diff --git a/server/cmd/api/api/telemetry.go b/server/cmd/api/api/telemetry.go index f8b09f22..e3b9f42f 100644 --- a/server/cmd/api/api/telemetry.go +++ b/server/cmd/api/api/telemetry.go @@ -41,30 +41,23 @@ func (s *ApiService) PutTelemetry(ctx context.Context, req oapi.PutTelemetryRequ if allDisabled { if wasActive { s.telemetrySession.Stop() - _ = s.applyTelemetryState() + s.stopTelemetryState() } return oapi.PutTelemetry200JSONResponse(oapi.TelemetryState{Config: disabledConfig(), Seq: int64(s.telemetrySession.Seq())}), nil } - if wasActive { - s.telemetrySession.UpdateConfig(cfg) - } else { - s.telemetrySession.Start(cuid2.Generate(), cfg) - } - - if err := s.applyTelemetryState(); err != nil { - if !wasActive { - // Roll back the freshly started session so a retry can succeed. - s.telemetrySession.Stop() - _ = s.applyTelemetryState() - } + // Reconcile the collector before committing the config so a startup failure + // leaves no partial state: the session keeps its prior config on error. + if err := s.reconcileTelemetryState(cfg.Categories); err != nil { logger.FromContext(ctx).Error("failed to apply telemetry state", "err", err) return oapi.PutTelemetry500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to start telemetry"}}, nil } if wasActive { + s.telemetrySession.UpdateConfig(cfg) return oapi.PutTelemetry200JSONResponse(s.buildTelemetryResponse()), nil } + s.telemetrySession.Start(cuid2.Generate(), cfg) return oapi.PutTelemetry201JSONResponse(s.buildTelemetryResponse()), nil } @@ -86,45 +79,51 @@ func (s *ApiService) PatchTelemetry(ctx context.Context, req oapi.PatchTelemetry cfg, allDisabled := mergeTelemetryConfig(s.telemetrySession.Config(), req.Body.Browser) if allDisabled { s.telemetrySession.Stop() - _ = s.applyTelemetryState() + s.stopTelemetryState() return oapi.PatchTelemetry200JSONResponse(oapi.TelemetryState{Config: disabledConfig(), Seq: int64(s.telemetrySession.Seq())}), nil } - s.telemetrySession.UpdateConfig(cfg) - if err := s.applyTelemetryState(); err != nil { + // Reconcile before committing so a collector startup failure leaves the + // session on its prior config rather than half-applied. + if err := s.reconcileTelemetryState(cfg.Categories); err != nil { logger.FromContext(ctx).Error("failed to apply telemetry state", "err", err) return oapi.PatchTelemetry500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to apply telemetry"}}, nil } + s.telemetrySession.UpdateConfig(cfg) return oapi.PatchTelemetry200JSONResponse(s.buildTelemetryResponse()), nil } -// applyTelemetryState reconciles the CDP collector and the api_call (control) -// middleware with the active telemetry config. The CDP collector runs iff a CDP -// category is captured; the middleware emits iff the control category is. Call -// after any session change. -func (s *ApiService) applyTelemetryState() error { - if !s.telemetrySession.Active() { - if s.cdpMonitor.IsRunning() { - s.cdpMonitor.Stop() +// reconcileTelemetryState reconciles the CDP collector and the api_call +// (control) middleware to the desired category set. The collector runs iff a +// CDP category is captured; the middleware emits iff the control category is. +// It returns an error only when the collector fails to start, and performs the +// fallible collector start before any other state change so callers can invoke +// it before committing the config and leave nothing half-applied on failure. +func (s *ApiService) reconcileTelemetryState(cats []oapi.TelemetryEventCategory) error { + switch { + case events.HasCDPCategory(cats) && !s.cdpMonitor.IsRunning(): + if err := s.cdpMonitor.Start(s.lifecycleCtx); err != nil { + return err } - DisableTelemetryMiddleware() - return nil + case !events.HasCDPCategory(cats) && s.cdpMonitor.IsRunning(): + s.cdpMonitor.Stop() } - cats := s.telemetrySession.Config().Categories if containsCategory(cats, events.Control) { EnableTelemetryMiddleware() } else { DisableTelemetryMiddleware() } + return nil +} - switch { - case events.HasCDPCategory(cats) && !s.cdpMonitor.IsRunning(): - return s.cdpMonitor.Start(s.lifecycleCtx) - case !events.HasCDPCategory(cats) && s.cdpMonitor.IsRunning(): +// stopTelemetryState tears down the collector and middleware after a session is +// cleared. +func (s *ApiService) stopTelemetryState() { + if s.cdpMonitor.IsRunning() { s.cdpMonitor.Stop() } - return nil + DisableTelemetryMiddleware() } // buildTelemetryResponse constructs a TelemetryState response from the current configuration. @@ -182,8 +181,10 @@ func containsCategory(cats []oapi.TelemetryEventCategory, target oapi.TelemetryE // config, whether every configurable category ended up disabled (stop signal), and any error. func telemetryConfigFromOAPI(cfg *oapi.BrowserTelemetryConfig) (telemetry.TelemetryConfig, bool, error) { if cfg == nil || cfg.Browser == nil { - // No config provided: the default set is applied downstream. - return telemetry.TelemetryConfig{}, false, nil + // No per-category settings: resolve to the explicit default set so the + // effective categories are known before the collector is reconciled. + cats := append([]oapi.TelemetryEventCategory(nil), events.DefaultCategories...) + return telemetry.TelemetryConfig{Categories: cats}, false, nil } defaultOn := categorySetOf(events.DefaultCategories) diff --git a/server/cmd/api/api/telemetry_test.go b/server/cmd/api/api/telemetry_test.go index acb215b3..75466304 100644 --- a/server/cmd/api/api/telemetry_test.go +++ b/server/cmd/api/api/telemetry_test.go @@ -2,6 +2,7 @@ package api import ( "context" + "errors" "testing" "github.com/kernel/kernel-images/server/lib/events" @@ -33,18 +34,18 @@ func allCategoriesDisabled() *oapi.BrowserTelemetryCategoriesConfig { } func TestTelemetryConfigFromOAPI(t *testing.T) { - t.Run("nil body returns defaults", func(t *testing.T) { + t.Run("nil body returns the default set", func(t *testing.T) { cfg, allDisabled, err := telemetryConfigFromOAPI(nil) require.NoError(t, err) assert.False(t, allDisabled) - assert.Empty(t, cfg.Categories) + assert.ElementsMatch(t, events.DefaultCategories, cfg.Categories) }) - t.Run("nil browser key returns defaults", func(t *testing.T) { + t.Run("nil browser key returns the default set", func(t *testing.T) { cfg, allDisabled, err := telemetryConfigFromOAPI(&oapi.BrowserTelemetryConfig{}) require.NoError(t, err) assert.False(t, allDisabled) - assert.Empty(t, cfg.Categories) + assert.ElementsMatch(t, events.DefaultCategories, cfg.Categories) }) t.Run("omitted enabled resolves to default state", func(t *testing.T) { @@ -355,3 +356,53 @@ type stubCdpMonitor struct{} func (s *stubCdpMonitor) Start(_ context.Context) error { return nil } func (s *stubCdpMonitor) Stop() {} func (s *stubCdpMonitor) IsRunning() bool { return false } + +// failingCdpMonitor always fails to start, to exercise the reconcile-before-commit path. +type failingCdpMonitor struct{ running bool } + +func (f *failingCdpMonitor) Start(_ context.Context) error { + return errors.New("collector start failed") +} +func (f *failingCdpMonitor) Stop() { f.running = false } +func (f *failingCdpMonitor) IsRunning() bool { return f.running } + +func TestTelemetryCollectorFailureLeavesConfigUnchanged(t *testing.T) { + ctx := context.Background() + + t.Run("fresh PUT failure starts no session", func(t *testing.T) { + svc := newTestService(t, newMockRecordManager()) + svc.cdpMonitor = &failingCdpMonitor{} + + resp, err := svc.PutTelemetry(ctx, oapi.PutTelemetryRequestObject{}) + require.NoError(t, err) + assert.IsType(t, oapi.PutTelemetry500JSONResponse{}, resp) + assert.False(t, svc.telemetrySession.Active(), "failed collector start must not leave a session active") + }) + + t.Run("PATCH failure keeps the prior config", func(t *testing.T) { + svc := newTestService(t, newMockRecordManager()) + // Start a session that does not need the CDP collector (system only). + tr := true + start := allCategoriesDisabled() + start.System = &oapi.BrowserTelemetryCategoryConfig{Enabled: &tr} + _, err := svc.PutTelemetry(ctx, oapi.PutTelemetryRequestObject{ + Body: &oapi.BrowserTelemetryConfig{Browser: start}, + }) + require.NoError(t, err) + before := svc.telemetrySession.Config().Categories + + // Now the collector cannot start; enabling a CDP category must fail + // without mutating the session config. + svc.cdpMonitor = &failingCdpMonitor{} + resp, err := svc.PatchTelemetry(ctx, oapi.PatchTelemetryRequestObject{ + Body: &oapi.BrowserTelemetryConfig{ + Browser: &oapi.BrowserTelemetryCategoriesConfig{ + Console: &oapi.BrowserTelemetryCategoryConfig{Enabled: &tr}, + }, + }, + }) + require.NoError(t, err) + assert.IsType(t, oapi.PatchTelemetry500JSONResponse{}, resp) + assert.ElementsMatch(t, before, svc.telemetrySession.Config().Categories, "failed PATCH must not change the persisted config") + }) +} From d6dc84a713887c9a0872a904f02b054ef2a7a1c5 Mon Sep 17 00:00:00 2001 From: Sayan Samanta Date: Tue, 2 Jun 2026 21:44:52 -0700 Subject: [PATCH 3/4] Commit telemetry config before reconciling the collector The prior atomicity fix reconciled the collector before committing the config, which dropped any events the collector emitted in the window before the session filter went live. Commit the config first (infallible) so the filter is active before the collector starts, then reconcile, and roll back fully to the prior config on a collector start failure. Reverting never needs a fallible collector start, so rollback cannot fail. Addresses the second Bugbot review on #268. Co-authored-by: Cursor --- server/cmd/api/api/telemetry.go | 60 ++++++++++++++++++++++----------- 1 file changed, 41 insertions(+), 19 deletions(-) diff --git a/server/cmd/api/api/telemetry.go b/server/cmd/api/api/telemetry.go index e3b9f42f..8f1f905e 100644 --- a/server/cmd/api/api/telemetry.go +++ b/server/cmd/api/api/telemetry.go @@ -46,18 +46,26 @@ func (s *ApiService) PutTelemetry(ctx context.Context, req oapi.PutTelemetryRequ return oapi.PutTelemetry200JSONResponse(oapi.TelemetryState{Config: disabledConfig(), Seq: int64(s.telemetrySession.Seq())}), nil } - // Reconcile the collector before committing the config so a startup failure - // leaves no partial state: the session keeps its prior config on error. + // Commit the config first so the filter is live before the collector emits, + // then reconcile. On collector-start failure, roll back to the prior state + // so a 500 never leaves telemetry half-applied. + var prev telemetry.TelemetryConfig + if wasActive { + prev = s.telemetrySession.Config() + s.telemetrySession.UpdateConfig(cfg) + } else { + s.telemetrySession.Start(cuid2.Generate(), cfg) + } + if err := s.reconcileTelemetryState(cfg.Categories); err != nil { + s.rollbackTelemetry(wasActive, prev) logger.FromContext(ctx).Error("failed to apply telemetry state", "err", err) return oapi.PutTelemetry500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to start telemetry"}}, nil } if wasActive { - s.telemetrySession.UpdateConfig(cfg) return oapi.PutTelemetry200JSONResponse(s.buildTelemetryResponse()), nil } - s.telemetrySession.Start(cuid2.Generate(), cfg) return oapi.PutTelemetry201JSONResponse(s.buildTelemetryResponse()), nil } @@ -76,45 +84,59 @@ func (s *ApiService) PatchTelemetry(ctx context.Context, req oapi.PatchTelemetry return oapi.PatchTelemetry200JSONResponse(s.buildTelemetryResponse()), nil } - cfg, allDisabled := mergeTelemetryConfig(s.telemetrySession.Config(), req.Body.Browser) + prev := s.telemetrySession.Config() + cfg, allDisabled := mergeTelemetryConfig(prev, req.Body.Browser) if allDisabled { s.telemetrySession.Stop() s.stopTelemetryState() return oapi.PatchTelemetry200JSONResponse(oapi.TelemetryState{Config: disabledConfig(), Seq: int64(s.telemetrySession.Seq())}), nil } - // Reconcile before committing so a collector startup failure leaves the - // session on its prior config rather than half-applied. + // Commit first so the filter is live before the collector emits, then + // reconcile and roll back on collector-start failure. + s.telemetrySession.UpdateConfig(cfg) if err := s.reconcileTelemetryState(cfg.Categories); err != nil { + s.rollbackTelemetry(true, prev) logger.FromContext(ctx).Error("failed to apply telemetry state", "err", err) return oapi.PatchTelemetry500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to apply telemetry"}}, nil } - s.telemetrySession.UpdateConfig(cfg) return oapi.PatchTelemetry200JSONResponse(s.buildTelemetryResponse()), nil } // reconcileTelemetryState reconciles the CDP collector and the api_call // (control) middleware to the desired category set. The collector runs iff a // CDP category is captured; the middleware emits iff the control category is. -// It returns an error only when the collector fails to start, and performs the -// fallible collector start before any other state change so callers can invoke -// it before committing the config and leave nothing half-applied on failure. +// Callers commit the session config first so the filter is live before the +// collector emits; this returns an error only when the collector fails to +// start, leaving the caller to roll back. func (s *ApiService) reconcileTelemetryState(cats []oapi.TelemetryEventCategory) error { + if containsCategory(cats, events.Control) { + EnableTelemetryMiddleware() + } else { + DisableTelemetryMiddleware() + } + switch { case events.HasCDPCategory(cats) && !s.cdpMonitor.IsRunning(): - if err := s.cdpMonitor.Start(s.lifecycleCtx); err != nil { - return err - } + return s.cdpMonitor.Start(s.lifecycleCtx) case !events.HasCDPCategory(cats) && s.cdpMonitor.IsRunning(): s.cdpMonitor.Stop() } + return nil +} - if containsCategory(cats, events.Control) { - EnableTelemetryMiddleware() - } else { - DisableTelemetryMiddleware() +// rollbackTelemetry restores telemetry to its prior state after a failed apply. +// A fresh session is torn down; an updated session is reverted to prev. Reverting +// never requires a fallible collector start (the failed start left it stopped), +// so the reconcile here cannot fail. +func (s *ApiService) rollbackTelemetry(wasActive bool, prev telemetry.TelemetryConfig) { + if !wasActive { + s.telemetrySession.Stop() + s.stopTelemetryState() + return } - return nil + s.telemetrySession.UpdateConfig(prev) + _ = s.reconcileTelemetryState(prev.Categories) } // stopTelemetryState tears down the collector and middleware after a session is From b1b4bd4a2ca6e512746207a48157402b38c66fea Mon Sep 17 00:00:00 2001 From: Sayan Samanta Date: Tue, 2 Jun 2026 21:51:18 -0700 Subject: [PATCH 4/4] Gate screenshot capture on the screenshot category The CDP collector captures a screenshot via ffmpeg on page-load and uncaught-exception events (throttled to once per 2s). With the screenshot category off by default, those captures ran and were then dropped at the telemetry filter, spending ffmpeg on output nobody receives. Pass a screenshotEnabled predicate (wired to TelemetrySession.CategoryEnabled) into the monitor and skip the capture entirely when the category is disabled. A nil predicate always captures, preserving existing test behavior. Co-authored-by: Cursor --- server/cmd/api/api/api.go | 3 ++- server/lib/cdpmonitor/cdp_test.go | 4 ++-- server/lib/cdpmonitor/monitor.go | 23 +++++++++++++---------- server/lib/cdpmonitor/monitor_test.go | 20 +++++++++++++++----- server/lib/cdpmonitor/screenshot.go | 6 ++++++ server/lib/telemetry/telemetry.go | 12 ++++++++++++ 6 files changed, 50 insertions(+), 18 deletions(-) diff --git a/server/cmd/api/api/api.go b/server/cmd/api/api/api.go index de27629e..b9b0a346 100644 --- a/server/cmd/api/api/api.go +++ b/server/cmd/api/api/api.go @@ -121,7 +121,8 @@ func New( return nil, fmt.Errorf("eventStream cannot be nil") } - mon := cdpmonitor.New(upstreamMgr, telemetrySession.Publish, displayNum, slog.Default()) + screenshotEnabled := func() bool { return telemetrySession.CategoryEnabled(events.Screenshot) } + mon := cdpmonitor.New(upstreamMgr, telemetrySession.Publish, displayNum, slog.Default(), screenshotEnabled) ctx, cancel := context.WithCancel(context.Background()) return &ApiService{ diff --git a/server/lib/cdpmonitor/cdp_test.go b/server/lib/cdpmonitor/cdp_test.go index 56b0422b..50994e47 100644 --- a/server/lib/cdpmonitor/cdp_test.go +++ b/server/lib/cdpmonitor/cdp_test.go @@ -293,7 +293,7 @@ func startMonitor(t *testing.T, srv *testServer, fn ResponderFunc) (*Monitor, *e t.Helper() ec := newEventCollector() upstream := newTestUpstream(srv.wsURL()) - m := New(upstream, ec.publishFn(), 99, discardLogger) + m := New(upstream, ec.publishFn(), 99, discardLogger, nil) require.NoError(t, m.Start(context.Background())) // Closed when Target.getTargets is responded to (last command of initSession). @@ -341,7 +341,7 @@ func newComputedMonitor(t *testing.T) (*Monitor, *eventCollector) { t.Helper() ec := newEventCollector() upstream := newTestUpstream("ws://127.0.0.1:0") - m := New(upstream, ec.publishFn(), 0, discardLogger) + m := New(upstream, ec.publishFn(), 0, discardLogger, nil) return m, ec } diff --git a/server/lib/cdpmonitor/monitor.go b/server/lib/cdpmonitor/monitor.go index 5a1b764f..9fc3dce3 100644 --- a/server/lib/cdpmonitor/monitor.go +++ b/server/lib/cdpmonitor/monitor.go @@ -58,6 +58,7 @@ type Monitor struct { lastScreenshotAt atomic.Int64 // unix millis of last capture screenshotInFlight atomic.Bool // true while a captureScreenshot goroutine is running screenshotFn func(ctx context.Context, displayNum int) ([]byte, error) // nil → real ffmpeg + screenshotEnabled func() bool // nil → always capture; gates ffmpeg on the screenshot category // bindingRateMu guards bindingLastSeen. bindingRateMu sync.Mutex @@ -78,17 +79,19 @@ type Monitor struct { } // New creates a Monitor. displayNum is the X display for ffmpeg screenshots. -func New(upstreamMgr UpstreamProvider, publish PublishFunc, displayNum int, log *slog.Logger) *Monitor { +// screenshotEnabled gates screenshot capture; a nil predicate always captures. +func New(upstreamMgr UpstreamProvider, publish PublishFunc, displayNum int, log *slog.Logger, screenshotEnabled func() bool) *Monitor { m := &Monitor{ - upstreamMgr: upstreamMgr, - publish: publish, - displayNum: displayNum, - log: log, - sessions: make(map[string]targetInfo), - computedStates: make(map[string]*computedState), - pending: make(map[int64]chan cdpMessage), - pendingRequests: make(map[string]networkReqState), - bindingLastSeen: make(map[string]time.Time), + upstreamMgr: upstreamMgr, + publish: publish, + displayNum: displayNum, + log: log, + screenshotEnabled: screenshotEnabled, + sessions: make(map[string]targetInfo), + computedStates: make(map[string]*computedState), + pending: make(map[int64]chan cdpMessage), + pendingRequests: make(map[string]networkReqState), + bindingLastSeen: make(map[string]time.Time), } m.lifecycleCtx = context.Background() m.mainSessionID.Store(mainSessionUnset) diff --git a/server/lib/cdpmonitor/monitor_test.go b/server/lib/cdpmonitor/monitor_test.go index daa5d6ff..483f13b1 100644 --- a/server/lib/cdpmonitor/monitor_test.go +++ b/server/lib/cdpmonitor/monitor_test.go @@ -21,7 +21,7 @@ func TestLifecycle(t *testing.T) { ec := newEventCollector() upstream := newTestUpstream(srv.wsURL()) - m := New(upstream, ec.publishFn(), 99, discardLogger) + m := New(upstream, ec.publishFn(), 99, discardLogger, nil) assert.False(t, m.IsRunning(), "idle at boot") @@ -48,7 +48,7 @@ func TestReconnect(t *testing.T) { upstream := newTestUpstream(srv1.wsURL()) ec := newEventCollector() - m := New(upstream, ec.publishFn(), 99, discardLogger) + m := New(upstream, ec.publishFn(), 99, discardLogger, nil) require.NoError(t, m.Start(context.Background())) defer m.Stop() @@ -110,6 +110,16 @@ func TestScreenshot(t *testing.T) { m.tryScreenshot(context.Background(), "Page.loadEventFired", "") require.Eventually(t, func() bool { return captureCount.Load() > before }, 2*time.Second, 20*time.Millisecond) }) + + t.Run("skips_capture_when_screenshot_disabled", func(t *testing.T) { + m.screenshotEnabled = func() bool { return false } + defer func() { m.screenshotEnabled = nil }() + m.lastScreenshotAt.Store(time.Now().Add(-3 * time.Second).UnixMilli()) + before := captureCount.Load() + m.tryScreenshot(context.Background(), "Page.loadEventFired", "") + time.Sleep(100 * time.Millisecond) + assert.Equal(t, before, captureCount.Load(), "no ffmpeg capture when screenshot category is disabled") + }) } // TestFailPendingCommandsUnblocksSend verifies that clearState (called during @@ -117,7 +127,7 @@ func TestScreenshot(t *testing.T) { func TestFailPendingCommandsUnblocksSend(t *testing.T) { ec := newEventCollector() upstream := newTestUpstream("ws://127.0.0.1:0") - m := New(upstream, ec.publishFn(), 0, discardLogger) + m := New(upstream, ec.publishFn(), 0, discardLogger, nil) // Pre-register a fake pending command channel as if send() had registered it. id := int64(42) @@ -152,7 +162,7 @@ func TestInitSessionAutoAttachFailure(t *testing.T) { ec := newEventCollector() upstream := newTestUpstream(srv.wsURL()) - m := New(upstream, ec.publishFn(), 99, discardLogger) + m := New(upstream, ec.publishFn(), 99, discardLogger, nil) require.NoError(t, m.Start(context.Background())) defer m.Stop() @@ -178,7 +188,7 @@ func TestAutoAttach(t *testing.T) { ec := newEventCollector() upstream := newTestUpstream(srv.wsURL()) - m := New(upstream, ec.publishFn(), 99, discardLogger) + m := New(upstream, ec.publishFn(), 99, discardLogger, nil) require.NoError(t, m.Start(context.Background())) defer m.Stop() diff --git a/server/lib/cdpmonitor/screenshot.go b/server/lib/cdpmonitor/screenshot.go index 71f795b0..87809aee 100644 --- a/server/lib/cdpmonitor/screenshot.go +++ b/server/lib/cdpmonitor/screenshot.go @@ -20,6 +20,12 @@ import ( // sourceEvent is the CDP event that triggered the capture; sessionID is used // to snapshot nav context before the async goroutine fires. func (m *Monitor) tryScreenshot(ctx context.Context, sourceEvent, sessionID string) { + // Skip the ffmpeg capture entirely when the screenshot category is not + // captured; otherwise the frame would be taken only to be dropped at the + // telemetry filter. + if m.screenshotEnabled != nil && !m.screenshotEnabled() { + return + } now := time.Now().UnixMilli() last := m.lastScreenshotAt.Load() if now-last < 2000 { diff --git a/server/lib/telemetry/telemetry.go b/server/lib/telemetry/telemetry.go index 2e668f84..576c8ecd 100644 --- a/server/lib/telemetry/telemetry.go +++ b/server/lib/telemetry/telemetry.go @@ -151,6 +151,18 @@ func (s *TelemetrySession) UpdateConfig(cfg TelemetryConfig) { s.categories = categorySet(cfg.Categories) } +// CategoryEnabled reports whether events in category c are currently captured. +// It returns false when no session is active. +func (s *TelemetrySession) CategoryEnabled(c oapi.TelemetryEventCategory) bool { + s.mu.Lock() + defer s.mu.Unlock() + if s.id == "" { + return false + } + _, ok := s.categories[c] + return ok +} + // Active reports whether a telemetry session is currently running. func (s *TelemetrySession) Active() bool { s.mu.Lock()