Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
5659a6a
Split platform-induced API calls out of the control telemetry category
archandatta Jul 31, 2026
02ad34d
Report browser-control CDP commands in the control telemetry category
archandatta Aug 4, 2026
3295be5
Emit platform_api_call when only platform is captured
archandatta Aug 10, 2026
0fb09d7
telemetry: make the category gate lock-free
archandatta Aug 11, 2026
c16c6ec
wsproxy: add a post-forward message observer
archandatta Aug 11, 2026
29df8a1
oapi: regenerate from the spec
archandatta Aug 11, 2026
c5f5774
devtoolsproxy: classify cdp commands off the forwarding path
archandatta Aug 11, 2026
7a91278
devtoolsproxy: emit one event per forwarded control command
archandatta Aug 11, 2026
5519b42
devtoolsproxy: sanitize cdp command arguments per method
archandatta Aug 11, 2026
0a64ebd
Marshal the platform payload type for platform_api_call
archandatta Aug 11, 2026
b42f24c
telemetry: allow excluding cdp methods from control events
archandatta Aug 11, 2026
33995b6
events: size the ring for cdp command volume and surface drops
archandatta Aug 11, 2026
d23b7f3
devtoolsproxy: test the safety invariants telemetry has to hold
archandatta Aug 11, 2026
d6869a3
api: cover the cdp exclusion config round trip
archandatta Aug 11, 2026
bc11737
devtoolsproxy: report the url scheme without the host
archandatta Aug 11, 2026
5ed59e2
telemetry: keep the new drop counters optional in the schema
archandatta Aug 11, 2026
6c5f06e
devtoolsproxy: drop script_present from Page.reload
archandatta Aug 11, 2026
f7e92ae
devtoolsproxy: decide a frame's method before copying its arguments
archandatta Aug 11, 2026
52ebb0b
devtoolsproxy: make the escaped-method test actually escape
archandatta Aug 11, 2026
874b52f
Merge remote-tracking branch 'origin/main' into sync-base
archandatta Aug 11, 2026
e54dc36
Merge branch 'archand/kernel-1870/telemetry-control-platform-split'
archandatta Aug 11, 2026
612205f
devtoolsproxy: count frames queued after the worker stops
archandatta Aug 11, 2026
0556175
Merge branch 'main' into archand/kernel-1870/cdp-control-telemetry
archandatta Aug 13, 2026
c1a7074
events: bound the ring by bytes as well as envelopes
archandatta Aug 13, 2026
71eabda
devtoolsproxy: bound the values a client can put in an event
archandatta Aug 13, 2026
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
3 changes: 3 additions & 0 deletions server/cmd/api/api/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,9 @@ func (s *ApiService) StreamTelemetryEvents(ctx context.Context, req oapi.StreamT
}

if result.Dropped > 0 {
// The client's next Last-Event-ID will skip this gap, so record it:
// a silent skip reads as a quiet stream rather than a lost one.
s.telemetrySession.RecordDropped(result.Dropped)
continue
}

Expand Down
95 changes: 71 additions & 24 deletions server/cmd/api/api/telemetry.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func (s *ApiService) PutTelemetry(ctx context.Context, req oapi.PutTelemetryRequ
s.telemetrySession.Stop()
s.stopTelemetryState()
}
return oapi.PutTelemetry200JSONResponse(oapi.TelemetryState{Config: disabledConfig(), Seq: int64(s.telemetrySession.Seq())}), nil
return oapi.PutTelemetry200JSONResponse(s.stoppedTelemetryResponse()), nil
}

// Commit the config first so the filter is live before the collector emits,
Expand Down Expand Up @@ -102,7 +102,7 @@ func (s *ApiService) PatchTelemetry(ctx context.Context, req oapi.PatchTelemetry
if allDisabled {
s.telemetrySession.Stop()
s.stopTelemetryState()
return oapi.PatchTelemetry200JSONResponse(oapi.TelemetryState{Config: disabledConfig(), Seq: int64(s.telemetrySession.Seq())}), nil
return oapi.PatchTelemetry200JSONResponse(s.stoppedTelemetryResponse()), nil
}

// Commit first so the filter is live before the collector emits, then
Expand Down Expand Up @@ -199,37 +199,69 @@ func (s *ApiService) stopTelemetryState() {
// buildTelemetryResponse constructs a TelemetryState response from the current configuration.
func (s *ApiService) buildTelemetryResponse() oapi.TelemetryState {
resp := oapi.TelemetryState{
Config: telemetryConfigToOAPI(s.telemetrySession.Config()),
Seq: int64(s.telemetrySession.Seq()),
Config: telemetryConfigToOAPI(s.telemetrySession.Config()),
Seq: int64(s.telemetrySession.Seq()),
DroppedEvents: lo.ToPtr(int64(s.telemetrySession.DroppedEvents())),
}
if appliedAt := s.telemetrySession.AppliedAt(); !appliedAt.IsZero() {
resp.AppliedAt = &appliedAt
}
return resp
}

// categoryField pairs a category with its config field so the helpers can walk
// the configurable categories without enumerating them inline.
// stoppedTelemetryResponse reports the cleared configuration. Seq and the
// dropped count are process-scoped, so they survive a session ending.
func (s *ApiService) stoppedTelemetryResponse() oapi.TelemetryState {
return oapi.TelemetryState{
Config: disabledConfig(),
Seq: int64(s.telemetrySession.Seq()),
DroppedEvents: lo.ToPtr(int64(s.telemetrySession.DroppedEvents())),
}
}

// categoryField pairs a category with its enabled flag so the helpers can walk
// the configurable categories without enumerating them inline. The flag rather
// than the config, because control carries settings the others do not.
type categoryField struct {
category oapi.TelemetryEventCategory
config *oapi.BrowserTelemetryCategoryConfig
enabled *bool
}

func categoryFields(b *oapi.BrowserTelemetryCategoriesConfig) []categoryField {
flag := func(c *oapi.BrowserTelemetryCategoryConfig) *bool {
if c == nil {
return nil
}
return c.Enabled
}
var control *bool
if b.Control != nil {
control = b.Control.Enabled
}
return []categoryField{
{events.Console, b.Console},
{events.Network, b.Network},
{events.Page, b.Page},
{events.Interaction, b.Interaction},
{events.Control, b.Control},
{events.Platform, b.Platform},
{events.Connection, b.Connection},
{events.System, b.System},
{events.Screenshot, b.Screenshot},
{events.Captcha, b.Captcha},
{events.Console, flag(b.Console)},
{events.Network, flag(b.Network)},
{events.Page, flag(b.Page)},
{events.Interaction, flag(b.Interaction)},
{events.Control, control},
{events.Platform, flag(b.Platform)},
{events.Connection, flag(b.Connection)},
{events.System, flag(b.System)},
{events.Screenshot, flag(b.Screenshot)},
{events.Captcha, flag(b.Captcha)},
}
}

// excludedCdpMethodsFromOAPI reads the cdp_command exclusion list, which only
// the control category carries.
func excludedCdpMethodsFromOAPI(cfg *oapi.BrowserTelemetryConfig) []oapi.BrowserCdpCommandMethod {
if cfg == nil || cfg.Browser == nil || cfg.Browser.Control == nil ||
cfg.Browser.Control.Cdp == nil || cfg.Browser.Control.Cdp.ExcludedMethods == nil {
return nil
}
return *cfg.Browser.Control.Cdp.ExcludedMethods
}

func categorySetOf(cats []oapi.TelemetryEventCategory) map[oapi.TelemetryEventCategory]bool {
set := make(map[oapi.TelemetryEventCategory]bool, len(cats))
for _, c := range cats {
Expand Down Expand Up @@ -262,14 +294,18 @@ func telemetryConfigFromOAPI(cfg *oapi.BrowserTelemetryConfig) (telemetry.Teleme

cats := make([]oapi.TelemetryEventCategory, 0, len(events.UserCategories))
for _, f := range categoryFields(cfg.Browser) {
if f.config != nil && f.config.Enabled != nil && *f.config.Enabled {
if f.enabled != nil && *f.enabled {
cats = append(cats, f.category)
}
}
if len(cats) == 0 {
return telemetry.TelemetryConfig{}, true, nil
}
return telemetry.TelemetryConfig{Categories: cats, ExportOTLP: exportOTLP}, false, nil
return telemetry.TelemetryConfig{
Categories: cats,
ExportOTLP: exportOTLP,
ExcludedCdpMethods: excludedCdpMethodsFromOAPI(cfg),
}, false, nil
}

// exportOTLPFromOAPI reads the OTLP export toggle from a config, defaulting to
Expand All @@ -295,10 +331,10 @@ func mergeTelemetryConfig(current telemetry.TelemetryConfig, patch *oapi.Browser

if patch.Browser != nil {
for _, f := range categoryFields(patch.Browser) {
if f.config == nil || f.config.Enabled == nil {
if f.enabled == nil {
continue // not mentioned in patch; keep current state
}
if *f.config.Enabled {
if *f.enabled {
active[f.category] = struct{}{}
} else {
delete(active, f.category)
Expand All @@ -312,14 +348,21 @@ func mergeTelemetryConfig(current telemetry.TelemetryConfig, patch *oapi.Browser
exportOTLP = *patch.Export.Otlp.Enabled
}

// So do the cdp_command exclusions: an omitted list is unchanged, an empty
// one clears them.
excluded := current.ExcludedCdpMethods
if patched := excludedCdpMethodsFromOAPI(patch); patched != nil {
excluded = patched
}

if len(active) == 0 {
return telemetry.TelemetryConfig{}, true
}
cats := make([]oapi.TelemetryEventCategory, 0, len(active))
for c := range active {
cats = append(cats, c)
}
return telemetry.TelemetryConfig{Categories: cats, ExportOTLP: exportOTLP}, false
return telemetry.TelemetryConfig{Categories: cats, ExportOTLP: exportOTLP, ExcludedCdpMethods: excluded}, false
}

// disabledConfig returns a BrowserTelemetryConfig with every configurable category explicitly disabled.
Expand All @@ -333,7 +376,7 @@ func disabledConfig() oapi.BrowserTelemetryConfig {
Network: off(),
Page: off(),
Interaction: off(),
Control: off(),
Control: &oapi.BrowserTelemetryControlConfig{Enabled: lo.ToPtr(false)},
Platform: off(),
Connection: off(),
System: off(),
Expand All @@ -359,13 +402,17 @@ func telemetryConfigToOAPI(cfg telemetry.TelemetryConfig) oapi.BrowserTelemetryC
on := active[cat]
return &oapi.BrowserTelemetryCategoryConfig{Enabled: &on}
}
control := &oapi.BrowserTelemetryControlConfig{Enabled: lo.ToPtr(active[events.Control])}
if len(cfg.ExcludedCdpMethods) > 0 {
control.Cdp = &oapi.BrowserTelemetryCdpControlConfig{ExcludedMethods: &cfg.ExcludedCdpMethods}
}
return oapi.BrowserTelemetryConfig{
Browser: &oapi.BrowserTelemetryCategoriesConfig{
Console: enabled(events.Console),
Network: enabled(events.Network),
Page: enabled(events.Page),
Interaction: enabled(events.Interaction),
Control: enabled(events.Control),
Control: control,
Platform: enabled(events.Platform),
Connection: enabled(events.Connection),
System: enabled(events.System),
Expand Down
88 changes: 84 additions & 4 deletions server/cmd/api/api/telemetry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
oapi "github.com/kernel/kernel-images/server/lib/oapi"
"github.com/kernel/kernel-images/server/lib/recorder"
"github.com/kernel/kernel-images/server/lib/scaletozero"
"github.com/samber/lo"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
Expand All @@ -28,7 +29,7 @@ func allCategoriesDisabled() *oapi.BrowserTelemetryCategoriesConfig {
Network: off(),
Page: off(),
Interaction: off(),
Control: off(),
Control: &oapi.BrowserTelemetryControlConfig{Enabled: lo.ToPtr(false)},
Platform: off(),
Connection: off(),
System: off(),
Expand Down Expand Up @@ -207,7 +208,7 @@ func TestTelemetryHandlersDriveMiddlewareToggle(t *testing.T) {
_, err := svc.PutTelemetry(ctx, oapi.PutTelemetryRequestObject{
Body: &oapi.BrowserTelemetryConfig{
Browser: &oapi.BrowserTelemetryCategoriesConfig{
Control: &oapi.BrowserTelemetryCategoryConfig{Enabled: &tr},
Control: &oapi.BrowserTelemetryControlConfig{Enabled: &tr},
},
},
})
Expand All @@ -217,7 +218,7 @@ func TestTelemetryHandlersDriveMiddlewareToggle(t *testing.T) {
_, err = svc.PatchTelemetry(ctx, oapi.PatchTelemetryRequestObject{
Body: &oapi.BrowserTelemetryConfig{
Browser: &oapi.BrowserTelemetryCategoriesConfig{
Control: &oapi.BrowserTelemetryCategoryConfig{Enabled: &f},
Control: &oapi.BrowserTelemetryControlConfig{Enabled: &f},
},
},
})
Expand Down Expand Up @@ -254,7 +255,7 @@ func TestTelemetryHandlersEnableMiddlewareForPlatformOnly(t *testing.T) {
_, err = svc.PatchTelemetry(ctx, oapi.PatchTelemetryRequestObject{
Body: &oapi.BrowserTelemetryConfig{
Browser: &oapi.BrowserTelemetryCategoriesConfig{
Control: &oapi.BrowserTelemetryCategoryConfig{Enabled: &f},
Control: &oapi.BrowserTelemetryControlConfig{Enabled: &f},
},
},
})
Expand Down Expand Up @@ -293,6 +294,10 @@ func TestGetTelemetry(t *testing.T) {
r200, ok := resp.(oapi.GetTelemetry200JSONResponse)
require.True(t, ok)
assert.Equal(t, started.Config, r200.Config)
// Optional in the schema so an older image's response still validates,
// but always set here: absent would mean "not reported", not zero.
require.NotNil(t, r200.DroppedEvents)
assert.Zero(t, *r200.DroppedEvents)
})
}

Expand Down Expand Up @@ -624,3 +629,78 @@ func (e *blockingStopExporter) Running() bool {
defer e.mu.Unlock()
return e.running
}

func TestCdpExcludedMethodsRoundTrip(t *testing.T) {
ctx := context.Background()
excluded := []oapi.BrowserCdpCommandMethod{"Input.dispatchMouseEvent", "Page.captureScreenshot"}
withExclusions := func() *oapi.BrowserTelemetryConfig {
return &oapi.BrowserTelemetryConfig{
Browser: &oapi.BrowserTelemetryCategoriesConfig{
Control: &oapi.BrowserTelemetryControlConfig{
Enabled: lo.ToPtr(true),
Cdp: &oapi.BrowserTelemetryCdpControlConfig{ExcludedMethods: &excluded},
},
},
}
}

t.Run("put stores them and the session exposes them to the proxy", func(t *testing.T) {
svc := newTestService(t, newMockRecordManager())
resp, err := svc.PutTelemetry(ctx, oapi.PutTelemetryRequestObject{Body: withExclusions()})
require.NoError(t, err)
created := resp.(oapi.PutTelemetry201JSONResponse)
require.NotNil(t, created.Config.Browser.Control.Cdp)
assert.Equal(t, excluded, *created.Config.Browser.Control.Cdp.ExcludedMethods)

// The proxy reads this set per command, so it has to reflect the config.
assert.Equal(t, map[string]struct{}{
"Input.dispatchMouseEvent": {},
"Page.captureScreenshot": {},
}, svc.telemetrySession.ExcludedCdpMethods())
})

t.Run("patch leaves an omitted list alone and an empty list clears it", func(t *testing.T) {
svc := newTestService(t, newMockRecordManager())
_, err := svc.PutTelemetry(ctx, oapi.PutTelemetryRequestObject{Body: withExclusions()})
require.NoError(t, err)

// Category toggle only: the exclusions are not mentioned, so they stand.
_, err = svc.PatchTelemetry(ctx, oapi.PatchTelemetryRequestObject{Body: &oapi.BrowserTelemetryConfig{
Browser: &oapi.BrowserTelemetryCategoriesConfig{
System: &oapi.BrowserTelemetryCategoryConfig{Enabled: lo.ToPtr(true)},
},
}})
require.NoError(t, err)
assert.Len(t, svc.telemetrySession.ExcludedCdpMethods(), 2)

empty := []oapi.BrowserCdpCommandMethod{}
_, err = svc.PatchTelemetry(ctx, oapi.PatchTelemetryRequestObject{Body: &oapi.BrowserTelemetryConfig{
Browser: &oapi.BrowserTelemetryCategoriesConfig{
Control: &oapi.BrowserTelemetryControlConfig{
Cdp: &oapi.BrowserTelemetryCdpControlConfig{ExcludedMethods: &empty},
},
},
}})
require.NoError(t, err)
assert.Empty(t, svc.telemetrySession.ExcludedCdpMethods())
})
}

// dropped_events was added to TelemetryState after it shipped, so it stays
// optional: a response from an image that predates it must still decode, and
// an old client's control block must still be a valid request.
func TestTelemetryStateStaysCompatibleWithOlderImages(t *testing.T) {
var state oapi.TelemetryState
err := json.Unmarshal([]byte(`{"config":{},"seq":42}`), &state)
require.NoError(t, err)
assert.Nil(t, state.DroppedEvents, "absent means not reported, which is not zero")
assert.EqualValues(t, 42, state.Seq)

// A client that predates control.cdp sends only enabled, and still parses.
var cfg oapi.BrowserTelemetryConfig
err = json.Unmarshal([]byte(`{"browser":{"control":{"enabled":true}}}`), &cfg)
require.NoError(t, err)
require.NotNil(t, cfg.Browser.Control)
assert.True(t, *cfg.Browser.Control.Enabled)
assert.Nil(t, cfg.Browser.Control.Cdp)
}
11 changes: 9 additions & 2 deletions server/cmd/api/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,12 @@ func main() {
}

// Construct events pipeline
// Sized for the control stream's event rate rather than the operational
// signals it started with: browser-control CDP commands are one event per
// keystroke and two per click, so a form-filling session produces thousands
// where a session used to produce tens.
eventStream, err := events.NewEventStream(events.EventStreamConfig{
RingCapacity: 1024,
RingCapacity: 8192,
})
if err != nil {
slogger.Error("failed to create event stream", "err", err)
Expand Down Expand Up @@ -321,8 +325,11 @@ func main() {
rDevtools.Get("/json/", jsonTargetHandler)
rDevtools.Get("/json/list", jsonTargetHandler)
rDevtools.Get("/json/list/", jsonTargetHandler)
// Checked once per forwarded client frame, so it reads the session's
// lock-free view rather than taking the telemetry lock.
controlEnabled := func() bool { return telemetrySession.CategoryEnabled(events.Control) }
rDevtools.Get("/*", func(w http.ResponseWriter, r *http.Request) {
devtoolsproxy.WebSocketProxyHandler(upstreamMgr, slogger, config.LogCDPMessages, stz, telemetrySession.Publish, wsRegistry).ServeHTTP(w, r)
devtoolsproxy.WebSocketProxyHandler(upstreamMgr, slogger, config.LogCDPMessages, stz, telemetrySession.Publish, controlEnabled, telemetrySession.ExcludedCdpMethods, wsRegistry).ServeHTTP(w, r)
})

srvDevtools := &http.Server{
Expand Down
2 changes: 1 addition & 1 deletion server/e2e/e2e_otlp_storage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ func enableControlExport(t *testing.T, ctx context.Context, client *instanceoapi
tr := true
resp, err := client.PutTelemetryWithResponse(ctx, instanceoapi.PutTelemetryJSONRequestBody{
Browser: &instanceoapi.BrowserTelemetryCategoriesConfig{
Control: &instanceoapi.BrowserTelemetryCategoryConfig{Enabled: &tr},
Control: &instanceoapi.BrowserTelemetryControlConfig{Enabled: &tr},
},
Export: &instanceoapi.BrowserTelemetryExportConfig{
Otlp: &instanceoapi.BrowserTelemetryOTLPExportConfig{Enabled: &tr},
Expand Down
Loading
Loading