diff --git a/server/cmd/api/api/middleware.go b/server/cmd/api/api/middleware.go new file mode 100644 index 00000000..8578b8f1 --- /dev/null +++ b/server/cmd/api/api/middleware.go @@ -0,0 +1,88 @@ +package api + +import ( + "context" + "encoding/json" + "net/http" + "sync/atomic" + "time" + + chiMiddleware "github.com/go-chi/chi/v5/middleware" + + "github.com/kernel/kernel-images/server/lib/events" + oapi "github.com/kernel/kernel-images/server/lib/oapi" +) + +// Per-request scratch shared between the chi-level HTTP middleware and the +// strict-server middleware so the latter can stamp the matched operationId. +type telemetryCtxKey struct{} + +type telemetryRequestCtx struct { + operationID string +} + +// Process-wide toggle for the api_call middleware. Flipped by +// Enable/DisableTelemetryMiddleware; both middleware layers short-circuit +// to passthroughs when false. +var telemetryMiddlewareEnabled atomic.Bool + +// EnableTelemetryMiddleware turns on api_call event emission. +func EnableTelemetryMiddleware() { telemetryMiddlewareEnabled.Store(true) } + +// DisableTelemetryMiddleware turns api_call event emission off. +func DisableTelemetryMiddleware() { telemetryMiddlewareEnabled.Store(false) } + +// TelemetryMiddlewareEnabled reports the current state. +func TelemetryMiddlewareEnabled() bool { return telemetryMiddlewareEnabled.Load() } + +// TelemetryHTTPMiddleware emits a BrowserApiCallEvent per documented operation, +// capturing the final status and wall-clock duration. +func TelemetryHTTPMiddleware(publish func(events.Event)) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if !telemetryMiddlewareEnabled.Load() { + next.ServeHTTP(w, r) + return + } + tc := &telemetryRequestCtx{} + ctx := context.WithValue(r.Context(), telemetryCtxKey{}, tc) + ww := chiMiddleware.NewWrapResponseWriter(w, r.ProtoMajor) + start := time.Now() + + next.ServeHTTP(ww, r.WithContext(ctx)) + + if tc.operationID == "" { + return + } + data, _ := json.Marshal(oapi.BrowserApiCallEventData{ + RequestId: chiMiddleware.GetReqID(ctx), + OperationId: tc.operationID, + Status: ww.Status(), + DurationMs: float32(time.Since(start).Microseconds()) / 1000.0, + }) + publish(events.Event{ + Ts: time.Now().UnixMicro(), + Type: "api_call", + Category: events.Api, + Source: oapi.BrowserEventSource{Kind: oapi.KernelApi}, + Data: data, + }) + }) + } +} + +// TelemetryStrictMiddleware records the matched OpenAPI operationId onto the +// per-request scratch so TelemetryHTTPMiddleware can include it in the event. +func TelemetryStrictMiddleware() oapi.StrictMiddlewareFunc { + return func(next oapi.StrictHandlerFunc, operationID string) oapi.StrictHandlerFunc { + return func(ctx context.Context, w http.ResponseWriter, r *http.Request, request any) (any, error) { + if !telemetryMiddlewareEnabled.Load() { + return next(ctx, w, r, request) + } + if tc, ok := ctx.Value(telemetryCtxKey{}).(*telemetryRequestCtx); ok { + tc.operationID = operationID + } + return next(ctx, w, r, request) + } + } +} diff --git a/server/cmd/api/api/middleware_test.go b/server/cmd/api/api/middleware_test.go new file mode 100644 index 00000000..c1466496 --- /dev/null +++ b/server/cmd/api/api/middleware_test.go @@ -0,0 +1,149 @@ +package api + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "sync" + "testing" + + chiMiddleware "github.com/go-chi/chi/v5/middleware" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/kernel/kernel-images/server/lib/events" + oapi "github.com/kernel/kernel-images/server/lib/oapi" +) + +// recordingPublisher captures published events for assertion. +type recordingPublisher struct { + mu sync.Mutex + events []events.Event +} + +func (rp *recordingPublisher) publish(ev events.Event) { + rp.mu.Lock() + defer rp.mu.Unlock() + rp.events = append(rp.events, ev) +} + +func (rp *recordingPublisher) snapshot() []events.Event { + rp.mu.Lock() + defer rp.mu.Unlock() + out := make([]events.Event, len(rp.events)) + copy(out, rp.events) + return out +} + +// Mirrors the oapi-codegen strict dispatcher: middleware chain -> inner +// handler -> response write. +func fakeStrictHandler(operationID string, status int, mws []oapi.StrictMiddlewareFunc) http.Handler { + inner := oapi.StrictHandlerFunc(func(ctx context.Context, w http.ResponseWriter, r *http.Request, request any) (any, error) { + return nil, nil + }) + for _, mw := range mws { + inner = mw(inner, operationID) + } + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, _ = inner(r.Context(), w, r, nil) + w.WriteHeader(status) + }) +} + +// Flips the package-level toggle on for the test, restoring prior state +// via t.Cleanup. +func withTelemetryMiddlewareEnabled(t *testing.T) { + t.Helper() + prev := TelemetryMiddlewareEnabled() + EnableTelemetryMiddleware() + t.Cleanup(func() { + if prev { + EnableTelemetryMiddleware() + } else { + DisableTelemetryMiddleware() + } + }) +} + +func TestTelemetryMiddleware_EmitsApiCallEventOnDocumentedRoute(t *testing.T) { + withTelemetryMiddlewareEnabled(t) + rp := &recordingPublisher{} + chain := chiHandler(t, rp.publish, "ProcessExec", http.StatusOK) + + req := httptest.NewRequest(http.MethodPost, "/process/exec", nil) + rec := httptest.NewRecorder() + chain.ServeHTTP(rec, req) + + captured := rp.snapshot() + 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, oapi.KernelApi, ev.Source.Kind) + + var data struct { + RequestID string `json:"request_id"` + OperationID string `json:"operation_id"` + Status int `json:"status"` + DurationMs float64 `json:"duration_ms"` + } + require.NoError(t, json.Unmarshal(ev.Data, &data)) + assert.NotEmpty(t, data.RequestID, "request_id should be set by chi RequestID middleware") + assert.Equal(t, "ProcessExec", data.OperationID) + assert.Equal(t, http.StatusOK, data.Status) + assert.GreaterOrEqual(t, data.DurationMs, 0.0) +} + +func TestTelemetryMiddleware_CapturesNonOKStatus(t *testing.T) { + withTelemetryMiddlewareEnabled(t) + rp := &recordingPublisher{} + chain := chiHandler(t, rp.publish, "ProcessExec", http.StatusInternalServerError) + + req := httptest.NewRequest(http.MethodPost, "/process/exec", nil) + rec := httptest.NewRecorder() + chain.ServeHTTP(rec, req) + + captured := rp.snapshot() + require.Len(t, captured, 1) + var data struct { + Status int `json:"status"` + } + require.NoError(t, json.Unmarshal(captured[0].Data, &data)) + assert.Equal(t, http.StatusInternalServerError, data.Status) +} + +func TestTelemetryMiddleware_SkipsUndocumentedRoutes(t *testing.T) { + withTelemetryMiddlewareEnabled(t) + rp := &recordingPublisher{} + mw := TelemetryHTTPMiddleware(rp.publish) + plain := mw(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + + req := httptest.NewRequest(http.MethodGet, "/health", nil) + chiMiddleware.RequestID(plain).ServeHTTP(httptest.NewRecorder(), req) + + assert.Empty(t, rp.snapshot(), "no event should be emitted when operationId is unset") +} + +func TestTelemetryMiddleware_ShortCircuitsWhenDisabled(t *testing.T) { + DisableTelemetryMiddleware() + rp := &recordingPublisher{} + chain := chiHandler(t, rp.publish, "ProcessExec", http.StatusOK) + + req := httptest.NewRequest(http.MethodPost, "/process/exec", nil) + rec := httptest.NewRecorder() + chain.ServeHTTP(rec, req) + + assert.Empty(t, rp.snapshot(), "disabled middleware must not emit") +} + +// Builds the same middleware stack as main.go: RequestID -> HTTP middleware -> +// strict dispatch -> inner handler. +func chiHandler(t *testing.T, publish func(events.Event), operationID string, status int) http.Handler { + t.Helper() + inner := fakeStrictHandler(operationID, status, []oapi.StrictMiddlewareFunc{TelemetryStrictMiddleware()}) + telemetry := TelemetryHTTPMiddleware(publish)(inner) + return chiMiddleware.RequestID(telemetry) +} diff --git a/server/cmd/api/api/telemetry.go b/server/cmd/api/api/telemetry.go index a4b4cfe4..fd013b09 100644 --- a/server/cmd/api/api/telemetry.go +++ b/server/cmd/api/api/telemetry.go @@ -3,8 +3,9 @@ package api import ( "context" - "github.com/nrednav/cuid2" oapi "github.com/kernel/kernel-images/server/lib/oapi" + "github.com/nrednav/cuid2" + "github.com/samber/lo" "github.com/kernel/kernel-images/server/lib/events" "github.com/kernel/kernel-images/server/lib/logger" @@ -25,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 four categories to enabled:false clears the configuration (200). +// Setting all five categories 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() @@ -45,12 +46,14 @@ func (s *ApiService) PutTelemetry(ctx context.Context, req oapi.PutTelemetryRequ // All categories disabled: clear the configuration. s.cdpMonitor.Stop() s.telemetrySession.Stop() + s.applyTelemetryMiddlewareState() return oapi.PutTelemetry200JSONResponse(oapi.TelemetryState{Config: disabledConfig(), Seq: int64(s.telemetrySession.Seq())}), nil } if wasActive { // Replace config on the running session. s.telemetrySession.UpdateConfig(cfg) + s.applyTelemetryMiddlewareState() return oapi.PutTelemetry200JSONResponse(s.buildTelemetryResponse()), nil } @@ -61,16 +64,18 @@ func (s *ApiService) PutTelemetry(ctx context.Context, req oapi.PutTelemetryRequ 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) return oapi.PutTelemetry500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to start telemetry"}}, nil } + s.applyTelemetryMiddlewareState() return oapi.PutTelemetry201JSONResponse(s.buildTelemetryResponse()), nil } // PatchTelemetry handles PATCH /telemetry. // Partially updates the telemetry configuration. Returns 404 if not configured. -// Setting all four categories to enabled:false clears the configuration (200). +// Setting all five categories to enabled:false clears the configuration (200). func (s *ApiService) PatchTelemetry(_ context.Context, req oapi.PatchTelemetryRequestObject) (oapi.PatchTelemetryResponseObject, error) { s.monitorMu.Lock() defer s.monitorMu.Unlock() @@ -88,14 +93,33 @@ func (s *ApiService) PatchTelemetry(_ context.Context, req oapi.PatchTelemetryRe // 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() } 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() { + DisableTelemetryMiddleware() + return + } + for _, c := range s.telemetrySession.Config().Categories { + if c == events.Api { + EnableTelemetryMiddleware() + return + } + } + DisableTelemetryMiddleware() +} + // buildTelemetryResponse constructs a TelemetryState response from the current configuration. func (s *ApiService) buildTelemetryResponse() oapi.TelemetryState { resp := oapi.TelemetryState{ @@ -127,13 +151,14 @@ func telemetryConfigFromOAPI(cfg *oapi.BrowserTelemetryConfig) (telemetry.Teleme networkOn := isEnabled(b.Network) pageOn := isEnabled(b.Page) interactionOn := isEnabled(b.Interaction) + apiOn := isEnabled(b.Api) - allDisabled := !consoleOn && !networkOn && !pageOn && !interactionOn + allDisabled := !consoleOn && !networkOn && !pageOn && !interactionOn && !apiOn if allDisabled { return telemetry.TelemetryConfig{}, true, nil } - cats := make([]oapi.TelemetryEventCategory, 0, 5) + cats := make([]oapi.TelemetryEventCategory, 0, 6) if consoleOn { cats = append(cats, events.Console) } @@ -146,6 +171,9 @@ func telemetryConfigFromOAPI(cfg *oapi.BrowserTelemetryConfig) (telemetry.Teleme if interactionOn { cats = append(cats, events.Interaction) } + if apiOn { + cats = append(cats, events.Api) + } // CategorySystem is always appended by TelemetrySession.Start/UpdateConfig; // no need to include it here. return telemetry.TelemetryConfig{Categories: cats}, false, nil @@ -177,6 +205,7 @@ func mergeTelemetryConfig(current telemetry.TelemetryConfig, patch *oapi.Browser 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. @@ -185,6 +214,7 @@ func mergeTelemetryConfig(current telemetry.TelemetryConfig, patch *oapi.Browser events.Network, events.Page, events.Interaction, + events.Api, } allDisabled := true for _, c := range userCats { @@ -204,16 +234,15 @@ func mergeTelemetryConfig(current telemetry.TelemetryConfig, patch *oapi.Browser return telemetry.TelemetryConfig{Categories: cats}, false } -// disabledConfig returns a BrowserTelemetryConfig with all four user-facing categories explicitly disabled. +// disabledConfig returns a BrowserTelemetryConfig with all five user-facing categories explicitly disabled. func disabledConfig() oapi.BrowserTelemetryConfig { - f := false - cat := &oapi.BrowserTelemetryCategoryConfig{Enabled: &f} return oapi.BrowserTelemetryConfig{ Browser: &oapi.BrowserTelemetryCategoriesConfig{ - Console: cat, - Network: cat, - Page: cat, - Interaction: cat, + 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)}, }, } } @@ -238,7 +267,7 @@ func telemetryConfigToOAPI(cfg telemetry.TelemetryConfig) oapi.BrowserTelemetryC Network: enabled(events.Network), Page: enabled(events.Page), Interaction: enabled(events.Interaction), + Api: enabled(events.Api), }, } } - diff --git a/server/cmd/api/api/telemetry_test.go b/server/cmd/api/api/telemetry_test.go index 91df62e9..21975da9 100644 --- a/server/cmd/api/api/telemetry_test.go +++ b/server/cmd/api/api/telemetry_test.go @@ -46,6 +46,7 @@ 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) @@ -62,7 +63,7 @@ func TestTelemetryConfigFromOAPI(t *testing.T) { }) require.NoError(t, err) assert.False(t, allDisabled) - assert.Len(t, cfg.Categories, 3) // console + page + interaction (network=false, others default true) + assert.Len(t, cfg.Categories, 4) // console + page + interaction + api (network=false, others default true) }) } @@ -126,6 +127,7 @@ func TestPutTelemetry(t *testing.T) { Network: &oapi.BrowserTelemetryCategoryConfig{Enabled: &f}, Page: &oapi.BrowserTelemetryCategoryConfig{Enabled: &f}, Interaction: &oapi.BrowserTelemetryCategoryConfig{Enabled: &f}, + Api: &oapi.BrowserTelemetryCategoryConfig{Enabled: &f}, }, }, }) @@ -138,10 +140,54 @@ func TestPutTelemetry(t *testing.T) { 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.Nil(t, r200.AppliedAt, "applied_at must be omitted when telemetry is unconfigured") }) } +func TestTelemetryHandlersDriveMiddlewareToggle(t *testing.T) { + ctx := context.Background() + t.Cleanup(DisableTelemetryMiddleware) + + svc := newTestService(t, newMockRecordManager()) + + DisableTelemetryMiddleware() + tr, f := true, false + _, err := svc.PutTelemetry(ctx, oapi.PutTelemetryRequestObject{ + Body: &oapi.BrowserTelemetryConfig{ + Browser: &oapi.BrowserTelemetryCategoriesConfig{ + Api: &oapi.BrowserTelemetryCategoryConfig{Enabled: &tr}, + }, + }, + }) + require.NoError(t, err) + assert.True(t, TelemetryMiddlewareEnabled(), "PUT with api=true should enable middleware") + + _, err = svc.PatchTelemetry(ctx, oapi.PatchTelemetryRequestObject{ + Body: &oapi.BrowserTelemetryConfig{ + Browser: &oapi.BrowserTelemetryCategoriesConfig{ + Api: &oapi.BrowserTelemetryCategoryConfig{Enabled: &f}, + }, + }, + }) + require.NoError(t, err) + assert.False(t, TelemetryMiddlewareEnabled(), "PATCH api=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}, + }, + }, + }) + require.NoError(t, err) + assert.False(t, TelemetryMiddlewareEnabled(), "all-disabled PUT should leave middleware off") +} + func TestGetTelemetry(t *testing.T) { ctx := context.Background() @@ -228,6 +274,7 @@ func TestPatchTelemetry(t *testing.T) { Network: &oapi.BrowserTelemetryCategoryConfig{Enabled: &f}, Page: &oapi.BrowserTelemetryCategoryConfig{Enabled: &f}, Interaction: &oapi.BrowserTelemetryCategoryConfig{Enabled: &f}, + Api: &oapi.BrowserTelemetryCategoryConfig{Enabled: &f}, }, }, }) @@ -240,6 +287,7 @@ func TestPatchTelemetry(t *testing.T) { 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) }) t.Run("put returns 201 after patch clears configuration", func(t *testing.T) { @@ -255,6 +303,7 @@ func TestPatchTelemetry(t *testing.T) { Network: &oapi.BrowserTelemetryCategoryConfig{Enabled: &f}, Page: &oapi.BrowserTelemetryCategoryConfig{Enabled: &f}, Interaction: &oapi.BrowserTelemetryCategoryConfig{Enabled: &f}, + Api: &oapi.BrowserTelemetryCategoryConfig{Enabled: &f}, }, }, }) @@ -275,11 +324,13 @@ func newMockRecordManager() *mockRecordManager { type mockRecordManager struct{} -func (m *mockRecordManager) RegisterRecorder(_ context.Context, _ recorder.Recorder) error { return nil } +func (m *mockRecordManager) RegisterRecorder(_ context.Context, _ recorder.Recorder) error { + return nil +} func (m *mockRecordManager) DeregisterRecorder(_ context.Context, _ recorder.Recorder) error { return nil } -func (m *mockRecordManager) GetRecorder(_ string) (recorder.Recorder, bool) { return nil, false } +func (m *mockRecordManager) GetRecorder(_ string) (recorder.Recorder, bool) { return nil, false } func (m *mockRecordManager) ListActiveRecorders(_ context.Context) []recorder.Recorder { return nil } func (m *mockRecordManager) StopAll(_ context.Context) error { return nil } diff --git a/server/cmd/api/main.go b/server/cmd/api/main.go index 90bc636b..c226e5a7 100644 --- a/server/cmd/api/main.go +++ b/server/cmd/api/main.go @@ -23,7 +23,6 @@ import ( "github.com/kernel/kernel-images/server/cmd/api/api" "github.com/kernel/kernel-images/server/cmd/config" "github.com/kernel/kernel-images/server/lib/chromedriverproxy" - "github.com/kernel/kernel-images/server/lib/telemetry" "github.com/kernel/kernel-images/server/lib/devtoolsproxy" "github.com/kernel/kernel-images/server/lib/events" "github.com/kernel/kernel-images/server/lib/logger" @@ -31,6 +30,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/kernel/kernel-images/server/lib/telemetry" ) func main() { @@ -54,6 +54,7 @@ func main() { stz := scaletozero.NewDebouncedControllerWithCooldown(scaletozero.NewUnikraftCloudController(), config.ScaleToZeroCooldown) r := chi.NewRouter() r.Use( + chiMiddleware.RequestID, chiMiddleware.Logger, chiMiddleware.Recoverer, func(next http.Handler) http.Handler { @@ -128,7 +129,11 @@ func main() { os.Exit(1) } - strictHandler := oapi.NewStrictHandler(apiService, nil) + // api_call event emission. Off until the telemetry handlers flip it on. + r.Use(api.TelemetryHTTPMiddleware(telemetrySession.Publish)) + strictHandler := oapi.NewStrictHandler(apiService, []oapi.StrictMiddlewareFunc{ + api.TelemetryStrictMiddleware(), + }) oapi.HandlerFromMux(strictHandler, r) // endpoints to expose the spec @@ -198,7 +203,7 @@ func main() { rDevtools.Get("/json/list", jsonTargetHandler) rDevtools.Get("/json/list/", jsonTargetHandler) rDevtools.Get("/*", func(w http.ResponseWriter, r *http.Request) { - devtoolsproxy.WebSocketProxyHandler(upstreamMgr, slogger, config.LogCDPMessages, stz).ServeHTTP(w, r) + devtoolsproxy.WebSocketProxyHandler(upstreamMgr, slogger, config.LogCDPMessages, stz, telemetrySession.Publish).ServeHTTP(w, r) }) srvDevtools := &http.Server{ diff --git a/server/lib/devtoolsproxy/proxy.go b/server/lib/devtoolsproxy/proxy.go index 1721d918..6f82442f 100644 --- a/server/lib/devtoolsproxy/proxy.go +++ b/server/lib/devtoolsproxy/proxy.go @@ -3,6 +3,7 @@ package devtoolsproxy import ( "bufio" "context" + "encoding/json" "errors" "fmt" "log/slog" @@ -18,6 +19,8 @@ import ( "time" "github.com/coder/websocket" + "github.com/kernel/kernel-images/server/lib/events" + oapi "github.com/kernel/kernel-images/server/lib/oapi" "github.com/kernel/kernel-images/server/lib/scaletozero" "github.com/kernel/kernel-images/server/lib/wsproxy" ) @@ -296,17 +299,25 @@ func maybePauseAfterCurrentRead(ctx context.Context, logger *slog.Logger, r *htt } } +// EventPublisher publishes a telemetry event onto the in-VM events +// pipeline. nil disables emission. +type EventPublisher func(ev events.Event) + // WebSocketProxyHandler returns an http.Handler that upgrades incoming connections and // proxies them to the current upstream websocket URL. It expects only websocket requests. // If logCDPMessages is true, all CDP messages will be logged with their direction. -func WebSocketProxyHandler(mgr *UpstreamManager, logger *slog.Logger, logCDPMessages bool, ctrl scaletozero.Controller) http.Handler { +// publish is invoked on accept (cdp_connect) and on teardown (cdp_disconnect); pass +// nil to disable emission. +func WebSocketProxyHandler(mgr *UpstreamManager, logger *slog.Logger, logCDPMessages bool, ctrl scaletozero.Controller, publish EventPublisher) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - var transform wsproxy.MessageTransform - if logCDPMessages { - transform = func(direction string, mt websocket.MessageType, msg []byte) []byte { + // Counts every relayed message so cdp_disconnect can report message_count. + var msgCount atomic.Int64 + var transform wsproxy.MessageTransform = func(direction string, mt websocket.MessageType, msg []byte) []byte { + if logCDPMessages { logCDPMessage(logger, direction, mt, msg) - return msg } + msgCount.Add(1) + return msg } acceptOpts := &websocket.AcceptOptions{ @@ -337,6 +348,9 @@ func WebSocketProxyHandler(mgr *UpstreamManager, logger *slog.Logger, logCDPMess } clientConn.SetReadLimit(100 * 1024 * 1024) + publishCdpConnect(publish) + connectedAt := time.Now() + // Dial upstream. If the URL is stale (Chromium just restarted), first // re-check the manager's latest URL in case we missed the notification, // then wait briefly for the next update from Subscribe. @@ -345,9 +359,11 @@ func WebSocketProxyHandler(mgr *UpstreamManager, logger *slog.Logger, logCDPMess switch { case errors.Is(err, context.Canceled), errors.Is(err, context.DeadlineExceeded), errors.Is(r.Context().Err(), context.Canceled), errors.Is(r.Context().Err(), context.DeadlineExceeded): clientConn.Close(websocket.StatusGoingAway, "request cancelled") + publishCdpDisconnect(publish, oapi.ContextCancelled, connectedAt, msgCount.Load()) default: logger.Error("failed to connect to upstream", slog.String("err", err.Error())) clientConn.Close(websocket.StatusInternalError, "upstream unavailable") + publishCdpDisconnect(publish, oapi.UpstreamError, connectedAt, msgCount.Load()) } return } @@ -359,6 +375,10 @@ func WebSocketProxyHandler(mgr *UpstreamManager, logger *slog.Logger, logCDPMess // forcing the client to reconnect with the new upstream. pumpCtx, pumpCancel := context.WithCancel(r.Context()) + // Set by the URL-watcher when it tears down the pump; cleanup falls + // back to client_close otherwise. + var reasonOverride atomic.Pointer[oapi.BrowserCdpDisconnectEventDataReason] + go func(currentUpstreamURL string) { for { select { @@ -373,6 +393,8 @@ func WebSocketProxyHandler(mgr *UpstreamManager, logger *slog.Logger, logCDPMess logger.Info("upstream URL changed, closing stale proxy session", slog.String("old_url", currentUpstreamURL), slog.String("new_url", newURL)) + reason := oapi.UpstreamChanged + reasonOverride.CompareAndSwap(nil, &reason) pumpCancel() return case <-pumpCtx.Done(): @@ -384,9 +406,16 @@ func WebSocketProxyHandler(mgr *UpstreamManager, logger *slog.Logger, logCDPMess var once sync.Once cleanup := func() { once.Do(func() { + reason := oapi.ClientClose + if rp := reasonOverride.Load(); rp != nil { + reason = *rp + } else if r.Context().Err() != nil { + reason = oapi.ContextCancelled + } pumpCancel() upstreamConn.Close(websocket.StatusNormalClosure, "") clientConn.Close(websocket.StatusNormalClosure, "") + publishCdpDisconnect(publish, reason, connectedAt, msgCount.Load()) }) } @@ -394,6 +423,36 @@ func WebSocketProxyHandler(mgr *UpstreamManager, logger *slog.Logger, logCDPMess }) } +func publishCdpConnect(publish EventPublisher) { + if publish == nil { + return + } + publish(events.Event{ + Ts: time.Now().UnixMicro(), + Type: "cdp_connect", + Category: events.System, + Source: oapi.BrowserEventSource{Kind: oapi.KernelApi}, + }) +} + +func publishCdpDisconnect(publish EventPublisher, reason oapi.BrowserCdpDisconnectEventDataReason, connectedAt time.Time, msgCount int64) { + if publish == nil { + return + } + data, _ := json.Marshal(oapi.BrowserCdpDisconnectEventData{ + DurationMs: float32(time.Since(connectedAt).Microseconds()) / 1000.0, + MessageCount: int(msgCount), + Reason: reason, + }) + publish(events.Event{ + Ts: time.Now().UnixMicro(), + Type: "cdp_disconnect", + Category: events.System, + Source: oapi.BrowserEventSource{Kind: oapi.KernelApi}, + Data: data, + }) +} + // normalizeUpstreamURL parses a raw DevTools URL and returns a clean form. func normalizeUpstreamURL(raw string) string { parsed, err := url.Parse(raw) diff --git a/server/lib/devtoolsproxy/proxy_test.go b/server/lib/devtoolsproxy/proxy_test.go index 57c02c67..80915eaf 100644 --- a/server/lib/devtoolsproxy/proxy_test.go +++ b/server/lib/devtoolsproxy/proxy_test.go @@ -2,6 +2,7 @@ package devtoolsproxy import ( "context" + "encoding/json" "fmt" "io" "log/slog" @@ -13,11 +14,14 @@ import ( "os/exec" "path/filepath" "strings" + "sync" "sync/atomic" "testing" "time" "github.com/coder/websocket" + "github.com/kernel/kernel-images/server/lib/events" + oapi "github.com/kernel/kernel-images/server/lib/oapi" "github.com/kernel/kernel-images/server/lib/scaletozero" ) @@ -127,7 +131,7 @@ func TestWebSocketProxyHandler_ProxiesEcho(t *testing.T) { // seed current upstream to echo server including path/query (bypass tailing) mgr.setCurrent((&url.URL{Scheme: u.Scheme, Host: u.Host, Path: u.Path, RawQuery: u.RawQuery}).String()) - proxy := WebSocketProxyHandler(mgr, logger, false, scaletozero.NewNoopController()) + proxy := WebSocketProxyHandler(mgr, logger, false, scaletozero.NewNoopController(), nil) proxySrv := httptest.NewServer(proxy) defer proxySrv.Close() @@ -395,3 +399,156 @@ func TestUpstreamManagerSubscriberGetsLatest(t *testing.T) { t.Fatal("timed out waiting for next update") } } + +// recordingPublisher captures published events for assertion. +type recordingPublisher struct { + mu sync.Mutex + events []events.Event +} + +func (rp *recordingPublisher) publish(ev events.Event) { + rp.mu.Lock() + defer rp.mu.Unlock() + rp.events = append(rp.events, ev) +} + +func (rp *recordingPublisher) snapshot() []events.Event { + rp.mu.Lock() + defer rp.mu.Unlock() + out := make([]events.Event, len(rp.events)) + copy(out, rp.events) + return out +} + +func TestWebSocketProxyHandler_EmitsConnectAndDisconnect(t *testing.T) { + echoSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + c, err := websocket.Accept(w, r, &websocket.AcceptOptions{OriginPatterns: []string{"*"}}) + if err != nil { + t.Errorf("accept failed: %v", err) + return + } + defer c.Close(websocket.StatusNormalClosure, "") + for { + mt, msg, err := c.Read(r.Context()) + if err != nil { + return + } + if err := c.Write(r.Context(), mt, msg); err != nil { + return + } + } + })) + defer echoSrv.Close() + + u, _ := url.Parse(echoSrv.URL) + u.Scheme = "ws" + u.Path = "/devtools/browser/x" + + logger := silentLogger() + mgr := NewUpstreamManager("/dev/null", logger) + mgr.setCurrent(u.String()) + + rp := &recordingPublisher{} + proxySrv := httptest.NewServer(WebSocketProxyHandler(mgr, logger, false, scaletozero.NewNoopController(), rp.publish)) + defer proxySrv.Close() + + pu, _ := url.Parse(proxySrv.URL) + pu.Scheme = "ws" + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + conn, _, err := websocket.Dial(ctx, pu.String(), nil) + if err != nil { + t.Fatalf("dial proxy failed: %v", err) + } + + // 3 round trips => 6 messages relayed by the proxy. + for i := 0; i < 3; i++ { + if err := conn.Write(ctx, websocket.MessageText, []byte("ping")); err != nil { + t.Fatalf("write %d: %v", i, err) + } + if _, _, err := conn.Read(ctx); err != nil { + t.Fatalf("read %d: %v", i, err) + } + } + + _ = conn.Close(websocket.StatusNormalClosure, "bye") + + if !waitForCondition(2*time.Second, func() bool { return len(rp.snapshot()) >= 2 }) { + t.Fatalf("expected 2 events, got %d", len(rp.snapshot())) + } + + captured := rp.snapshot() + 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[1].Type; got != "cdp_disconnect" { + t.Fatalf("second event type = %q, want cdp_disconnect", got) + } + var disconnect struct { + DurationMs float64 `json:"duration_ms"` + MessageCount int64 `json:"message_count"` + Reason oapi.BrowserCdpDisconnectEventDataReason `json:"reason"` + } + if err := json.Unmarshal(captured[1].Data, &disconnect); err != nil { + t.Fatalf("unmarshal disconnect data: %v", err) + } + if disconnect.Reason != oapi.ClientClose { + t.Fatalf("disconnect reason = %q, want %q", disconnect.Reason, oapi.ClientClose) + } + if disconnect.MessageCount < 6 { + t.Fatalf("disconnect message_count = %d, want >= 6", disconnect.MessageCount) + } + if disconnect.DurationMs <= 0 { + t.Fatalf("disconnect duration_ms = %f, want > 0", disconnect.DurationMs) + } +} + +func TestWebSocketProxyHandler_EmitsUpstreamErrorOnDialFailure(t *testing.T) { + port, err := getFreePort() + if err != nil { + t.Fatalf("get free port: %v", err) + } + deadURL := fmt.Sprintf("ws://127.0.0.1:%d/devtools/browser/dead", port) + + logger := silentLogger() + mgr := NewUpstreamManager("/dev/null", logger) + mgr.setCurrent(deadURL) + + rp := &recordingPublisher{} + proxySrv := httptest.NewServer(WebSocketProxyHandler(mgr, logger, false, scaletozero.NewNoopController(), rp.publish)) + defer proxySrv.Close() + + pu, _ := url.Parse(proxySrv.URL) + pu.Scheme = "ws" + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + if c, _, err := websocket.Dial(ctx, pu.String(), nil); err == nil { + _ = c.Close(websocket.StatusNormalClosure, "") + } + + if !waitForCondition(15*time.Second, func() bool { return len(rp.snapshot()) >= 2 }) { + t.Fatalf("expected 2 events, got %d: %+v", len(rp.snapshot()), rp.snapshot()) + } + captured := rp.snapshot() + if captured[0].Type != "cdp_connect" { + t.Fatalf("first event type = %q, want cdp_connect", captured[0].Type) + } + if captured[1].Type != "cdp_disconnect" { + t.Fatalf("second event type = %q, want cdp_disconnect", captured[1].Type) + } + var disconnect struct { + Reason oapi.BrowserCdpDisconnectEventDataReason `json:"reason"` + } + if err := json.Unmarshal(captured[1].Data, &disconnect); err != nil { + t.Fatalf("unmarshal disconnect data: %v", err) + } + if disconnect.Reason != oapi.UpstreamError { + t.Fatalf("disconnect reason = %q, want %q", disconnect.Reason, oapi.UpstreamError) + } +} diff --git a/server/lib/events/event.go b/server/lib/events/event.go index 892c7718..d331084a 100644 --- a/server/lib/events/event.go +++ b/server/lib/events/event.go @@ -15,6 +15,7 @@ const ( Network = oapi.TelemetryEventCategory("network") Page = oapi.TelemetryEventCategory("page") Interaction = oapi.TelemetryEventCategory("interaction") + Api = oapi.TelemetryEventCategory("api") System = oapi.TelemetryEventCategory("system") ) @@ -25,6 +26,7 @@ var AllCategories = []oapi.TelemetryEventCategory{ Network, Page, Interaction, + Api, System, } diff --git a/server/lib/oapi/oapi.go b/server/lib/oapi/oapi.go index c720f8b3..9b350bf0 100644 --- a/server/lib/oapi/oapi.go +++ b/server/lib/oapi/oapi.go @@ -26,6 +26,144 @@ import ( openapi_types "github.com/oapi-codegen/runtime/types" ) +// Defines values for BrowserApiCallEventType. +const ( + ApiCall BrowserApiCallEventType = "api_call" +) + +// Valid indicates whether the value is a known member of the BrowserApiCallEventType enum. +func (e BrowserApiCallEventType) Valid() bool { + switch e { + case ApiCall: + return true + default: + return false + } +} + +// Defines values for BrowserCaptchaSolveResultEventType. +const ( + CaptchaSolveResult BrowserCaptchaSolveResultEventType = "captcha_solve_result" +) + +// Valid indicates whether the value is a known member of the BrowserCaptchaSolveResultEventType enum. +func (e BrowserCaptchaSolveResultEventType) Valid() bool { + switch e { + case CaptchaSolveResult: + return true + default: + return false + } +} + +// Defines values for BrowserCaptchaSolveResultEventDataCaptchaType. +const ( + BrowserCaptchaSolveResultEventDataCaptchaTypeGeetest BrowserCaptchaSolveResultEventDataCaptchaType = "geetest" + BrowserCaptchaSolveResultEventDataCaptchaTypeHcaptcha BrowserCaptchaSolveResultEventDataCaptchaType = "hcaptcha" + BrowserCaptchaSolveResultEventDataCaptchaTypeOther BrowserCaptchaSolveResultEventDataCaptchaType = "other" + BrowserCaptchaSolveResultEventDataCaptchaTypeRecaptchaV2 BrowserCaptchaSolveResultEventDataCaptchaType = "recaptcha_v2" + BrowserCaptchaSolveResultEventDataCaptchaTypeRecaptchaV3 BrowserCaptchaSolveResultEventDataCaptchaType = "recaptcha_v3" + BrowserCaptchaSolveResultEventDataCaptchaTypeTurnstile BrowserCaptchaSolveResultEventDataCaptchaType = "turnstile" +) + +// Valid indicates whether the value is a known member of the BrowserCaptchaSolveResultEventDataCaptchaType enum. +func (e BrowserCaptchaSolveResultEventDataCaptchaType) Valid() bool { + switch e { + case BrowserCaptchaSolveResultEventDataCaptchaTypeGeetest: + return true + case BrowserCaptchaSolveResultEventDataCaptchaTypeHcaptcha: + return true + case BrowserCaptchaSolveResultEventDataCaptchaTypeOther: + return true + case BrowserCaptchaSolveResultEventDataCaptchaTypeRecaptchaV2: + return true + case BrowserCaptchaSolveResultEventDataCaptchaTypeRecaptchaV3: + return true + case BrowserCaptchaSolveResultEventDataCaptchaTypeTurnstile: + return true + default: + return false + } +} + +// Defines values for BrowserCaptchaSolveResultEventDataStatus. +const ( + Abandoned BrowserCaptchaSolveResultEventDataStatus = "abandoned" + Failure BrowserCaptchaSolveResultEventDataStatus = "failure" + Success BrowserCaptchaSolveResultEventDataStatus = "success" + Timeout BrowserCaptchaSolveResultEventDataStatus = "timeout" +) + +// Valid indicates whether the value is a known member of the BrowserCaptchaSolveResultEventDataStatus enum. +func (e BrowserCaptchaSolveResultEventDataStatus) Valid() bool { + switch e { + case Abandoned: + return true + case Failure: + return true + case Success: + return true + case Timeout: + return true + default: + return false + } +} + +// Defines values for BrowserCdpConnectEventType. +const ( + CdpConnect BrowserCdpConnectEventType = "cdp_connect" +) + +// Valid indicates whether the value is a known member of the BrowserCdpConnectEventType enum. +func (e BrowserCdpConnectEventType) Valid() bool { + switch e { + case CdpConnect: + return true + default: + return false + } +} + +// Defines values for BrowserCdpDisconnectEventType. +const ( + CdpDisconnect BrowserCdpDisconnectEventType = "cdp_disconnect" +) + +// Valid indicates whether the value is a known member of the BrowserCdpDisconnectEventType enum. +func (e BrowserCdpDisconnectEventType) Valid() bool { + switch e { + case CdpDisconnect: + return true + default: + return false + } +} + +// Defines values for BrowserCdpDisconnectEventDataReason. +const ( + ClientClose BrowserCdpDisconnectEventDataReason = "client_close" + ContextCancelled BrowserCdpDisconnectEventDataReason = "context_cancelled" + UpstreamChanged BrowserCdpDisconnectEventDataReason = "upstream_changed" + UpstreamError BrowserCdpDisconnectEventDataReason = "upstream_error" +) + +// Valid indicates whether the value is a known member of the BrowserCdpDisconnectEventDataReason enum. +func (e BrowserCdpDisconnectEventDataReason) Valid() bool { + switch e { + case ClientClose: + return true + case ContextCancelled: + return true + case UpstreamChanged: + return true + case UpstreamError: + return true + default: + return false + } +} + // Defines values for BrowserConsoleErrorEventType. const ( ConsoleError BrowserConsoleErrorEventType = "console_error" @@ -125,6 +263,36 @@ func (e BrowserInteractionScrollSettledEventType) Valid() bool { } } +// Defines values for BrowserLiveViewConnectEventType. +const ( + LiveViewConnect BrowserLiveViewConnectEventType = "live_view_connect" +) + +// Valid indicates whether the value is a known member of the BrowserLiveViewConnectEventType enum. +func (e BrowserLiveViewConnectEventType) Valid() bool { + switch e { + case LiveViewConnect: + return true + default: + return false + } +} + +// Defines values for BrowserLiveViewDisconnectEventType. +const ( + LiveViewDisconnect BrowserLiveViewDisconnectEventType = "live_view_disconnect" +) + +// Valid indicates whether the value is a known member of the BrowserLiveViewDisconnectEventType enum. +func (e BrowserLiveViewDisconnectEventType) Valid() bool { + switch e { + case LiveViewDisconnect: + return true + default: + return false + } +} + // Defines values for BrowserMonitorDisconnectedEventType. const ( MonitorDisconnected BrowserMonitorDisconnectedEventType = "monitor_disconnected" @@ -718,6 +886,7 @@ func (e ProcessStreamEventStream) Valid() bool { // Defines values for PublishEventRequestCategory. const ( + PublishEventRequestCategoryApi PublishEventRequestCategory = "api" PublishEventRequestCategoryConsole PublishEventRequestCategory = "console" PublishEventRequestCategoryInteraction PublishEventRequestCategory = "interaction" PublishEventRequestCategoryNetwork PublishEventRequestCategory = "network" @@ -728,6 +897,8 @@ const ( // Valid indicates whether the value is a known member of the PublishEventRequestCategory enum. func (e PublishEventRequestCategory) Valid() bool { switch e { + case PublishEventRequestCategoryApi: + return true case PublishEventRequestCategoryConsole: return true case PublishEventRequestCategoryInteraction: @@ -745,25 +916,28 @@ func (e PublishEventRequestCategory) Valid() bool { // Defines values for TelemetryEventCategory. const ( - Console TelemetryEventCategory = "console" - Interaction TelemetryEventCategory = "interaction" - Network TelemetryEventCategory = "network" - Page TelemetryEventCategory = "page" - System TelemetryEventCategory = "system" + TelemetryEventCategoryApi TelemetryEventCategory = "api" + TelemetryEventCategoryConsole TelemetryEventCategory = "console" + TelemetryEventCategoryInteraction TelemetryEventCategory = "interaction" + TelemetryEventCategoryNetwork TelemetryEventCategory = "network" + TelemetryEventCategoryPage TelemetryEventCategory = "page" + TelemetryEventCategorySystem TelemetryEventCategory = "system" ) // Valid indicates whether the value is a known member of the TelemetryEventCategory enum. func (e TelemetryEventCategory) Valid() bool { switch e { - case Console: + case TelemetryEventCategoryApi: + return true + case TelemetryEventCategoryConsole: return true - case Interaction: + case TelemetryEventCategoryInteraction: return true - case Network: + case TelemetryEventCategoryNetwork: return true - case Page: + case TelemetryEventCategoryPage: return true - case System: + case TelemetryEventCategorySystem: return true default: return false @@ -818,6 +992,40 @@ type BatchComputerActionRequest struct { Actions []ComputerAction `json:"actions"` } +// BrowserApiCallEvent An HTTP call handled by the kernel-images-api server. +type BrowserApiCallEvent struct { + // Data Per-call payload for `api_call` events. + Data *BrowserApiCallEventData `json:"data,omitempty"` + + // Source Provenance metadata identifying which producer emitted the event. + Source BrowserEventSource `json:"source"` + + // Truncated True if the data field was truncated due to size limits. + Truncated *bool `json:"truncated,omitempty"` + + // Ts Event timestamp in Unix microseconds. + Ts int64 `json:"ts"` + Type BrowserApiCallEventType `json:"type"` +} + +// BrowserApiCallEventType defines model for BrowserApiCallEvent.Type. +type BrowserApiCallEventType string + +// BrowserApiCallEventData Per-call payload for `api_call` events. +type BrowserApiCallEventData struct { + // DurationMs Wall-clock duration of the handler in milliseconds. + DurationMs float32 `json:"duration_ms"` + + // OperationId OpenAPI operationId of the matched route (e.g. `processExec`, `takeScreenshot`). + OperationId string `json:"operation_id"` + + // RequestId Per-request identifier from the kernel-images-api request middleware. + RequestId string `json:"request_id"` + + // Status HTTP response status code. + Status int `json:"status"` +} + // BrowserCallStack CDP Runtime.StackTrace representing the JavaScript call stack at the time of an event. Fields use CDP naming conventions rather than snake_case to match the Chrome DevTools Protocol wire format. type BrowserCallStack struct { // CallFrames Ordered list of call frames, outermost first. @@ -845,6 +1053,105 @@ type BrowserCallStack struct { Parent *BrowserCallStack `json:"parent,omitempty"` } +// BrowserCaptchaSolveResultEvent A captcha solve attempt reached a terminal outcome. +type BrowserCaptchaSolveResultEvent struct { + // Data Per-attempt payload for `captcha_solve_result` events. + Data *BrowserCaptchaSolveResultEventData `json:"data,omitempty"` + + // Source Provenance metadata identifying which producer emitted the event. + Source BrowserEventSource `json:"source"` + + // Truncated True if the data field was truncated due to size limits. + Truncated *bool `json:"truncated,omitempty"` + + // Ts Event timestamp in Unix microseconds. + Ts int64 `json:"ts"` + Type BrowserCaptchaSolveResultEventType `json:"type"` +} + +// BrowserCaptchaSolveResultEventType defines model for BrowserCaptchaSolveResultEvent.Type. +type BrowserCaptchaSolveResultEventType string + +// BrowserCaptchaSolveResultEventData Per-attempt payload for `captcha_solve_result` events. +type BrowserCaptchaSolveResultEventData struct { + // CaptchaType Captcha vendor family. Producers normalize provider-specific task names into this set: enterprise variants of recaptcha collapse into their version bucket (v2 / v3), and anything not covered (e.g. DataDome, MtCaptcha, plain OCR) is reported as `other`. + CaptchaType BrowserCaptchaSolveResultEventDataCaptchaType `json:"captcha_type"` + + // DurationMs Wall-clock duration from solve start to terminal outcome. + DurationMs float32 `json:"duration_ms"` + + // ErrorCode Solver-specific error code on failure (e.g. `ERROR_CAPTCHA_UNSOLVABLE`). Absent on success. + ErrorCode *string `json:"error_code,omitempty"` + + // Status Terminal outcome. `success`: solver returned a usable solution. `failure`: solver returned an error (see `error_code`). `timeout`: solver did not return within the caller's wait budget. `abandoned`: caller cancelled or the page navigated away mid-solve. + Status BrowserCaptchaSolveResultEventDataStatus `json:"status"` + + // TaskId Solver-assigned identifier. Opaque, useful for support cross-references. + TaskId *string `json:"task_id,omitempty"` + + // WebsiteHost Host of the page where the captcha was solved. + WebsiteHost *string `json:"website_host,omitempty"` + + // WebsitePath Path of the page where the captcha was solved. Query string excluded. + WebsitePath *string `json:"website_path,omitempty"` +} + +// BrowserCaptchaSolveResultEventDataCaptchaType Captcha vendor family. Producers normalize provider-specific task names into this set: enterprise variants of recaptcha collapse into their version bucket (v2 / v3), and anything not covered (e.g. DataDome, MtCaptcha, plain OCR) is reported as `other`. +type BrowserCaptchaSolveResultEventDataCaptchaType string + +// BrowserCaptchaSolveResultEventDataStatus Terminal outcome. `success`: solver returned a usable solution. `failure`: solver returned an error (see `error_code`). `timeout`: solver did not return within the caller's wait budget. `abandoned`: caller cancelled or the page navigated away mid-solve. +type BrowserCaptchaSolveResultEventDataStatus string + +// BrowserCdpConnectEvent An external client (e.g. customer SDK, Playwright, Puppeteer) connected to the CDP WebSocket proxy on this VM. +type BrowserCdpConnectEvent struct { + // Source Provenance metadata identifying which producer emitted the event. + Source BrowserEventSource `json:"source"` + + // Truncated True if the data field was truncated due to size limits. + Truncated *bool `json:"truncated,omitempty"` + + // Ts Event timestamp in Unix microseconds. + Ts int64 `json:"ts"` + Type BrowserCdpConnectEventType `json:"type"` +} + +// BrowserCdpConnectEventType defines model for BrowserCdpConnectEvent.Type. +type BrowserCdpConnectEventType string + +// BrowserCdpDisconnectEvent An external client disconnected from the CDP WebSocket proxy on this VM. Pair with the immediately preceding `cdp_connect` on the same stream. +type BrowserCdpDisconnectEvent struct { + // Data Per-disconnect payload for `cdp_disconnect` events. + Data *BrowserCdpDisconnectEventData `json:"data,omitempty"` + + // Source Provenance metadata identifying which producer emitted the event. + Source BrowserEventSource `json:"source"` + + // Truncated True if the data field was truncated due to size limits. + Truncated *bool `json:"truncated,omitempty"` + + // Ts Event timestamp in Unix microseconds. + Ts int64 `json:"ts"` + Type BrowserCdpDisconnectEventType `json:"type"` +} + +// BrowserCdpDisconnectEventType defines model for BrowserCdpDisconnectEvent.Type. +type BrowserCdpDisconnectEventType string + +// BrowserCdpDisconnectEventData Per-disconnect payload for `cdp_disconnect` events. +type BrowserCdpDisconnectEventData struct { + // DurationMs Wall-clock duration of the connection in milliseconds. + DurationMs float32 `json:"duration_ms"` + + // MessageCount Number of CDP messages relayed across the connection in either direction. + MessageCount int `json:"message_count"` + + // Reason Why the connection ended. `client_close`: the client initiated the close. `upstream_changed`: Chromium restarted mid-session and the proxy tore down so the client could reconnect against the new upstream. `upstream_error`: upstream dial or message pump errored. `context_cancelled`: the request context was cancelled (typically server shutdown). + Reason BrowserCdpDisconnectEventDataReason `json:"reason"` +} + +// BrowserCdpDisconnectEventDataReason Why the connection ended. `client_close`: the client initiated the close. `upstream_changed`: Chromium restarted mid-session and the proxy tore down so the client could reconnect against the new upstream. `upstream_error`: upstream dial or message pump errored. `context_cancelled`: the request context was cancelled (typically server shutdown). +type BrowserCdpDisconnectEventDataReason string + // BrowserConsoleErrorEvent A browser console error or uncaught JavaScript exception event. Emitted from two distinct CDP sources with different data shapes. Runtime.consoleAPICalled (console.error calls) produces level, text, args, and stack_trace. Runtime.exceptionThrown (uncaught exceptions) produces text, line, column, source_url, and stack_trace. Fields not applicable to the source are absent. type BrowserConsoleErrorEvent struct { Data *BrowserConsoleErrorEventData `json:"data,omitempty"` @@ -1169,6 +1476,59 @@ type BrowserInteractionScrollSettledEventData struct { Url *string `json:"url,omitempty"` } +// BrowserLiveViewConnectEvent A live view client connected to the headful browser's WebRTC server (Neko). Headful only; not emitted for headless images. +type BrowserLiveViewConnectEvent struct { + // Data Per-session payload for `live_view_connect` events. + Data *BrowserLiveViewConnectEventData `json:"data,omitempty"` + + // Source Provenance metadata identifying which producer emitted the event. + Source BrowserEventSource `json:"source"` + + // Truncated True if the data field was truncated due to size limits. + Truncated *bool `json:"truncated,omitempty"` + + // Ts Event timestamp in Unix microseconds. + Ts int64 `json:"ts"` + Type BrowserLiveViewConnectEventType `json:"type"` +} + +// BrowserLiveViewConnectEventType defines model for BrowserLiveViewConnectEvent.Type. +type BrowserLiveViewConnectEventType string + +// BrowserLiveViewConnectEventData Per-session payload for `live_view_connect` events. +type BrowserLiveViewConnectEventData struct { + // SessionId Live view session identifier. Stable across reconnects, so a transient network blip can emit two events with the same `session_id`. + SessionId string `json:"session_id"` +} + +// 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 { + // Data Per-session payload for `live_view_disconnect` events. + Data *BrowserLiveViewDisconnectEventData `json:"data,omitempty"` + + // Source Provenance metadata identifying which producer emitted the event. + Source BrowserEventSource `json:"source"` + + // Truncated True if the data field was truncated due to size limits. + Truncated *bool `json:"truncated,omitempty"` + + // Ts Event timestamp in Unix microseconds. + Ts int64 `json:"ts"` + Type BrowserLiveViewDisconnectEventType `json:"type"` +} + +// BrowserLiveViewDisconnectEventType defines model for BrowserLiveViewDisconnectEvent.Type. +type BrowserLiveViewDisconnectEventType string + +// BrowserLiveViewDisconnectEventData Per-session payload for `live_view_disconnect` events. +type BrowserLiveViewDisconnectEventData struct { + // DurationMs Wall-clock duration of the connection in milliseconds. + DurationMs float32 `json:"duration_ms"` + + // SessionId Live view session identifier; matches the corresponding `live_view_connect` event. + SessionId string `json:"session_id"` +} + // BrowserMonitorDisconnectedEvent The CDP connection to Chrome was lost. Telemetry events may be dropped until monitor_reconnected arrives. Treat any in-progress computed state (network_idle, page_layout_settled) as unreliable until then. type BrowserMonitorDisconnectedEvent struct { Data *BrowserMonitorDisconnectedEventData `json:"data,omitempty"` @@ -1843,6 +2203,9 @@ 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"` + // Console Console output (log, warn, error) and uncaught exceptions. Console *BrowserTelemetryCategoryConfig `json:"console,omitempty"` @@ -1862,7 +2225,7 @@ type BrowserTelemetryCategoryConfig struct { 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 four categories to enabled: false to clear the telemetry configuration. +// 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. type BrowserTelemetryConfig struct { // Browser Per-category telemetry capture settings for browser events. Browser *BrowserTelemetryCategoriesConfig `json:"browser,omitempty"` @@ -2487,7 +2850,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 four categories to enabled: false to clear the telemetry configuration. + // 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 BrowserTelemetryConfig `json:"config"` // Seq Process-monotonic sequence number of the last published event. Does not reset across configuration changes. @@ -3409,6 +3772,174 @@ func (t *KnownBrowserTelemetryEvent) MergeBrowserMonitorInitFailedEvent(v Browse return err } +// AsBrowserApiCallEvent returns the union data inside the KnownBrowserTelemetryEvent as a BrowserApiCallEvent +func (t KnownBrowserTelemetryEvent) AsBrowserApiCallEvent() (BrowserApiCallEvent, error) { + var body BrowserApiCallEvent + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromBrowserApiCallEvent overwrites any union data inside the KnownBrowserTelemetryEvent as the provided BrowserApiCallEvent +func (t *KnownBrowserTelemetryEvent) FromBrowserApiCallEvent(v BrowserApiCallEvent) error { + v.Type = "api_call" + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeBrowserApiCallEvent performs a merge with any union data inside the KnownBrowserTelemetryEvent, using the provided BrowserApiCallEvent +func (t *KnownBrowserTelemetryEvent) MergeBrowserApiCallEvent(v BrowserApiCallEvent) error { + v.Type = "api_call" + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +// AsBrowserCdpConnectEvent returns the union data inside the KnownBrowserTelemetryEvent as a BrowserCdpConnectEvent +func (t KnownBrowserTelemetryEvent) AsBrowserCdpConnectEvent() (BrowserCdpConnectEvent, error) { + var body BrowserCdpConnectEvent + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromBrowserCdpConnectEvent overwrites any union data inside the KnownBrowserTelemetryEvent as the provided BrowserCdpConnectEvent +func (t *KnownBrowserTelemetryEvent) FromBrowserCdpConnectEvent(v BrowserCdpConnectEvent) error { + v.Type = "cdp_connect" + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeBrowserCdpConnectEvent performs a merge with any union data inside the KnownBrowserTelemetryEvent, using the provided BrowserCdpConnectEvent +func (t *KnownBrowserTelemetryEvent) MergeBrowserCdpConnectEvent(v BrowserCdpConnectEvent) error { + v.Type = "cdp_connect" + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +// AsBrowserCdpDisconnectEvent returns the union data inside the KnownBrowserTelemetryEvent as a BrowserCdpDisconnectEvent +func (t KnownBrowserTelemetryEvent) AsBrowserCdpDisconnectEvent() (BrowserCdpDisconnectEvent, error) { + var body BrowserCdpDisconnectEvent + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromBrowserCdpDisconnectEvent overwrites any union data inside the KnownBrowserTelemetryEvent as the provided BrowserCdpDisconnectEvent +func (t *KnownBrowserTelemetryEvent) FromBrowserCdpDisconnectEvent(v BrowserCdpDisconnectEvent) error { + v.Type = "cdp_disconnect" + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeBrowserCdpDisconnectEvent performs a merge with any union data inside the KnownBrowserTelemetryEvent, using the provided BrowserCdpDisconnectEvent +func (t *KnownBrowserTelemetryEvent) MergeBrowserCdpDisconnectEvent(v BrowserCdpDisconnectEvent) error { + v.Type = "cdp_disconnect" + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +// AsBrowserLiveViewConnectEvent returns the union data inside the KnownBrowserTelemetryEvent as a BrowserLiveViewConnectEvent +func (t KnownBrowserTelemetryEvent) AsBrowserLiveViewConnectEvent() (BrowserLiveViewConnectEvent, error) { + var body BrowserLiveViewConnectEvent + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromBrowserLiveViewConnectEvent overwrites any union data inside the KnownBrowserTelemetryEvent as the provided BrowserLiveViewConnectEvent +func (t *KnownBrowserTelemetryEvent) FromBrowserLiveViewConnectEvent(v BrowserLiveViewConnectEvent) error { + v.Type = "live_view_connect" + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeBrowserLiveViewConnectEvent performs a merge with any union data inside the KnownBrowserTelemetryEvent, using the provided BrowserLiveViewConnectEvent +func (t *KnownBrowserTelemetryEvent) MergeBrowserLiveViewConnectEvent(v BrowserLiveViewConnectEvent) error { + v.Type = "live_view_connect" + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +// AsBrowserLiveViewDisconnectEvent returns the union data inside the KnownBrowserTelemetryEvent as a BrowserLiveViewDisconnectEvent +func (t KnownBrowserTelemetryEvent) AsBrowserLiveViewDisconnectEvent() (BrowserLiveViewDisconnectEvent, error) { + var body BrowserLiveViewDisconnectEvent + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromBrowserLiveViewDisconnectEvent overwrites any union data inside the KnownBrowserTelemetryEvent as the provided BrowserLiveViewDisconnectEvent +func (t *KnownBrowserTelemetryEvent) FromBrowserLiveViewDisconnectEvent(v BrowserLiveViewDisconnectEvent) error { + v.Type = "live_view_disconnect" + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeBrowserLiveViewDisconnectEvent performs a merge with any union data inside the KnownBrowserTelemetryEvent, using the provided BrowserLiveViewDisconnectEvent +func (t *KnownBrowserTelemetryEvent) MergeBrowserLiveViewDisconnectEvent(v BrowserLiveViewDisconnectEvent) error { + v.Type = "live_view_disconnect" + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +// AsBrowserCaptchaSolveResultEvent returns the union data inside the KnownBrowserTelemetryEvent as a BrowserCaptchaSolveResultEvent +func (t KnownBrowserTelemetryEvent) AsBrowserCaptchaSolveResultEvent() (BrowserCaptchaSolveResultEvent, error) { + var body BrowserCaptchaSolveResultEvent + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromBrowserCaptchaSolveResultEvent overwrites any union data inside the KnownBrowserTelemetryEvent as the provided BrowserCaptchaSolveResultEvent +func (t *KnownBrowserTelemetryEvent) FromBrowserCaptchaSolveResultEvent(v BrowserCaptchaSolveResultEvent) error { + v.Type = "captcha_solve_result" + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergeBrowserCaptchaSolveResultEvent performs a merge with any union data inside the KnownBrowserTelemetryEvent, using the provided BrowserCaptchaSolveResultEvent +func (t *KnownBrowserTelemetryEvent) MergeBrowserCaptchaSolveResultEvent(v BrowserCaptchaSolveResultEvent) error { + v.Type = "captcha_solve_result" + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + func (t KnownBrowserTelemetryEvent) Discriminator() (string, error) { var discriminator struct { Discriminator string `json:"type"` @@ -3423,6 +3954,14 @@ func (t KnownBrowserTelemetryEvent) ValueByDiscriminator() (interface{}, error) return nil, err } switch discriminator { + case "api_call": + return t.AsBrowserApiCallEvent() + case "captcha_solve_result": + return t.AsBrowserCaptchaSolveResultEvent() + case "cdp_connect": + return t.AsBrowserCdpConnectEvent() + case "cdp_disconnect": + return t.AsBrowserCdpDisconnectEvent() case "console_error": return t.AsBrowserConsoleErrorEvent() case "console_log": @@ -3433,6 +3972,10 @@ func (t KnownBrowserTelemetryEvent) ValueByDiscriminator() (interface{}, error) return t.AsBrowserInteractionKeyEvent() case "interaction_scroll_settled": return t.AsBrowserInteractionScrollSettledEvent() + case "live_view_connect": + return t.AsBrowserLiveViewConnectEvent() + case "live_view_disconnect": + return t.AsBrowserLiveViewDisconnectEvent() case "monitor_disconnected": return t.AsBrowserMonitorDisconnectedEvent() case "monitor_init_failed": @@ -17372,302 +17915,328 @@ func (sh *strictHandler) StreamTelemetryEvents(w http.ResponseWriter, r *http.Re // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "GJ8K0HgBAA==", + "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", + "IvFk6Ub/vwEAAP//O92XVQ+aAQA=", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/server/openapi.yaml b/server/openapi.yaml index 0b11325d..7f56f440 100644 --- a/server/openapi.yaml +++ b/server/openapi.yaml @@ -1298,7 +1298,7 @@ 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 four categories + 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. operationId: putTelemetry @@ -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 four categories to enabled: false clears the configuration. + all five categories to enabled: false clears the configuration. operationId: patchTelemetry requestBody: required: true @@ -1466,6 +1466,7 @@ components: - network - page - interaction + - api - system source: $ref: "#/components/schemas/BrowserEventSource" @@ -1520,6 +1521,7 @@ components: - network - page - interaction + - api - system default: system source: @@ -1552,7 +1554,7 @@ components: 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 four categories to enabled: false to clear the telemetry configuration. + Set all five categories to enabled: false to clear the telemetry configuration. properties: browser: $ref: "#/components/schemas/BrowserTelemetryCategoriesConfig" @@ -1573,6 +1575,14 @@ components: network: $ref: "#/components/schemas/BrowserTelemetryCategoryConfig" description: HTTP request/response metadata. + api: + $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. additionalProperties: false BrowserTelemetryCategoryConfig: type: object @@ -2455,6 +2465,229 @@ components: truncated: type: boolean description: True if the data field was truncated due to size limits. + BrowserApiCallEventData: + type: object + description: Per-call payload for `api_call` events. + additionalProperties: false + required: [request_id, operation_id, status, duration_ms] + properties: + request_id: + type: string + description: Per-request identifier from the kernel-images-api request middleware. + operation_id: + type: string + description: OpenAPI operationId of the matched route (e.g. `processExec`, `takeScreenshot`). + status: + type: integer + description: HTTP response status code. + duration_ms: + type: number + description: Wall-clock duration of the handler in milliseconds. + BrowserApiCallEvent: + type: object + description: An HTTP call handled by the kernel-images-api server. + required: [ts, type, source] + properties: + ts: + type: integer + format: int64 + description: Event timestamp in Unix microseconds. + type: + type: string + const: api_call + source: + $ref: "#/components/schemas/BrowserEventSource" + data: + $ref: "#/components/schemas/BrowserApiCallEventData" + truncated: + type: boolean + description: True if the data field was truncated due to size limits. + BrowserCdpConnectEvent: + type: object + description: An external client (e.g. customer SDK, Playwright, Puppeteer) connected to the CDP WebSocket proxy on this VM. + required: [ts, type, source] + properties: + ts: + type: integer + format: int64 + description: Event timestamp in Unix microseconds. + type: + type: string + const: cdp_connect + source: + $ref: "#/components/schemas/BrowserEventSource" + truncated: + type: boolean + description: True if the data field was truncated due to size limits. + BrowserCdpDisconnectEventData: + type: object + description: Per-disconnect payload for `cdp_disconnect` events. + additionalProperties: false + required: [duration_ms, message_count, reason] + properties: + duration_ms: + type: number + description: Wall-clock duration of the connection in milliseconds. + message_count: + type: integer + description: Number of CDP messages relayed across the connection in either direction. + reason: + type: string + description: > + Why the connection ended. `client_close`: the client initiated the close. + `upstream_changed`: Chromium restarted mid-session and the proxy tore down + so the client could reconnect against the new upstream. `upstream_error`: + upstream dial or message pump errored. `context_cancelled`: the request + context was cancelled (typically server shutdown). + enum: + - client_close + - upstream_changed + - upstream_error + - context_cancelled + BrowserCdpDisconnectEvent: + type: object + description: An external client disconnected from the CDP WebSocket proxy on this VM. Pair with the immediately preceding `cdp_connect` on the same stream. + required: [ts, type, source] + properties: + ts: + type: integer + format: int64 + description: Event timestamp in Unix microseconds. + type: + type: string + const: cdp_disconnect + source: + $ref: "#/components/schemas/BrowserEventSource" + data: + $ref: "#/components/schemas/BrowserCdpDisconnectEventData" + truncated: + type: boolean + description: True if the data field was truncated due to size limits. + BrowserLiveViewConnectEventData: + type: object + description: Per-session payload for `live_view_connect` events. + additionalProperties: false + required: [session_id] + properties: + session_id: + type: string + description: Live view session identifier. Stable across reconnects, so a transient network blip can emit two events with the same `session_id`. + 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] + properties: + ts: + type: integer + format: int64 + description: Event timestamp in Unix microseconds. + type: + type: string + const: live_view_connect + source: + $ref: "#/components/schemas/BrowserEventSource" + data: + $ref: "#/components/schemas/BrowserLiveViewConnectEventData" + truncated: + type: boolean + description: True if the data field was truncated due to size limits. + BrowserLiveViewDisconnectEventData: + type: object + description: Per-session payload for `live_view_disconnect` events. + additionalProperties: false + required: [session_id, duration_ms] + properties: + session_id: + type: string + description: Live view session identifier; matches the corresponding `live_view_connect` event. + duration_ms: + type: number + description: Wall-clock duration of the connection in milliseconds. + 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] + properties: + ts: + type: integer + format: int64 + description: Event timestamp in Unix microseconds. + type: + type: string + const: live_view_disconnect + source: + $ref: "#/components/schemas/BrowserEventSource" + data: + $ref: "#/components/schemas/BrowserLiveViewDisconnectEventData" + truncated: + type: boolean + description: True if the data field was truncated due to size limits. + BrowserCaptchaSolveResultEventData: + type: object + description: Per-attempt payload for `captcha_solve_result` events. + additionalProperties: false + required: [captcha_type, status, duration_ms] + properties: + captcha_type: + type: string + description: > + Captcha vendor family. Producers normalize provider-specific task + names into this set: enterprise variants of recaptcha collapse into + their version bucket (v2 / v3), and anything not covered (e.g. + DataDome, MtCaptcha, plain OCR) is reported as `other`. + enum: + - hcaptcha + - recaptcha_v2 + - recaptcha_v3 + - turnstile + - geetest + - other + status: + type: string + description: > + Terminal outcome. `success`: solver returned a usable solution. + `failure`: solver returned an error (see `error_code`). + `timeout`: solver did not return within the caller's wait budget. + `abandoned`: caller cancelled or the page navigated away mid-solve. + enum: + - success + - failure + - timeout + - abandoned + duration_ms: + type: number + description: Wall-clock duration from solve start to terminal outcome. + task_id: + type: string + description: Solver-assigned identifier. Opaque, useful for support cross-references. + website_host: + type: string + description: Host of the page where the captcha was solved. + website_path: + type: string + description: Path of the page where the captcha was solved. Query string excluded. + error_code: + type: string + description: Solver-specific error code on failure (e.g. `ERROR_CAPTCHA_UNSOLVABLE`). Absent on success. + BrowserCaptchaSolveResultEvent: + type: object + description: A captcha solve attempt reached a terminal outcome. + required: [ts, type, source] + properties: + ts: + type: integer + format: int64 + description: Event timestamp in Unix microseconds. + type: + type: string + const: captcha_solve_result + source: + $ref: "#/components/schemas/BrowserEventSource" + data: + $ref: "#/components/schemas/BrowserCaptchaSolveResultEventData" + truncated: + type: boolean + description: True if the data field was truncated due to size limits. KnownBrowserTelemetryEvent: description: > Discriminated union of browser telemetry events emitted by the Kernel @@ -2487,6 +2720,12 @@ components: - $ref: "#/components/schemas/BrowserMonitorReconnectedEvent" - $ref: "#/components/schemas/BrowserMonitorReconnectFailedEvent" - $ref: "#/components/schemas/BrowserMonitorInitFailedEvent" + - $ref: "#/components/schemas/BrowserApiCallEvent" + - $ref: "#/components/schemas/BrowserCdpConnectEvent" + - $ref: "#/components/schemas/BrowserCdpDisconnectEvent" + - $ref: "#/components/schemas/BrowserLiveViewConnectEvent" + - $ref: "#/components/schemas/BrowserLiveViewDisconnectEvent" + - $ref: "#/components/schemas/BrowserCaptchaSolveResultEvent" discriminator: propertyName: type mapping: @@ -2512,6 +2751,12 @@ components: monitor_reconnected: "#/components/schemas/BrowserMonitorReconnectedEvent" monitor_reconnect_failed: "#/components/schemas/BrowserMonitorReconnectFailedEvent" monitor_init_failed: "#/components/schemas/BrowserMonitorInitFailedEvent" + api_call: "#/components/schemas/BrowserApiCallEvent" + cdp_connect: "#/components/schemas/BrowserCdpConnectEvent" + cdp_disconnect: "#/components/schemas/BrowserCdpDisconnectEvent" + live_view_connect: "#/components/schemas/BrowserLiveViewConnectEvent" + live_view_disconnect: "#/components/schemas/BrowserLiveViewDisconnectEvent" + captcha_solve_result: "#/components/schemas/BrowserCaptchaSolveResultEvent" TelemetryState: type: object description: Current telemetry configuration.