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/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/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..8f1f905e 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,40 @@ 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.stopTelemetryState() } - // 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 } + // 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 { - // Replace config on the running session. + prev = s.telemetrySession.Config() 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.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 } - 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,38 +80,70 @@ 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 + } + + 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 } + // 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 + } 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() { - if !s.telemetrySession.Active() { +// 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. +// 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(): + return s.cdpMonitor.Start(s.lifecycleCtx) + case !events.HasCDPCategory(cats) && s.cdpMonitor.IsRunning(): + s.cdpMonitor.Stop() + } + return nil +} + +// 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 } - for _, c := range s.telemetrySession.Config().Categories { - if c == events.Api { - EnableTelemetryMiddleware() - return - } + s.telemetrySession.UpdateConfig(prev) + _ = s.reconcileTelemetryState(prev.Categories) +} + +// stopTelemetryState tears down the collector and middleware after a session is +// cleared. +func (s *ApiService) stopTelemetryState() { + if s.cdpMonitor.IsRunning() { + s.cdpMonitor.Stop() } DisableTelemetryMiddleware() } @@ -132,101 +160,98 @@ 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 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 } - 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 +259,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..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" @@ -12,33 +13,74 @@ 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 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 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 false returns allDisabled=true", func(t *testing.T) { + 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("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 +88,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 +105,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 +165,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 +174,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 +191,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 +293,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 +302,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 +311,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) @@ -349,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") + }) +} 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/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 dbc15dbb..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) @@ -398,7 +401,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 +502,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 +536,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 +561,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..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() @@ -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) @@ -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 72b7b007..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 { @@ -85,7 +91,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..576c8ecd 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{}{} } - return &TelemetrySession{es: es, categories: cats} + if events.HasCDPCategory(cats) { + set[events.Monitor] = struct{}{} + } + 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,19 @@ 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 = 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 } - s.categories[events.System] = struct{}{} + _, ok := s.categories[c] + return ok } // 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) + } +}