diff --git a/server/cmd/api/api/middleware.go b/server/cmd/api/api/middleware.go new file mode 100644 index 00000000..cc370478 --- /dev/null +++ b/server/cmd/api/api/middleware.go @@ -0,0 +1,91 @@ +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, err := json.Marshal(map[string]any{ + "request_id": chiMiddleware.GetReqID(ctx), + "operation_id": tc.operationID, + "status": ww.Status(), + "duration_ms": float64(time.Since(start).Microseconds()) / 1000.0, + }) + if err != nil { + return + } + 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..f0f46367 100644 --- a/server/cmd/api/api/telemetry.go +++ b/server/cmd/api/api/telemetry.go @@ -3,8 +3,8 @@ package api import ( "context" - "github.com/nrednav/cuid2" oapi "github.com/kernel/kernel-images/server/lib/oapi" + "github.com/nrednav/cuid2" "github.com/kernel/kernel-images/server/lib/events" "github.com/kernel/kernel-images/server/lib/logger" @@ -25,7 +25,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 +45,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 +63,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 +92,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 +150,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 +170,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 +204,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 +213,7 @@ func mergeTelemetryConfig(current telemetry.TelemetryConfig, patch *oapi.Browser events.Network, events.Page, events.Interaction, + events.Api, } allDisabled := true for _, c := range userCats { @@ -204,7 +233,7 @@ 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} @@ -214,6 +243,7 @@ func disabledConfig() oapi.BrowserTelemetryConfig { Network: cat, Page: cat, Interaction: cat, + Api: cat, }, } } @@ -238,7 +268,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..1f324619 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,33 @@ 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) + +// CDP proxy disconnect reasons emitted on cdp_disconnect events. +const ( + cdpReasonClientClose = "client_close" + cdpReasonUpstreamChanged = "upstream_changed" + cdpReasonUpstreamError = "upstream_error" + cdpReasonContextCanceled = "context_cancelled" +) + // 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 +356,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 +367,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, cdpReasonContextCanceled, connectedAt, msgCount.Load()) default: logger.Error("failed to connect to upstream", slog.String("err", err.Error())) clientConn.Close(websocket.StatusInternalError, "upstream unavailable") + publishCdpDisconnect(publish, cdpReasonUpstreamError, connectedAt, msgCount.Load()) } return } @@ -359,6 +383,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[string] + go func(currentUpstreamURL string) { for { select { @@ -373,6 +401,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 := cdpReasonUpstreamChanged + reasonOverride.CompareAndSwap(nil, &reason) pumpCancel() return case <-pumpCtx.Done(): @@ -384,9 +414,16 @@ func WebSocketProxyHandler(mgr *UpstreamManager, logger *slog.Logger, logCDPMess var once sync.Once cleanup := func() { once.Do(func() { + reason := cdpReasonClientClose + if rp := reasonOverride.Load(); rp != nil { + reason = *rp + } else if r.Context().Err() != nil { + reason = cdpReasonContextCanceled + } pumpCancel() upstreamConn.Close(websocket.StatusNormalClosure, "") clientConn.Close(websocket.StatusNormalClosure, "") + publishCdpDisconnect(publish, reason, connectedAt, msgCount.Load()) }) } @@ -394,6 +431,39 @@ 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 string, connectedAt time.Time, msgCount int64) { + if publish == nil { + return + } + data, err := json.Marshal(map[string]any{ + "duration_ms": float64(time.Since(connectedAt).Microseconds()) / 1000.0, + "message_count": msgCount, + "reason": reason, + }) + if err != nil { + return + } + 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..9fe5d136 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,13 @@ import ( "os/exec" "path/filepath" "strings" + "sync" "sync/atomic" "testing" "time" "github.com/coder/websocket" + "github.com/kernel/kernel-images/server/lib/events" "github.com/kernel/kernel-images/server/lib/scaletozero" ) @@ -127,7 +130,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 +398,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 string `json:"reason"` + } + if err := json.Unmarshal(captured[1].Data, &disconnect); err != nil { + t.Fatalf("unmarshal disconnect data: %v", err) + } + if disconnect.Reason != cdpReasonClientClose { + t.Fatalf("disconnect reason = %q, want %q", disconnect.Reason, cdpReasonClientClose) + } + 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 string `json:"reason"` + } + if err := json.Unmarshal(captured[1].Data, &disconnect); err != nil { + t.Fatalf("unmarshal disconnect data: %v", err) + } + if disconnect.Reason != cdpReasonUpstreamError { + t.Fatalf("disconnect reason = %q, want %q", disconnect.Reason, cdpReasonUpstreamError) + } +} 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 2269d704..ab1d12b2 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" @@ -667,6 +835,7 @@ func (e ProcessStreamEventStream) Valid() bool { // Defines values for PublishEventRequestCategory. const ( + PublishEventRequestCategoryApi PublishEventRequestCategory = "api" PublishEventRequestCategoryConsole PublishEventRequestCategory = "console" PublishEventRequestCategoryInteraction PublishEventRequestCategory = "interaction" PublishEventRequestCategoryNetwork PublishEventRequestCategory = "network" @@ -677,6 +846,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: @@ -694,25 +865,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 @@ -767,6 +941,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. @@ -794,6 +1002,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"` @@ -1118,6 +1425,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"` @@ -1792,6 +2152,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"` @@ -1811,7 +2174,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"` @@ -2419,7 +2782,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. @@ -3311,6 +3674,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"` @@ -3325,6 +3856,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": @@ -3335,6 +3874,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": @@ -17042,283 +17585,309 @@ func (sh *strictHandler) StreamTelemetryEvents(w http.ResponseWriter, r *http.Re // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "/wEAAP//c4aJWK9pAQA=", + "H4sIAAAAAAAC/+y9eXMbt5Y4+lVQfFNlaYakZMfJvGvX7w9FkhNNvOhJcnJvrvIosPuQxFUT6ABoSXTK", + "89lf4RygFxLNTZKX+1x1a8YRG9vZcXCWvzqJmuZKgrSm8+KvjgaTK2kA/+NHnp7BnwUYe6y10u5PiZIW", + "pHX/5HmeiYRboeTev4yS7m8mmcCUu3/9h4ZR50Xn/9qr5t+jX80ezfbx48duJwWTaJG7STov3ILMr9j5", + "2O0cKjnKRPKpVg/LuaVPpAUtefaJlg7LsXPQN6CZ/7DbeavsK1XI9BPt462yDNfruN/850QKNpkcqmle", + "WNAHifs8IMrtJE2F+xPPTrXKQVvhCGjEMwPzKxywoZuKqRFL/HSM43yGWcXgDpLCAjNucmkFz7JZv9Pt", + "5LV5/+r4Ae6fzdnf6RQ0pCwTxrolFmfus2P8h1CSGatyw5RkdgJsJLSxDBxk3ILCwtSsgmMTIA5fUyFP", + "aOTTbsfOcui86HCt+QwBquHPQmhIOy/+WZ7hj/I7NfwXEPX9qNWtAX2Qi0OeZcc3HuFzkJTs54uLU5bw", + "LGMTLtMMUjac4WGuQUvIemLKx2B6PBfMIGEtgjLldiW5RLZz5IY5ElGFTmDNCXDkOY342O1YXciEWweO", + "+bNd6AKYGOFZ3A7ZSECWsltuWDmKpQU4xBrxAVgmpsIadzwPzKFSGXDEiY0QCm6FWTEFY/k0Z0Ky91Lc", + "salItDKQKJnibCOlp9x2XnSEtD88r6YX0sIYkEXpL391QBZTRGwuBg4nNcwaq4UcL5CANWHCEpBrUsOR", + "x9oGjHcKuoekkvNZpnjKRkqzq7DZKwZuXhMhkEKjiBlMI2D8jWdZL8lUcs3Cd47tHNqIIrWD7FRkmagB", + "1Z9QFtMhgdCtR4uICDG8y0EenJ6w8quTNCwydbIEUqaVExo70B/32VWuVQLGOD6/6rIry6/hPNEA0kyU", + "vdqt7SDghdACxkbXd5DzvzOROqk0EqDZSKtpC7OFr6ciTTO45RqiixrLbRGBKrJ10MSMvmKJSuuzlAQ4", + "R1O1g8zBtVyv28DpEopz5HZueXK9uMXDo1N2VkjHQH385ELzBJiGXINxIJJjhM3/8Bt+juNIThn3LeMW", + "f3SjUUpLor4+e+XY3LDCAHMrSD51EyVKup9RkmtuJ6CZnXDJjOTXMEi4QTmAtIDzHk60mgI7gpsLpTLD", + "TrWyKlEZuxUaGLF0/1IukLrb4SvNp7CGZsHTjPDjLnPUp6fKWNIiDf0xt4TKiql8S5S/sMjvoFVvyA2k", + "jD5kxCPsVtiJID2VCRmlg25nVEjUKW/5FBbnrmEifOjgC12mNINpbmeMKBMFA5dKzqaqMOXHJkrCbjdr", + "nMZ9FjkLfR0/Df12ksZpj/67xo7R3RU6Wxz+/uy1O7I7exAjfraRyGKMOsdhDTDX9knLNUDSbeI7xmpN", + "G2FOaC9KQhL2LONDyBBRuH1kKoscSDKQm5lMWMILA3F5l3MdrMgsezfqvPjnWhq8kggf/1hQMDhlYzNI", + "SbgV/KvpLwCzxnJLBVFukwk/V9kNnIEpMttmE7GEPmXGfcu4tY60mQaOeoIzx6jCgVAVNlFTuJdF1LKv", + "b8ZRq3Hk0TNA9Aw0wuyhDaVlWNncZgok1DCbYsdoN6HC1wEYc+LMU+wNyFRpNuJTkc36TmmlRQLaMOnA", + "nDlE5lrdiBR0z+SQiJFImOXmGkWZYUJaxexEGGbAvmDgrpS5FgbYDdeCS2ucuNMQOCRRWcZzA2EgCM1u", + "QBunGIZFcg2W7dw8Y3vs5rvdLuMyZVzOnOgeM6ksS9QNKkQSOA64R8ppkzfWH6jL8owLyd4dnu0yYZxt", + "oLQjTW7YlXJa/IqUcKCNid9ZxyE/wOzmWfM/v3OUUGhprMgcOYwBrLuFdjs4ZYSWupubsGjakQQxlmvr", + "OCkmOBYMWbw+DpyptrgQ0mMNdfgtmnXuCjriIit0acMen529OxscHpxeHP58MHj/9vzd618Pfnx9fLXb", + "ZwdDZ2G5QaZInKW7kXF5MX8OduWnuXpBZ9ZMgwMxysvC8GEG7ge8M/fZld9p7GvpD7VjANhVBQy36ysn", + "T1Rhq3GpSJGSaHzdLnBaAfQTw265sGxYpGOwfXbFh1ymSkJ69cJ/whIuE8jczdfrwpyPgUl+I8YoBvkt", + "nzkzvIdrNunNH9sJMjqSAyNtstPtlItFScrxXfSy4LHMjRFjB5OahcLe5fzPArrOvB0VpL5NkTuuYE6w", + "mp6GEWiQCcRRegtDIywMJspEdN/PiizTEgq3E9Dg4Uks71QEAiJdOn/O7SRyDeJ2sv787P8pQJcmJdwl", + "WZFGl10wCGqycosrS5ofKikhse1eE7jzzrYkE46RiOWSwlg1Bc3Oj37pstOMz261GE9sl50WeQ4WQO+6", + "m4ibG1JGIhNvKb/B8FyhvMy1upuRQ0kY9uubRVXwzSRYNAnSfODh+uCWQJofCZNsShBpOQbS6pq/AtXs", + "lAu63ODXYjqFVHAL2YzlGhJIHR9c1Q57FTyPxt1EjNXAp/ezRhdO+80QXUp1FZ4fn/C2tD6rLc4ZoI3t", + "P7z3zk/s/rKOA28KxvAxDBJVxHiM7r9ubsdE/mNnEWZ85pQ0ar/IuiDQ2ZMKTX+Lewo0cBO7Lf82mc3P", + "CdIpIXZFjD5IMmWcIYNfEe8LKaxAwqU/KuMspCIn/hwkEy7HaICgk0kUU6YBbURIyc4Agxa0s5dRU6Kc", + "sEoDS9WtZEbVV0tUkaXOJvc45mMupCHvmIRbFtatbwHNqqsX5W8sFc6a0wGuLC+mORlidFYlLdzZQWkq", + "+QMHJ6X/Hdm2Mqd27CwXzsia+acDZiaFdUfYbVpRdVB2up15SNX/hHtCp8jcjlazX52O58mtpIBlDKmk", + "URngw1er72BI3zqIuI+9Mas0c7KsGE9s3Z0JdwnkRFTkuzyeClspjFvl1IgVMrFI9CQzDCmIVIzQ0LMk", + "Ns2E52D6pUPVr39wenLICRn+L31/Z+BZZnYdabkbomEZ3EDWZQ6mXcb12NB1DX0uA/TEVHOX276YaEeP", + "O+XZyl/qU9OcmZDQ9S7Jrj/KoNBZZB3vwXV2vX+fdNcHby3RSMY1MI6XmJgXdhONN4/VbwqvXeERrDwT", + "PrS+iyJiU+8ijjwk4dD52J33nTvKjrBtlpUMy/W4mLqZWaJAJ2Sm0wFNn53S0wRTMpu5y4v09OhZto37", + "Gt78xYvgnP+WmCTi5Wn48xv+79pFqhIqSFPIomtvfI6148oSZUXcpx6g6AaxG565qyrPbvnMsEvybFx2", + "7gXF6OvB4l5e1x4LPh+gKinX8oSw8HTA7AQftjTcNvf4ABtr+HWCtF3bS1067bsd5K1FuYN6JRgQ7ptq", + "z0KyobKTILxzbidm9T0e11mUGH8syIzXary2Qs7UmLRtpREzNe6G3/tCjlT1X7dcyy4Dm/R3+w+gZcJG", + "v+mYlTomU+NH0jANJHxZ+mUjNbFEDLdagW6OLsu5MXg70aoYT1ghRyKz6IlHUUJv333vfb1Cx7sqvMeq", + "YQP4OyNzFw7g6UvGs4yhE53NawPjbDngmjn522fnQN4Qk0NSPkKOiixjjhDIpvs0cusVBm3No2cRO6vl", + "FSGku4bcalDRwo78R15MhbsVcloVnhXk2lRJYd0VQ1qF4D88Ou0FzeCv9OwkeJDphmy5HoPtUuwBGeDe", + "3Y13kVwlE8fStxPhoyFoJypJCu0uhBGLG6eKerMdlvHXeuBLzVFPm4nrdsVT0K2zpiohXNF3tfm77kYN", + "+L4BPJnUThddR/KbgYE/F1d5o6SySvpLrJCJuyXi61UFLgozTIK50aXP3L4gLTdgVd5D8qiPjAJhDZHp", + "/QOtcAn+g3qgkecwWqfmzojCg76Kzh9o009UW2LHWLyneU9MdU4TDsqZ5cPdZSsGZbAGZ1/giAs3YFmU", + "hoYMbrik57eJMETKL+n1wX0wwjiOEieOF/A3Yp1u6eIovwV7q/R1zVu2XCjUkFUHbPPIFQkuUV91/b+h", + "F1CrG5DcEekULEeTwGNu5qiZGN1f2DUD74UoOX/R9IG4uRUenGsvlCg5MFDGP0u26aYrBG9depU+FAR1", + "nHCuhUzb7JNwoD76OoO/LRbU5dVY6af3wrXPrigwb8BzcfWC/YL/wQ5OT4JDa8fJGX0D5FKlP/bGIEGj", + "jRV2zq7gzoJ0hHD1ggn5L3oX8Pspf+uzq0wlPBv48MOrF8zMjIUp839gupDSYYxnSo6NSKGx3aZTLc07", + "3U61f/dTWKjjZGttoej7ZCCVdmKLGCmr6CFoMyIGJ62ID/Y8n+yRqjg5auA78MIcbyHyl3DMz9bmP4PT", + "Dab9EFYXCwyD0ZMTGsmmPHfYveU6xciDnvCU4nbvRJsqbBlgQUqG/equvga9VDUnKFl5bFhYNuUzNgTG", + "5Yz9z/m7t2giNayehcNgfD9FfB9mIrleeeMp8NrjPg2WBM9t4ay8G8ErIkRpV0XRbX3Fie7v20Wn9aIj", + "KngNEEsPfd1pR8gDX3oMZJBYFQnfPDw/Z+FXvPUHLy4e2AnIDC2lFptgHItrfvOaWT5uxF7OzeawVOQ5", + "aAzrJUnz4/uLi3dvu+ygy45Ofm0xQqLW+K/CCPQ/O7HlU2daFu4yq/HRNjr9XWxuuMXYjbteopROheS2", + "eSp3FgfFXNxBZuJuptmSiWfbTzxHfHcdt1K3wjZhaOk9p0aCv8BspcS6htlQcZ1+ankV9vZNWq0lra5h", + "9oiyqoGMB5ZUbucLUPsFZuSqruy/XzwhEkBJghy7LXbZjzy5NjlP3L05Lka2EIdBcKH3d4IP9ElhyMtL", + "6SEzJJNcgzEt4mV9cYmTLxeXJ29P31902cXx3y8Ozo7bhea8QQb3kBDniVZZdg7WZpCulBUGv2aGPvcS", + "I9xc+MhWn+TKiFquHj4qCznufjr5sniyb5JmLUlDGBx4JD+i0GnB0AOLHydfBhEzgFZnd72SVH12E0UO", + "V89E7qsxGEe16xgGuN6sdb3ZQ6/nXRpbCEBaa5VBqGLAe4WRyGYRhCgD3OThBEFWrHMSFYNbY6nZgyw1", + "nxhEFFKizh/ab2gRwktl62txA84QXBHNyjJxA+xGwG0VUjQXouquwqMiC8L3iWG/wfDs4rB0g7yFa7Xb", + "Zz/775TMZi8xgCNI5JHSOEsGxjBKc7yXdI2d7ZtQbRWqDsUDh+LHCpNtxcfm8YrBfd0IVlw4QHu84jL3", + "+OuS1Bed5H123vBglyF1psuMYpxZzaVBBglO4GEmcpZwiWSOAVvek1jG8GJg7lW1pauNPMZrAHx1cPIi", + "f8eDk9dl8ipIOYaV4WzhuPdm8m8hyZvz+eMFJi/DyoNz+xcUoLytXHnpSwyE6GRNafkUzN8m1zZ8WFoz", + "teUNPRYf1fi/RWpc+GyFGoysCg8WjhUyZWyfXaC9ZvUsCD7v1061ynNIWSGtyMIb9aCUqO6KprW4AdNn", + "Fxq4RUe4kL1cq7G744bCLhgZaoHteIk7EGmGAQxjGGR8pgobLge7jBtWSA2ZQCFOK9sJyHuJoDaIfZNB", + "rTIoYLuuZR5aBi1Fyyoh1CSGttD/M/x7+XJenQYfeBLkhEEZuF8+LpYvdeGXfv1Nbm7UarCsDkv3oDiR", + "wr7iIlvJ0UFAUd6As9GH4FMWMvGB9ntfdpnbzDdmWcksDgGDEYLskXglhpPNOMVYyNvpagp2ojCHtiQm", + "HyBjISfPJp3PuxgpgKNvwB4UVh1Yy5PJGi5G3MTq054FVbMWT0S1XINBNPQAA1yEmZQORrib8MJYepDP", + "qgsDeVQw59/02VvFRoWmkjPz6vJWZJlXhWUioGfQh+DDGBS+MeNKZiwR+bgc2YqdR1FgDer02er96q8D", + "T8xOlRExOzINVMxuQQPDV4MiL4MefPb7qMiyGSo8pUPRpiZX1XVgZMUHVINncG/Ldu5UEb7n89bAMXFz", + "cHalRQmHMc8xCoTM5cOmVYsVLQxY9C/MBaEFF4PVPLl2s3mjgY00mEm4tQvDciWkfVBh8U1QbCwoHl9G", + "3Ec+BIZb96KMRdXmrsTM8mtAVqmlmpZ+7yY/rAPUBQaPbXI1fKqqfK3urxy0UKlImCm/DR6A8Jh448Ml", + "HoKN5nb0jYtWclGFl0diohhKNuOhXEYe13/kBn543gOZqBRSdvr2pzVJrITVcGZhpcXr1l5yxrekKE7S", + "DFY+mgelItIQVjv3ZM7Z9/v7U8P+LARYzznk65WKCdkbZWI8scxXl8TI6Pu948y9mH5jk0U2qbu+HppB", + "PPG8VjwVcrz0rrRIRRmNCtc6n8N+MmqUBnAg5pkGns4cUDwBYWSLs8I43vvcpVAqlmuhNLsKB/ZTXOEc", + "9YdEYXe77KrQ2VWXXYXME/fvMmHkirJarjT4HEwHgKta1vhLdhWhQMx1yrmmotEsV3mRIWlgmga3LOEG", + "7plw3gryb5piJQt4inuka9lyzDxwLAgVrliFqDoXhRHzGWAYSjGOVEKt4YvqqMUDXN+GjBbM6Kv95h01", + "EuyLF8dnZ4PDd2/fHh9enLx7Ozg7fvX+/Pho80LIjucjhZDxhSTcmZQWYyE5+lXmZEHr44hbtcbq8YX9", + "Sftn/tOLWQ61+zGusJAdWQ/494mRv0h1Kylm0DAhsS4ZO/LZaF32Cmwy6bK//3zWZVTpo8vO7SwDMwF3", + "2TuZ8jF02RtIBe+yV8qNuYA7e+Guel1WY+luVS2qy95wKUa4w1MNI1rjnZ2AJlk3VXqNyrON2s41quhW", + "BLk0psSDMPR0WFdVBPRhNnhLTtHmMrS+i2/Sc6X09Eh4JLG5gIwHFpgh23NlOYUyLRQ1drMYkwdBVIBM", + "aplCm+y7nmW0WNLYgyVkE/XdSn5PjvdaZdVJ+KaPtTSETLFPB2broSFSmOaZthZcxouonGvjhEmuwelZ", + "kiqYzB0FlzADDVRWaxm7oI/Ly3vj92uKjFprsDBDnE/oSaGluL1/b+CGhVKmbnIsz05666fjiy47fXd+", + "0VK+Whk7CDInjrOhSmeoH9wse6fvL8o7T9cdjt9wkfFhBi36iI4Wp9d3pOMyzCsdwkj5oiRhFKIBD4am", + "cg3YCEZdwAOp3i4rpPizgEZN9eoF4puavb+a9WTcbYqwSuAsCIT1NDD1dthABftmEBoSEDfVhe2V23TN", + "l1d+iOTvkOI94TSsi09iSJUhQ5IesB5Go9dO9U2lr6HSCV6PptPn0fHASt2RWBQzHvwNWqxkItZAQokC", + "d5a9OXlzTDVGPqle9zurK/Z1FJa3UlRQAMtMkqmYtgna8tBhwhJUpP0cZPYmdpp12XyTr2+3ti9enTxQ", + "Y58wTcvNPzpXLT3/3S9dVrZz291W65X1twMjLlVvp3wMR2p6SIm2rxVP1/BIHr170xgQKnw58nET9tNy", + "RpwLVd79Knq17vOb1mrVWhi2marpwKdRoz/v4f14y1Hz0H68NB+UwIoIMIormIZCQoxeWCnfVEgWXle5", + "9VVYFkh55IDQxULHVtwgXgPZh1hDCgzYcXYZogorOO322XsD7Moaqqxy23zfjYQ4z5fRb5xsJdO+xnDc", + "ddM3KXi3JX3zqQeLN0rRu4nB4dVLlAV9A1gKJcw0ESO8l1UX5RthCo79woYiE3bWZ8c8mTQGUPwF3Uuf", + "9vyq7tD626vWJ5AFzRDux5ADniodrleXiCymhWeyBo3sHL4+3/UkWmbLnILGU8sE2IWYArYnOzg9ubdS", + "md/xN32yHg05gH0KCnoU36aPeVmE3tFczkqDMEFaPVsI1NnxBXf3Uew3xCPLQWPJxd1ohksdlIMULBeZ", + "2TylJ7BFDXCMW6vFsLBgVnAQHmmRhyY8HWhInM0gZF7Y5XTcAJKvm5BASk9nWBYJJwkuLwx56PoGM05x", + "CM/nh6/P43SO6juSBVRf1yRKh3uKMB5XO87yQUiEuMPX57txVbxAk/6itGGlxVDzAf9eFT9ugKgs7BjN", + "uhaxVpBR5FVMHqPW1TlW86Hecwf2e6myndYwSpJ8pdh/zfXYXVK92TUqMnbKhbs+vD48/YRy32/1m7xf", + "Ie+T/FHEfB38DyzesyTfUpx62qxIkyjzvuLUV1mIShGRVtMHPn59eFrVuBKj4IdrLdo6iAsNd6MpO/bO", + "zbtWCqZUabvoO3r3hrkPItKvtk5b/xeZgm7Z9hn+uO7GX3rFS+3YyCvmKx6UgfMXYirkuHeQZeq2R09B", + "8ZRT8QHaK5JxDbxlQ1Rwgpk/C96U69Xcq55R6zNi0JU7AlOa3YgUVPippQLq4yqv+tac4CLsPYL+woVi", + "RtbWymu1xlJ89e25uhHPO7qyMPyBXFzldr6ppRVqSfHHucA2EPCFO6/Q5qvI8mtxXb0tU2/W47x69W/f", + "t2ueD5Hv34Yeort9dsi1FoB1scsiuCNqdCQkSp8hlpG1zJeC7jLsShJKVtc9VfPF2u/N5XMA+Mbry3m9", + "gv9jcHwMGZtlK2ynZauOt/jFphX538ItW16Vn5Udbctb8YrC/NRifonV4BvGLxyJ+uIO6e+1UvQvffg3", + "7SBSlL+lEfLGFfcfrK7+py2XX9GAVQ9W256iXWqWUEVFa7PC8neF0DcPo1JaXpnK0v5zXmc24TdA7YlQ", + "X5VPy6ZJO42nhbKBsTCsNj29OGCpb4wLYycyhdxZp1QzuJ7K8ZJxZoQcZ8DcF5TiSc/lqQJqfzdEnSfu", + "2+Pu23PEpnL9MZ8kLvjwXQ5yySOZhNvS4LB8iH3JSS44WCocTLaGL6IQsmguFP0BaRjpk8aZXQrzMiHU", + "kDdKgQhT5eH4in9uC6FNjFGNkl6rcm68/dLMtqkZMiV1I805Oy+WidNnh0qaYgra3e8o0WjObsI+C6G2", + "/gSrNVgsJiSss504eroFzx4ia2cRcd+MpOXMZPlwQKT6+Ey0hY2EW4tbMhcLvW28heR4EYPKPQsiUSsJ", + "FA0sZ5sq/ar9RaxXj4TbbFYuxYePYglYYbOIe4SizzMvQ9w3pZWIgiG+mahZEaaquZba55ini6U2xRIS", + "qR1yGdTr8XN0VIdd3zdmrgFOnbg73c6QJ9djrQqZDvxfDOgbkcDAaXjQ7g8TriGt/htj6aOtVsKuQ3mY", + "Q25hrNx98VDJkRhvUVcvoSlmtZozvt41Bl1gIxJHacN6s7FILi/PxcZeh/lzzPwpFqMoqaNOD32LvYzP", + "3CUhseJG2FlohQksKYxVU9AsxZJxL5iQQwd4bMSDjYyD1rquTWd6PBehdqVTTGX3m55vq58iRJIJZ0Zl", + "N1Ar2eNIhLpuu4HZQnG/TIwgmSVlbyJUdUKONDdWFwlCuRzpg+rpVbanJLuipj5XLODI6Sbs+4r9jh4R", + "3L4DIlOFzQvLdrDzpe9xqbXSu7jrSDtnn7ZS1st+xD2+p/fZcqkA4h1sW2G67BpmqbqVpusrMe/i5rxp", + "/Ygbqye975VRk6FLUp/uq+PHRN8pun3naW+nfkehzJ25mKjXh6cOSB+XyMuWPWwmd2hQiFhw0qW84tSl", + "kCf6xVcp6S5NaawLP1D5LKdvS8GG/6YK/u6ixZr5OcpdCemqJow3nlIY8SKzJC6cfbVTTubX3qWZDi4O", + "f14x1w72+6J7GxYAzu2MEVzZ1V8fr3bRqmZS9VT+ksRYWEuD5UIaJqxh+PAuLRUE6rML5XfibP5UGKpH", + "XA29EZx212UzVbBpQTmVKW7hLs9EIiy7cme7cjNcIZquGo2lSrNwLXLYhgyqoqBJhCDK3moNLTWvm/rs", + "3dSZ8dXREd42IOoFIdCqcqSwfXZe/wD35r6gwA76AmetZ09fg0O+FRqyWX06nmVhbQGGpnZ/GzmRXv2A", + "8y+smGTAfcPFOCxitxG/o3XNuFZbIYpY7AD1RhUGfHLbhubysLA2Fp+EUzL61Z08CG586qxZTRmMbKfb", + "0WI8cf9/KtI0C2YUXTJuuU6jxhHK/Zb8gAtvwFFLI6+bqlWdnnDGYt7x00QXmKgsHVzDzMSOl5JR7n52", + "53Pf1ut70aybtO2VxZT6ffnlUCR1Xjyd56K31L3cWafujkaVl3IINpFfd/EyFmmR8HfW1gQqtDBYt6/U", + "P7aZKdpI6o84kebYd8fHO2xIo/Ekh0MvYZMweaOT17ZNyLsdX8tNH1QG0foi8iDoRV/nWHvaJXEFSWGB", + "cSpFQomyQ26TSZ9dTIBdUS0TUkPUCcLQ+9ulrGbJ6YGbfDKLhadptAMCqiI0UGlszjWfggVt+pfy+I4n", + "1t1CZfk7jWyk/uA1CnXREAvg3og03iWYWHnqZMYqMbcosD52O6nm4/WGH2k+nh89VTew3ug36gbmR2Pv", + "pYHvILVs8Kn78BeY1caSobpqILVlqQ8DO0gKbdRKpXAO9hA/rI/OgOq1Lh3oPvIkXPPkLJZMDFflBQpr", + "1Deu4bcBb5o5lJqoQFmCpoHbxsnDQWKSu5p0xTGdnriAO1uCZ57L42m33c6hBm7hCDOvlZ5tpzynKo1A", + "9V1Ow1kaZmfuQ7ajEotpCxq7VGEm1n9///1unx3V7Nf//v57tKDdpVW76f7ff+73/vuPv77rPv/4H/G3", + "NDuJOJuHRmVO2lSbCH15Ejz63CJ7/f9cXbDMrRQD5hFkYOGU28l2cFxxhLDxFJd5+I2fQYK6b7zd7mMe", + "wJMFH6MOi9ROwg6yfMJlMQUtEmcIT2Z5KGxfwz/vfTjo/b7f+1vvj//6j/XCso6EyTO+rpk/F5MNaMy1", + "KtyU5mb0XRWV1hKAh3VNB5pbWD2l/5pprKIq2c8f2I7vPCCLLGNihA8MKVhI8CVuN7rorUhjBDW/Gn62", + "dP9R0M5roMcxuJ3YbDG2SyObrO6YAE0h47OGHbo/b6ocuU8WkgyGYG8BZNiIM7TR0sB4GE+9Tv5Tp2rv", + "YLUYqTIVUkzdRvdjOFlao9R75q1yArJs5zG/t+BAp2sdQcjtZVqW0zBTpezk/2AZDboS4t20sGrKrUic", + "xe3OMOSGevjTgihfMpBjfw5+R+d4ur+/v1871/fRg93nluGOsNElIy4p32kMk2SZMGhW/vOuy2Z/1E36", + "nAttStyFZOrbichoE2Mhx332xpl63nZk3LIMuLHsGZUiRh9yudP5LdcAMuV3J/TrMwRe9R/zp1n6I+Gy", + "QcOxlt7vDbBJMeWyl4lrYD/CB4EpX/oGKmpGDN/yGR2ECWkscEzZz4R0V3q83uYq822+f8MGnW41rHJv", + "BjnogYExUhqxA+QDZLLB1HuJx1I1Q1Vr73SNzxtH+n5Dvixj7nBfCxg8oV0scsNK/lw4Z/MWu99+jS23", + "hLRF+8J8JA8v7ydHMdG+QfaGtseeNvb6dOW1s1W5H2tNBvac0QbGeHfucqshfBidm+5ypxmf3aIUXlcZ", + "xAsS1W6H1ZSY/b941YranM4OpuIGe//Dbzj9EyeozU3XTPzjhBvGsRy6+/1JzsfwpMue+If4J3S7fBI6", + "hrEbrrH7jr86TvMMXrDLDr/lwuIDW3+srNp5MrE2Ny/29oC+6Sdq+mT3JdNgCy1Z7XN8etzZfXnZqbsw", + "m8HdFMuTNOjwhwU6fEPS2p8RrzC+ynV46w7mNROG/bDfkPDfNeT7alpD4K9JDwY3vCE5hApac1RQnW7R", + "uR6ofC4KAIs+ehJ2dlMFH19kM160w2968Z5IwcGEyapWJW5uh17Jd0mMpKAj+zm3XKbYAhw3Vma31A8W", + "qZWRqlhOYDmZf+9aczaq/7/sGQLq0Ia00TIg7mlvhM35BWIE8kpkcCJHalEeCTNIhV6+K9Rf+O5QXuda", + "Kqup1hwdp8qnaJBQyZgy5LoMBEm5hZ5PxVssWROVO+5YdLsdCmuovEiXXXZSfXune+5/lx13sbns9PRt", + "T/fc/y478UI1ksf2/SM30OxyLcIryiIk1r4VB5t1kUjEBxgMZxYidHIuPqBgwZ/7Ph0obEPAOn1g8Yx+", + "d43FuoEOajj0QG8jp3N8bW4JNnMf0HM0NcaGtuqc65AfH41CD+016XBbXJZLbYvUzagk7hbzEVSzHOo+", + "sMOz44OL406389vZCf7/o+PXx/iPs+O3B2+O14iGohCXVoMFCxnNPwO14PdIuP+aonWfskL6VPIyunC+", + "IVEoweHlNsVnUO6XMwuEIbSGMAeeMcvvlFTT2Qvs4Uchfr4YYzW7sRr4lN1OMN4v5ZZf4YOY0lO0LJQs", + "cY02hNvKEDJ1y3bIw01bIte3f1q9aofDVZdpGHNN7YbVyC3M8iJ0cRG2zw55loHuVX/0AMAX1nfnF2yv", + "3P1eLciD4halsZoLGcIlhSHIvmQGgF3N7aW8j2JtSjPhOfTZrzwTaZnZn+BmQg9Mw/iYu7sHTR0AHOpn", + "Jj4u8okJtZuEr2+CNlJaYZwU/pTnuaD+BTwXA7fWirfFg1w48BBJdTs+SmaAUTKDoPyXznBIQ87dCLJW", + "ysnSvOxBvGKONG90rqaxtb6mK4fP98YtQ2wG3hpaPgF9ixbS/PhMjdcb/VqNw9h6Y3t6vls+Q609PT6G", + "xObB54h1Z/kFZrE55prsrz3dYs/8TjfSaHrzPt6NadbGd1tD5G68IeV2bT9rs9U79m3TFLE21UK/sa2b", + "u8Um3Xi+xblqHVq26YHT6TabWKxVCrRqaNJtq/+/ZaOF2oShJPbG5cYbc/ganJtXOO10W2uibVl9Lsw4", + "V1lp7bJDTW5eLLCzef2icpok36AKRjlK8XSTNOUwrpait3H64+IcG8CxJWWpuxAUv2m+Ab1Bo0k+e4tm", + "M1mGH7sdJWH9cMN5xfSxu8mwmjZcc2CMeTYdWmeZzcZGuH+zCSoxtOa4GEFtMDTO1RtMULHCBoMWSG3r", + "cmUbjQ3Mvvl6dd7aCjHbzBC3yDYfXBpimw+NGF1rTtKimjcbvWgQbTZ+wcbYcvgW/Nxiha05unElWldk", + "zl1g1h82b8OuOTJqTG84dsul2y58WGrhtTAWHU0Rp4zWfOauwIsuHiHJ44g5ANIGz1n5orhsV6UbNfI2", + "Wmq+SOZlpsa+4EvpK65Vz170itUeieZLHY1Lr7qFO9tamqal9MaFmPpCbeWOqJAdpSat649teaqqLx3z", + "MGGQwamP6Dwr7dt5l/S6oaYhkGv7ENO2GdYOLV2I6NssGuMBoxIwxO2e8QipMJbLBBqPVN8/dhSC2/NG", + "UQj3f5r3nuTqHd79k0s7B8W4c3kVeVZhDoHCmFVbkem6M21ErtvHyaVg7GBVvB8Yi+X6lSxfOVaFy3U7", + "RierJqb83bXnnH8bCwt0a6eIQejddV0ubfB4+hNljbN3v5Sl7xflurpeSbUnVA0Cyobh/dUvf+o6epZT", + "bpOJD8XbDuNtsXhH7TF4paB49nx/84i8o9ZIPOwAqugZocsKA+S1nojxBIytmibRkKqPA5JPs1f8D/vd", + "7/a7z77vPt3/I75FBK13Kq3C18hH6mgYFZSmowEz51EEV0meSldBmHsa8JjCUF4qxCWN780+SCZaTYXb", + "+1/tq1PJq0P/qa9xXp0/vMNhYpGhzCbGU55T3K+EW8z3b4QrUOKRg+UEeDoqsi6lR4W/ZC3k2RoCedQa", + "+liSzXfP9tcLhJyPh99O864IUgxaN6gtygaeGYpMnK/eVyNRh+79Ln3LNTDL85zsq+VxUEsUaRnYPV2l", + "Ua9hhrU0DTMOOF6jr69g4+u/9uF9bnYzmw4VJUjjQr4Gvlsi1MwYAuO1b5kp8lxp/+J2lyqrVHYpdwwA", + "+/vTp3iW2ZSlMMKuVUqa3T7zwT5VX5XLzhmGgFx2uuyyg+4B+ueh1Rn96yDzf3r1/WWnf0khfhQFJgzF", + "KCa4QZ4Z5XaZqOnQqyzj4+Jpvv+yIXoA/wtX+68LPsRpNwDonLRG6EblNRWvO76D5MHiubg73hRjBmfS", + "yRGpCpNFsmS5HjdDA/8ZSfOmmbgeF2WRzvWpipuBVqoZ2Bc/RuFD9nwxP2zP4IayXIsbkcEYWsQON4PC", + "5zounzLUwHNfu6lkkaH2CDJ+MVuQzh55rUdAh9xaM4EsK0HudEERL0GW3MYSkpW+djxcXVZ3eD26YNfP", + "6N9raRGq8Tp/gNU2F8ibdvL6KxbT7XH218d5hB3LG6GVxItHGauHFWR8zaAa6GvQqCh/Id5usxC7dgS2", + "R9IROley4b3C6Hid6UqEleeI1Fdcdh88Ls/fdhmMFyGGO2EH8bhNf1TmPlna7SkFrQfDH57Hg2p+eN4D", + "6YanjD5lw2I0aikyR1F1606mCts+2cd27P0iqpS3zdB3LsZOySL1yrJwVY16mygz+HlDqHUujs/edJbP", + "Ww/t8Z//cvL6dafbOXl70el2fn5/ujqix6+9hIjP0BTdVpugGcvZ6cU/ekOeXEPaDoZEZSZevNGCnmJH", + "ukRlxZQqIS6Lee12tLpdNZf7ZMNAbZy1SxtdArHznN/KOsDWqrkRUd2LZXF5lil3tRtYO1utBQ/814yz", + "3ECRql55+p3Ti3/szgvWqmRAVebkBkgjtajLONJCZaV5xNGFpn6Ies/YbVC6sJL7bPtlPkYL8jbxuoU8", + "P6k5jPnQCSTOjJttGT/ksbS8d+clsk6O4qLW/x6t63WORYd6ZbnTSHGv2n5KP25RiLSlFaAzxwfcxv3E", + "VL8NsVEnMz9sA1dxK6uVvQg3KQZTq2xSGNKy7VIpLwZ5rJH0sbFiirGLh6fvWYH+9Bx0AtLyMUQL2y9R", + "o8dBfYbKegFWE066lcC1ykbpdqYwbYv+rXaswSDm2RSmzkak3ZeBwa39GpfofyoTU1NJupDSoY+O3VZr", + "rx2xqZDbKZ0jbrmTZLdakAN0jvQo8B777MTLU69lWKT1VVbXiyvn/WPlme9lL7rt+CRH46ZbPKH7woJs", + "I5IqKwo/YP7zfmddl4o/igZeRXZvYjudH4doU6bBt/NwJwoY9BkTSi/Um7ovNsuHtYpY3CmiJijE3+le", + "N7e0EILtWCGa7rqWaCgFKU0uDLvEgZedNpZ1+49oAXKE+9BnVSvlmUwKeV3fsE9gKdNi1mRiil1G/N/P", + "D1E2uvfh0KGoFQFAeu6eD+eOiHFfrKlpZVN+wYKdTeHz9XJgZRkJXwivqurWDWUX6zXoulicsBvmj+Y3", + "R/v7XzSj3gMn9O9dznVFmsDyst7rlqSgMgSg42lCIyExnn0da6GqNRBGtdkKK90uZAYt/tmURRNqvzcy", + "Xte2bard+kFbbnYOzmhz1fcZg3kVNXMG43XK/az3PPMzPcuUpR/G3lewpFBCi8P+N3TUbzLRmo/3NNcT", + "46vlj5yQ1BLu9Zy/wZzRF9MAhW4A7CqUbfPwoEtEr6jZ0ySMqKRuVvbZ9DE3s3xwt/z942elxQclsW4M", + "rsX4VBXS9hlFcbj7Jf7dMMwW7TIJY974u8NDXMHRDlaUifjV7ThZY/1U3crI8kUeX/w+AQtlbaH1fd+r", + "uKJqdVMWQGoutTlTbDzl2lEEC1WhNpRaIk1BrsiDpWiH6inJD1r5FO6/a9n2K5HBKeipwHq7Zrv9j7Uq", + "8rh/Cn/yKYaa/dS45G+ayxop1/TD8+e7m1VnUrcy9hzi9oo/4QNI2O/7lv2uk/dIKXh5BVt69aQHNnx5", + "TretnLQkD7VeZmzDWta8MFDPSqeqsjkkjvfT0sW+oY++/mCM9cViLvp6/n8jtmp/JVPWF48CxJkwr8xv", + "3CYPWgyrrFSGt2YsGhjP4HeMK25gtXuz5HY/HyvHZrM1Ql5aA3gQAvcsqYV9KOIBKmeVbRs+cige5Y5j", + "b0BrkYIJ5cE9BHbrOH+2v8pXGvUchrf/iM+vZsBSHfEHKuyFmw4EfSLPiYDb3+eqfdTfp8r20UuhsxQg", + "U36HCefiA5zINz+27wCDfY1Pk3/z45oYma+z9HTNAJRzq/L7EprSCbh5VvPLydSXl8c2QCovm36ONU9g", + "VGTMTArrrCCfUD3FMCp0LQmJcQBaF7mF1HfadMCKPwtsUlGOONht6BHLyVWZz/IGMpVvGpt3gVW7aGjV", + "LcwqJ/FrJTbYXNZ2pJR4cBwtLQrZzJ3Hgpt/tvpee1UbxBCsw8jpXO2UY9NXitsWI6h3jCW6psaGGCfx", + "mhvbw5V7J0c+Gq3wQd/n58fBb+TdZcJQdS0KaFnoybLB85o7Y/Cs/bEUh21B8nNFA6hc0K3Q4LupkVMF", + "E92xeFBeKyjgMcdApnge6swQQrHk3On77EAPhdVch9x/b2cZajVEhQSqtHkNjKc0WZ+9Wuhusay6QTdW", + "lgB3DLqHzhsim7K/HaShYlXoX/SfPt9/b+4vRzhvLWCqyxaLGkTL5TbcaV+G76xCyP+cv3tbus5i0M6E", + "8VBaXqqBKteQM3oe+s2qxTG4Elp8C41HasZ0DjbQjNdPpZO4tTeTdZKbinVX/ZnWb8+EvZga3ZkajZka", + "tWD9RUyHhk60Ox/guGEPp8f1XZa4Pw/vXFu8KLbV1I/0rskz0eJc/K3ZU7bZwzYAs9m/wOHXT0lZGmXL", + "wWpHIrQ2o4HrP79ibQlfhHWj8v++6P/Wysvrp4wbu6BX2VHo1YbdZ4N+a4KFbo3xXmEb3Jj88ekcUdqZ", + "q+G8sRvtfpVOr2FmrFbXYKLVCaOxD/EKiltlxYRwvWofISuolh3jJNGduxS7k/Qv5dFCwxNsaskNpqtg", + "PtReGurU7lKTCye3Qjj5pfTxv04EuLXQcuGSqXDNqa3XgBTbwb/9n30HF5+0s9u/lLWKmViG30FtlpOW", + "uFU67TlZmdILmQ8oLU8upNW8576iBc2ldFaA5FSICNUb/Zzzwjg8OcOE9kYS2u1lCeqibVK6LX0FHCki", + "XLEwOimDiTK2LOnfUkhKDRzDJLCcFrE5yoQ7de0s91mumJCOExzHucvsSzYVxvJrILMH9SRaFAizIU+u", + "Tc4TqIiA7ffZO5nNvAgzMQiwHSMykDabNeB0KavPkDZ2CVTlzWy//zRK9S3tuVt7KvymhYWyC8R2jL4c", + "W41whVD4LCy4bTOIj9gci17jfEM939mMnWArMnZwetLpdm5AG9rOfv9pfx/9fjlIbLHW+a6/3//Ol/3C", + "g+yFbJK9UcbHweeTRJw+b0CPqasefkkkAHfC4JO+kmC6rMid8mFzk0byUW6Eu2zloG+EUTrtEpNhSc5C", + "WpEh5Mqvj+DmQqnMsMsOmntSyPFlB7NWsam5MEwN0WZKQ+9Rqg2JbhCfOIXE5HBIHowU3X42mYRVXuH5", + "CRVg7I8qnVEoY9UlpErS3fuXIScjaczIC2mAZqTZvTsSwdAqNkWw+lqF/7zs9HrXQplrSlro9Xx7pt44", + "Ly47f+xun2dAG4qTVfWd409KNcKcNVzn2f5+xD+N+yd8U1vi8mge2fMVKz92O89pppjlUa649yMPPEk1", + "cz92O9+vMw7rF0ie+VFYY3M65e5i03lPdFluMeOFTCYeCW7zfs84rKLeXGUiqTyh7VxRGNC90JOkWgaw", + "kLMWBhhONWOVC6oMeBjy8ue+o6rupVzJLmxzbrmUm7LLIWisvR2gwKZc8jFdJ6/9pbbZjRCpmB2HFojn", + "vs1W91Jin8MeFmeGtJyRzlHOH8gQfZmHR6d7ITdZyV3UP9ipGtJLif6KAMuVnH0a0Lg9c8dVQ8yiWgf5", + "ffZLyATzP0k+BXMpd3y+kdemh0pdCzAejpcd6pyIxW/9i8qknIH+2r+U5wAslD5GSoZqJ/2xUuMMSsLe", + "o5eOMlsy/J1A6gsnu/P/yI1IDgo7eXcD+mdr8+PQRo9gEN0wOorcx+Z9PtY8BVOO8kr1Db/zZSSEkuYU", + "9Kmjk86L7551O6cqL3JzkGXqFtJXSr/XmcE3vcWyzp0/Pj6UXAu08tWKtnmyc2dpl3BFnime9squpabH", + "ZdoL3zqxp0zE0HmPw6igpmZTJ0HKKdgHkTOuk4m4cRwOdxZbtdkJTFkhU9Bsb6KmsEciZK9aeu+y2N//", + "LnGsgP+C7qV090HtZNy0vgLJbSG3MDRKyXkpP6GhQfAqBaM5kOmZh/EymTQtMityru2eu/P2gq+szeao", + "QNmerll944wPQj/CBBMEuG3UXmhOHy+j+0plDqf4amwVyzOegC9/HdC1GdbnHggOer/z3of93t/6g94f", + "fz3tPvv++/jj9geRD0Yi1mf694ogQ0MJH3tYyJwyWSr2KXe9g73GQqrplEsxAmNRRe/WvRBDIR0nrrLq", + "y+35esSxm8lSA66G3e2suKexeNSSGogUIO1GpB1xTckcgprlf265tyCCSmzWiHyHGyeQzG5dCJZH9NLQ", + "36X3hsHGi0u945BFK5maa3Iy12HP0CObb78XOkj32YH/FTU/ReE4c4a8ZVbwLJv5LhoTlZVdX++SrDCO", + "eJ3502VGMakY9vmm0HdWChvDEi7JR5EBvwHskBCCGoxVuQlOhJHQxvr696F5X9luWJRVJ8hbGZryYUWr", + "/qUMJZoLg0+NzoZIJp6rUqD8HXcvrPyAmJpB5VTcatcwoy6JHlyXMrxf5nzmZvHPCgx7n/esFjlzpqNM", + "KIIYML1cpuJGpAXP/DQxyfsjGoLNLorbm4FLfaaLK1WN4LYzRnDKlgYAn5P3SkagjpFRBqjT9BybzTVo", + "DMzWRFzVmvGR8BXp/bglmqhbVuhsGdj6s2LoXEyLjNIFievqvWvjjsQFHJG7as+J+nY0nQFPD2uurRi0", + "HgpdzbatiK25u1fZfdUviXpqgW/uDV13aPIsl3kmC16+NnCib7Adnk3n5CORftwDui35o9fT5xZRX+qA", + "hS9GYP1GDtngTF8DX2VD1DiayqDXR8LQYqvVtZHzIOvXCl/F+IzicW9EaApQ3pa/GIz/LFJfgkPd1qv7", + "NdHcbPUbt/qwshBaLRj5HQQq9STslo9UznLjoaaeW1ZbehXC0AM536dwLG5CKzgyTDPgBtC2qnfYWdFE", + "L2bxlC0hH4k0F5sebyk33ERfiLrErVR1EwlNHPEwRzFjsEQwg7IXeauQ+Also8blY6rHeDHNOO9i1AGd", + "tDzEQ0DxJ7CNwAZveZCwCCutY3w0e2jHgVvW2nwkMl/szn0v69BDwZ3s85L6m1BCsoGdoBXLiPdK0ph1", + "MNboW75Ejvo6fdU6+IyPMrP23l+G25OfvMr7qBUbu5SxEmIUIoZlrnINE5B0b16sVdZlBuBSus3E640x", + "bis3+ljY/kgDpGCurcr7So/37tz/ybWyau/u6VP6R55xIfdoshRG/QnJcx/ONVFSaVMP/PCxjOG87kbt", + "g8kTDwpMGzDehUZYUGn0xcMXwHskdljoN78lNyBCkVq+JGuBdHzdl4R0uQbh15uWtImqC34NVQrfY1mM", + "C5mIHz2OlmocDEvdyylztlpptXdzQbFUG6BY18+K0EOe44skZxWCQhDaCnSqLGsXYpRjyW58HmI2c9bb", + "nnK8HXIj3d9szcarSdKmtdjw8zWqOHozsJHk6Bv7SpapMaZAWpFcG7YjlfUJuOTirFEQG8KE3whH0nzG", + "brievWS2QC+d72MeGDjETA2VndSOQs+NIecSMzS979I/dXfr0aoh5AdfehouzZ1yDjSFqwV2Ke4DvUgU", + "LBQiu4MovAqxYeTA6PU05MAte8t6PQq62mf0gkAGOb0hXMUk5HlIdXwk9qsl324rHT15fSE+JNpMZSsQ", + "erh1lvEG1lwI+m0Rjj7g8pHwMh/PeS8nBwURfjFay52NnBrtWEipxm4jgiUSKuFL8T6W8RApPf2JHRp+", + "9RDIvKi+3nsPhgdYM/z4Pmh+vv+31ePcvjKRPHxcQMtxHGmMzF6igVsYlBVGkUyKmDcePyzzPh/LJd9c", + "ZSNSebosTZXO+QWxLp2UcYynrMAf8JJCBmvh5Qg/fGy80Cr1VgFb+3xKlNAR0/tx1vPV494q+0oVMn1A", + "ZxHuvN52eB5vIQxhCcpeUSjAl40tLELwb4AoxEeJI3UrM8VTx12DDwKTbcdgY8ndttDSMM5+PzmlbOJa", + "9IhvkmvRVg3pl1XBgHqn5zn8+/WPhP5d5BjtovkULGiDhUXbWmmUnIPeYavKkBZnQYdDYQ1uN+7PAlAc", + "UNBOKJ3QpIFuPZJoVSmGPzZSzh6u97pQOqiHM5ZZxkhYdQB/jXTpkVUXIYwHQvNHbqFXY9M1CNZy3f9g", + "LNuxXNdCn6bB8YKx+26u3aV0fSmXEDb73diUqdEItGFGjCU288e0jhE3FnS5IJZKlemlTKH+J/dvrimJ", + "8YPI/YWYJxMBN9iICOz8LMhG8VePGlc5GH0tbNX9a7Gsfnlc9A722c9iPAFN/1V252JmSv28Q6glGxaW", + "WX4NLFNyDLp/KXuECWNfsP912KYp2NMu80k1DrGQsp3//W5/v/f9/j578+Oe2XUDfdJQc+B3XTbkGZeJ", + "M6XcyD3EANv536ff18YS4ppD/7sb8BmGfL/f+78bgxa2+bSLfy1HPNvvPS9HtGCkRi0DnKZTR0dVLjD8", + "q8pp9qDqdGu/0ZbxHyZW7HFTqei5915i8cLz9v/PRKNtHrsUj05+DUJelBeLTdFQtulbVyagJPBgXegY", + "+KVo2M1swqpV4SJBoZVX64P4FZLNT2AbnRxDYe4F7JVkkwlj0U43rXRTNZTcTpl8nZRSnTpCKtX1LaO8", + "v6+QVjASHjFPQbqLtIEtCNuub6Fp3iM+Oz/E1Q2feSt3x1eIJzwBtknD3IJlzKyBp+WlO8rLZ8BTf+Ve", + "j5VxsWASuvm/FG5WiQXbq8pB38uWQNEfjZH8yogFIzLLq4wbWBKHARL0g1o5wlbuXqwK+XgBfi3lJ7fO", + "XKtVW/TheF8hIs/BRro011C3h5UqzUTkJYYpdaX90RZzCEOGC2ZqUV6G0owyrDLwCsGHwWiYKi8DKE60", + "35LRFcyDB0vhKi2SlhysbZqu1ioSeIN2vTasQaBumunks5yWd1ZdnquOUHiwLCfEUpng9LWLukji08jb", + "a3V2CK7NpQmcHB0vyG/Ui4xyNYU1lW9zITQs1tQ3xhzk3Xww1tiU9NN6kdJaFmp5cbZqPT6oJxbeI+tv", + "GT9sSdi/i7wi6xoC/22InNeTiedIdIHevXNlBcFv6hpt44tLuZoxVrtIGx7RSznnEm1PJfY+zgdjruBV", + "WYx7mMC866VUISuZofv5mNb9Kx9UdLe8EFLVKScDMhFQcVbDqaypFnmo/O73honCWDrLkVOvh9/0qnG7", + "/c3qkwU8PIq4OPAw/DcXGfPk2iI2bueTfeduArXa2Y91B4iU514ft1sWJsJjRxvKvZfizwJiNaUrrrz1", + "4FhZpnfxronHZA9dP+MzERsdpu6k9knQclyzxBBae38FkH/0JQKBEgDn6U3lFbnNOSnQ8eA9Dd7vUOJx", + "me9htavheaxoJSFK5fnXj6hzLI7tToTZ9BHn0TyS9ij+tNWVRP3QXplj+uwT4mreLWThztJuo/6gVe8B", + "53i19WWpI/HcVXloNardhX18LvbF4Sme+q/O33vn58c9n5rbu4iWeX0DqeC+kuEI6y9jWVsf7rszL8R2", + "Gy934ZVuQdRFHuU+fo1kSnW456Hs0wlJ7JYU6y7zy4OMMOF1HYfnUc344gvOz0/47v2uKvYZOp+0Nj1p", + "1CX+4fnztm1ip5CWbS1tlULMt47Gv6c7dktvRplu/bWrUXRLOc0Z4iGrUK1Mjc1eBdj4E50a+/6ULXJ4", + "jiB85e5llBsEjSfxqnZUtF9ifJmRyjJ1G488aHSLq/UzmUezktmsqognRoz2zoRhfmtLGLNdq2yyTu3s", + "8dWqDwa+z2bns2m012q8pipzhPVFa6+YZnCbxgKCbmlikDzjs1tstLbnS8SsUbqoLKx/Wo72vYql4z4N", + "ZlLrg4SoubOMj7mQhm7iof6+bwp8KZVkmUp4NlHGvvjbs2fPqCQyzjrhBrszUEPyJzkfw5Mue+LnfUKF", + "pZ74KZ+UVZhDBpQum+HaMGO1OSxDZQstqyYJgbxijhMPgurch6QdHuNmt7DWZ8p6iOwDWxLH8sIr4H6J", + "pYaqI2BKzznunCgiQpyeQUgmIXe0X/RrzfofLXe2XOEz0UFjB20UUFUK0/6bL6LEVKKmUyclzEwmE62k", + "KkyoKBUQjP33V2IYe/4/Lopxic+LY7+FNiTjz585sXARt3wJcv/y/8C7+bVoZudGEf2LwDTP1ffyaual", + "JmFpyReFSO9zWdgKoe40X2QVoHe/fJXxBU6UiLG7aVoV2sMvoTgNRnyAlTR3Rp/921Adnecb3T1cgBL2", + "Z+Ls9OIfvSGVKV1NfMZyW7S7IoPIp68+Ne09sh6jQ8VUmP/lq4xS9ghgJhyvHfWpWMOmwa/+baQOHucz", + "20+0hTb76ccZlsUl99tX63GrNB8jOltKh6qwqxxxFfBUYZd65D6TPLqHZ6k8mxu2po8pQFcVNi+o/2Qm", + "RpDMkgy+PaA83gNKjapVYeccZmVL4r3qETYuXSlzuGzn+6iJ2gtNg9vrNrU1n/5sKdqfqbZFmdida7gR", + "eGcMDYjr/YwXsO6Ty1qlWMg+qyN+6etZ+WhVtj+uNbBkv9UaZDYqJRWhDp5/FSiHtz1kodCLP2OtaqC8", + "WjQiwPam+fN7pxPU2qHT02NDwJW/9l4JiQ0gewexJmplO1I1qjqg6trUNLjPfiq45tICxcsNgZ29Ovzu", + "u+/+1l/+AtLYyjnFo2y1Ex/Lsu1G3Fae7T9bxtjCSTKRZUxIJ9rGGozpshxrxTKrZ+T7xNL4ugnuM7B6", + "1jsYuR8Wy0wV4zHlimLJWuyuUmvOXnU20TNiguoQS3tAf/yKE06pzJVBXqTmhGtIlEyQ9mjNHzzzjG3u", + "W/u1zAdYplDCapTpuRBkv8CvoSmMLnf5YAl2PMvq0zbBttBdKBJ699jKt7nIUt37dBmLeiHwFVaIQgiU", + "FRIrueY7eCpZl3U5aHZyhO1FsG7gWBiLHVCwHJyTIP1FLKt8GZJV/vg4rq2xvXnlQ+E+bzE+q/Km+iFw", + "m4RnYNUH0GrP94pcWoKX7gpuol/fUPcCNwMW/lDMzdJ1yOU6zfD6MmI/X1ycMqv5aCQSpiQTts8OeZaF", + "WiEHpydUfk4YN+Wt01a3/BqYsGwICS8MsPdSXGs+svRr6OqX+KLp1+ALAM9CEYOQc/Lrm2ipDzrmuTv5", + "hfodtOqsE9aI3/es6rlTMg+r9EGQc5LCNFeW1IafGeEKAao1EPUXEQdyOd7OwFilsUm2nvKMpi6PUlb5", + "rNboOvmrbtGEQGg2N0NWA1o0Is2AEEpjSzPn1zdMKl9KhEmA1HjbZgJZyrhDW/SVXd4fNyAfCTU08SrM", + "lH3WVxbaabTEb+kXz8LHz/efMzFa2sU9As+fwJZd2B+zfvxcz/xY3ZH4Abe13RYrx7fP39J79ZRrX2CW", + "8l0JIa2IQK2WcAtjpQUYBncOWMIRhsH6EfU6Kmyo0hk1vcag7vRluMnVp9CAHVLtBIQuKcH4tqcboZ75", + "nploOI0wJ6laxpY88cK3TU8y4NqEYk21U7b1Qm0S0SN0v6LAi3KZeqHNT+fD3ZqKP1fGdKxk5zJGKGI1", + "qcGuoPxAh8/2nzbp8JYTIdb8KBVNvvThVW7cvhsnrBvwUKT6ksSu+18po7362UxEnhb281H3F0/Nm2YL", + "Pc6GDHzecKLzZQqmofRr6R9xY+xE/gsSa7Azo/u06uRdLUAPARQH6T8yjBsjxhKohZBUVklvAguZaOBY", + "7jz0S2SSMhK5TNmISzdKFWjJOaZTOcjw2JBU/ZPjzDHMhKnEP71fPNIjHq2FS3ymR7zqnPIGMpVHiRQ3", + "iGGpeejwnNPW76MAmg0laL41iGSe/BYe2uY9ziCpMdQNsOabUzUzkXCfHfNkwkaaTykQF8s/KD1lVyJ9", + "wf4y8OfHy0uZcstfsL/AA6znAO7+fnkpr5ysbxBkWf4/AWN6JRkTDEEbdP0kWhkzJwB8atxLxtlrbmwP", + "cdA7OaI7qLv7BR1Uo2jHNTc8E9QRXoMppuHaGTjsSKucNkVBPdQNZsxzEwy6K5FesZGALH2Byo/u0CBu", + "IKXfhKEqCnbCJXvK+AR4GkKOM7dXAyDx0254a7sF7RhbYN5s2QNwWIxGoPvsMBP4le9bYzVPriOzOW5O", + "wUJicb999gqjr2sMTcnoUs2BjHrYlstWdqdHlUMGhvUbACwwHejBiaNb4WA14TmG+GObCpCgRcKumkLi", + "inrphHBvf3LwRvBwhmN/wXbO1PCD7bjPZ9jq1lEKNXDgLFVJMQXpRl3ZWQ5Xu/QYgjM+MezKUeAV0ovS", + "07LgxDQk7V157fufuK0j/Jj4vcsMZJD4/dDk0c4PSCzN462s6nbmyA0YH1nsvCPMvHDus3dTYbHJHMiU", + "7VOOeBQ1oV3CuvyETX4bTIHt/YkFwLGI1pBgHQFairs1hLT9qjAmPQZUb0gNGvp8eRprSejXa0i3ry6F", + "Y/4EjBt2jg+CvXNHJJ4s3ej/LwAA//+bO8wV7ooBAA==", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/server/openapi.yaml b/server/openapi.yaml index 1c913ed1..42c2805c 100644 --- a/server/openapi.yaml +++ b/server/openapi.yaml @@ -1214,7 +1214,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 @@ -1247,7 +1247,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 @@ -1382,6 +1382,7 @@ components: - network - page - interaction + - api - system source: $ref: "#/components/schemas/BrowserEventSource" @@ -1436,6 +1437,7 @@ components: - network - page - interaction + - api - system default: system source: @@ -1468,7 +1470,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" @@ -1489,6 +1491,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 @@ -2371,6 +2381,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 @@ -2403,6 +2636,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: @@ -2428,6 +2667,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.