Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 4 additions & 8 deletions server/cmd/api/api/telemetry.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,8 +199,9 @@ func containsCategory(cats []oapi.TelemetryEventCategory, target oapi.TelemetryE
}

// 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.
// Selection is opt-in: with no browser config the default set is used; with a browser config only
// the categories explicitly enabled there are captured (anything omitted is off). Returns the
// config, whether the result is empty (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
Expand All @@ -209,14 +210,9 @@ func telemetryConfigFromOAPI(cfg *oapi.BrowserTelemetryConfig) (telemetry.Teleme
return telemetry.TelemetryConfig{Categories: cats}, false, nil
}

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 {
if f.config != nil && f.config.Enabled != nil && *f.config.Enabled {
cats = append(cats, f.category)
}
}
Expand Down
89 changes: 55 additions & 34 deletions server/cmd/api/api/telemetry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package api

import (
"context"
"encoding/json"
"errors"
"testing"

Expand Down Expand Up @@ -48,71 +49,83 @@ func TestTelemetryConfigFromOAPI(t *testing.T) {
assert.ElementsMatch(t, events.DefaultCategories, cfg.Categories)
})

t.Run("omitted enabled resolves to default state", func(t *testing.T) {
t.Run("opt-in captures exactly the enabled categories", func(t *testing.T) {
tr := true
cfg, allDisabled, err := telemetryConfigFromOAPI(&oapi.BrowserTelemetryConfig{
Browser: &oapi.BrowserTelemetryCategoriesConfig{
Console: &oapi.BrowserTelemetryCategoryConfig{}, // Enabled nil → default state (on)
Console: &oapi.BrowserTelemetryCategoryConfig{Enabled: &tr},
Network: &oapi.BrowserTelemetryCategoryConfig{Enabled: &tr},
},
})
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)
assert.ElementsMatch(t, []oapi.TelemetryEventCategory{events.Console, events.Network}, cfg.Categories)
})

t.Run("screenshot is opt-in", func(t *testing.T) {
t.Run("omitted category is off (opt-in)", func(t *testing.T) {
tr := true
cfg, _, err := telemetryConfigFromOAPI(&oapi.BrowserTelemetryConfig{
Browser: &oapi.BrowserTelemetryCategoriesConfig{
Screenshot: &oapi.BrowserTelemetryCategoryConfig{Enabled: &tr},
Console: &oapi.BrowserTelemetryCategoryConfig{Enabled: &tr},
},
})
require.NoError(t, err)
assert.Contains(t, cfg.Categories, events.Screenshot)
// Only console is enabled; default-bundle categories are not added in.
assert.Equal(t, []oapi.TelemetryEventCategory{events.Console}, cfg.Categories)
})

t.Run("all configurable categories false returns allDisabled=true", func(t *testing.T) {
t.Run("enabled:nil is treated as off", func(t *testing.T) {
_, allDisabled, err := telemetryConfigFromOAPI(&oapi.BrowserTelemetryConfig{
Browser: allCategoriesDisabled(),
Browser: &oapi.BrowserTelemetryCategoriesConfig{
Console: &oapi.BrowserTelemetryCategoryConfig{}, // Enabled nil → off
},
})
require.NoError(t, err)
assert.True(t, allDisabled)
assert.True(t, allDisabled, "a browser config that enables nothing clears telemetry")
})

t.Run("disabling only the default-on categories does not clear", func(t *testing.T) {
f := false
_, allDisabled, err := telemetryConfigFromOAPI(&oapi.BrowserTelemetryConfig{
t.Run("screenshot is opt-in", func(t *testing.T) {
tr := true
cfg, _, err := telemetryConfigFromOAPI(&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},
Screenshot: &oapi.BrowserTelemetryCategoryConfig{Enabled: &tr},
},
})
require.NoError(t, err)
// control/connection/system/captcha remain at their default-on state.
assert.False(t, allDisabled)
assert.Contains(t, cfg.Categories, events.Screenshot)
})

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{
Console: &oapi.BrowserTelemetryCategoryConfig{Enabled: &tr},
Network: &oapi.BrowserTelemetryCategoryConfig{Enabled: &f},
},
t.Run("empty browser config clears", func(t *testing.T) {
_, allDisabled, err := telemetryConfigFromOAPI(&oapi.BrowserTelemetryConfig{
Browser: &oapi.BrowserTelemetryCategoriesConfig{},
})
require.NoError(t, err)
assert.False(t, allDisabled)
// 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)
assert.True(t, allDisabled)
})
}

func TestPutTelemetryIgnoresUnknownCategory(t *testing.T) {
// Forward-compat: a newer control plane may send a telemetry category this
// image does not yet know. The strict handler decodes the body with
// encoding/json (no DisallowUnknownFields), so an unknown category must be
// ignored, not rejected, and known categories must still apply.
ctx := context.Background()
svc := newTestService(t, newMockRecordManager())

var body oapi.PutTelemetryJSONRequestBody
raw := []byte(`{"browser":{"console":{"enabled":true},"future_category":{"enabled":true}}}`)
require.NoError(t, json.Unmarshal(raw, &body))

resp, err := svc.PutTelemetry(ctx, oapi.PutTelemetryRequestObject{Body: &body})
require.NoError(t, err)
r201, ok := resp.(oapi.PutTelemetry201JSONResponse)
require.True(t, ok, "expected 201, got %T", resp)
require.NotNil(t, r201.Config.Browser)
require.NotNil(t, r201.Config.Browser.Console)
require.NotNil(t, r201.Config.Browser.Console.Enabled)
assert.True(t, *r201.Config.Browser.Console.Enabled, "known category should be captured")
}

func TestPutTelemetry(t *testing.T) {
ctx := context.Background()

Expand Down Expand Up @@ -373,7 +386,15 @@ func TestTelemetryCollectorFailureLeavesConfigUnchanged(t *testing.T) {
svc := newTestService(t, newMockRecordManager())
svc.cdpMonitor = &failingCdpMonitor{}

resp, err := svc.PutTelemetry(ctx, oapi.PutTelemetryRequestObject{})
// Enable a CDP category so the (failing) collector start is attempted.
tr := true
resp, err := svc.PutTelemetry(ctx, oapi.PutTelemetryRequestObject{
Body: &oapi.BrowserTelemetryConfig{
Browser: &oapi.BrowserTelemetryCategoriesConfig{
Console: &oapi.BrowserTelemetryCategoryConfig{Enabled: &tr},
},
},
})
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")
Expand Down
9 changes: 3 additions & 6 deletions server/lib/events/event.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,10 @@ var UserCategories = []oapi.TelemetryEventCategory{
}

// 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.
// per-category settings: the lightweight operational signals. CDP categories
// (console/network/page/interaction) and screenshot are excluded so the default
// never starts the CDP collector or emits high-volume streams; they are opt-in.
var DefaultCategories = []oapi.TelemetryEventCategory{
Console,
Network,
Page,
Interaction,
Control,
Connection,
System,
Expand Down
Loading
Loading