diff --git a/server/cmd/api/api/api.go b/server/cmd/api/api/api.go index b9b0a346..5360c9e8 100644 --- a/server/cmd/api/api/api.go +++ b/server/cmd/api/api/api.go @@ -92,6 +92,14 @@ type ApiService struct { monitorMu sync.Mutex lifecycleCtx context.Context lifecycleCancel context.CancelFunc + + // Reader for durable S2 telemetry storage. Nil when S2 is not configured. + telemetryReader *events.S2Reader +} + +// s2Enabled reports whether durable S2 telemetry storage is configured. +func (s *ApiService) s2Enabled() bool { + return s.telemetryReader != nil } var _ oapi.StrictServerInterface = (*ApiService)(nil) @@ -105,6 +113,7 @@ func New( telemetrySession *telemetry.TelemetrySession, eventStream *events.EventStream, displayNum int, + telemetryReader *events.S2Reader, ) (*ApiService, error) { switch { case recordManager == nil: @@ -140,6 +149,7 @@ func New( cdpMonitor: mon, lifecycleCtx: ctx, lifecycleCancel: cancel, + telemetryReader: telemetryReader, }, nil } diff --git a/server/cmd/api/api/api_test.go b/server/cmd/api/api/api_test.go index 4f2c2165..42c00717 100644 --- a/server/cmd/api/api/api_test.go +++ b/server/cmd/api/api/api_test.go @@ -321,7 +321,7 @@ func newTelemetrySession(t *testing.T) (*telemetry.TelemetrySession, *events.Eve func newSvc(t *testing.T, mgr recorder.RecordManager) (*ApiService, error) { t.Helper() ts, es := newTelemetrySession(t) - return New(mgr, newMockFactory(), newTestUpstreamManager(), scaletozero.NewNoopController(), newMockNekoClient(t), ts, es, 0) + return New(mgr, newMockFactory(), newTestUpstreamManager(), scaletozero.NewNoopController(), newMockNekoClient(t), ts, es, 0, nil) } func TestApiService_PatchChromiumFlags(t *testing.T) { diff --git a/server/cmd/api/api/display_test.go b/server/cmd/api/api/display_test.go index 154b6359..c37700c3 100644 --- a/server/cmd/api/api/display_test.go +++ b/server/cmd/api/api/display_test.go @@ -36,7 +36,7 @@ func testFFmpegFactory(t *testing.T, tempDir string) recorder.FFmpegRecorderFact func newTestServiceWithFactory(t *testing.T, mgr recorder.RecordManager, factory recorder.FFmpegRecorderFactory) *ApiService { t.Helper() ts, es := newTelemetrySession(t) - svc, err := New(mgr, factory, newTestUpstreamManager(), scaletozero.NewNoopController(), newMockNekoClient(t), ts, es, 0) + svc, err := New(mgr, factory, newTestUpstreamManager(), scaletozero.NewNoopController(), newMockNekoClient(t), ts, es, 0, nil) require.NoError(t, err) return svc } diff --git a/server/cmd/api/api/events.go b/server/cmd/api/api/events.go index 27dfc50a..4c623e48 100644 --- a/server/cmd/api/api/events.go +++ b/server/cmd/api/api/events.go @@ -12,6 +12,7 @@ import ( "time" "github.com/kernel/kernel-images/server/lib/events" + "github.com/kernel/kernel-images/server/lib/logger" oapi "github.com/kernel/kernel-images/server/lib/oapi" ) @@ -35,7 +36,7 @@ func (s *ApiService) PublishTelemetryEvent(_ context.Context, req oapi.PublishTe if cat, ok := events.CategoryForType(body.Type); ok { ev.Category = cat } else if body.Category != nil { - cat := oapi.TelemetryEventCategory(*body.Category) + cat := *body.Category if !cat.Valid() { return oapi.PublishTelemetryEvent400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "invalid category"}}, nil } @@ -136,6 +137,159 @@ func (s *ApiService) StreamTelemetryEvents(ctx context.Context, req oapi.StreamT return oapi.StreamTelemetryEvents200TexteventStreamResponse{Body: pr, Headers: headers}, nil } +const ( + // defaultSince is the window start used when neither since nor offset is + // given: a duration meaning "this long ago", matching the public API default. + defaultSince = "5m" + // defaultPageSize / maxPageSize bound how many records one page reads. + defaultPageSize = 20 + maxPageSize = 100 +) + +// ReadTelemetryEvents handles GET /telemetry/events. +// Reads one page of archived telemetry envelopes for this browser from durable +// S2 storage in ascending sequence order, applying the category filter. The +// X-Has-More / X-Next-Offset response headers carry the pagination cursor. +// Returns an empty list when S2 storage is not configured. +func (s *ApiService) ReadTelemetryEvents(ctx context.Context, req oapi.ReadTelemetryEventsRequestObject) (oapi.ReadTelemetryEventsResponseObject, error) { + log := logger.FromContext(ctx) + + if !s.s2Enabled() { + return readTelemetryEventsOKResponse{}, nil + } + + opts, err := buildReadOptions(req.Params) + if err != nil { + return oapi.ReadTelemetryEvents400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: err.Error()}}, nil + } + result, err := s.telemetryReader.Read(ctx, opts, log) + if err != nil { + log.Error("failed to read telemetry events from S2", "err", err) + return oapi.ReadTelemetryEvents500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to read telemetry events"}}, nil + } + + // has_more / next cursor track the raw stream position, independent of the + // category filter, so a filtered page may come back empty while more remain. + envs := filterByCategory(result.Envelopes, req.Params.Category) + + return readTelemetryEventsOKResponse{envs: envs, nextSeqNum: result.NextSeqNum, hasMore: result.HasMore}, nil +} + +// buildReadOptions maps query params to a bounded, paginated read. offset is the +// opaque cursor and takes precedence over since as the start (until still bounds +// the page); since/until accept an RFC-3339 timestamp or a duration like "5m". +func buildReadOptions(p oapi.ReadTelemetryEventsParams) (events.ReadOptions, error) { + var opts events.ReadOptions + + switch { + case p.Offset != nil: + // Offset is the cursor; no request validator is mounted, so clamp a + // negative value to 0 rather than wrap it into a huge uint64. + seq := uint64(max(*p.Offset, 0)) + opts.SeqNum = &seq + case p.Since != nil: + ms, err := parseTimeParam(*p.Since) + if err != nil { + return opts, fmt.Errorf("since: %w", err) + } + opts.Timestamp = &ms + case p.Until != nil: + // until-only: read from the start of the stream (seqnum 0, clamped to the + // oldest retained record) up to until, rather than anchoring the start at + // defaultSince ago which would silently empty a far-past window. + seq := uint64(0) + opts.SeqNum = &seq + default: + // No bounds given at all: default the start to defaultSince ago. + ms, err := parseTimeParam(defaultSince) + if err != nil { + return opts, fmt.Errorf("since: %w", err) + } + opts.Timestamp = &ms + } + + if p.Until != nil { + ms, err := parseTimeParam(*p.Until) + if err != nil { + return opts, fmt.Errorf("until: %w", err) + } + opts.Until = &ms + } + + count := uint64(pageSize(p.Limit)) + opts.Count = &count + return opts, nil +} + +// parseTimeParam parses an RFC-3339 timestamp or a non-negative duration like +// "5m" (interpreted as that long before now) into unix milliseconds. +func parseTimeParam(s string) (uint64, error) { + if d, err := time.ParseDuration(s); err == nil { + if d < 0 { + return 0, fmt.Errorf("duration must not be negative: %q", s) + } + return uint64(time.Now().Add(-d).UnixMilli()), nil + } + if t, err := time.Parse(time.RFC3339, s); err == nil { + return uint64(t.UnixMilli()), nil + } + return 0, fmt.Errorf("invalid time value %q: want an RFC-3339 timestamp or a duration like 5m", s) +} + +// pageSize clamps a requested limit into [1, maxPageSize], defaulting when unset. +func pageSize(limit *int) int { + switch { + case limit == nil: + return defaultPageSize + case *limit < 1: + return 1 + case *limit > maxPageSize: + return maxPageSize + default: + return *limit + } +} + +func filterByCategory(envs []events.Envelope, cats *[]oapi.TelemetryEventCategory) []events.Envelope { + if cats == nil || len(*cats) == 0 { + return envs + } + want := make(map[oapi.TelemetryEventCategory]struct{}, len(*cats)) + for _, c := range *cats { + want[c] = struct{}{} + } + out := make([]events.Envelope, 0, len(envs)) + for _, e := range envs { + if _, ok := want[e.Event.Category]; ok { + out = append(out, e) + } + } + return out +} + +// readTelemetryEventsOKResponse serializes a page of events.Envelope directly, +// matching the SSE stream and publish endpoints. The pagination cursor rides in +// the X-Has-More / X-Next-Offset headers (X-Next-Offset only when there is more). +type readTelemetryEventsOKResponse struct { + envs []events.Envelope + nextSeqNum uint64 + hasMore bool +} + +func (r readTelemetryEventsOKResponse) VisitReadTelemetryEventsResponse(w http.ResponseWriter) error { + w.Header().Set("X-Has-More", strconv.FormatBool(r.hasMore)) + if r.hasMore { + w.Header().Set("X-Next-Offset", strconv.FormatUint(r.nextSeqNum, 10)) + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + envs := r.envs + if envs == nil { + envs = []events.Envelope{} + } + return json.NewEncoder(w).Encode(envs) +} + // publishTelemetryEventOKResponse serializes events.Envelope directly so the response // is identical in shape to the SSE stream frames. type publishTelemetryEventOKResponse struct{ env events.Envelope } diff --git a/server/cmd/api/api/events_test.go b/server/cmd/api/api/events_test.go index 36ab935f..e6cb9035 100644 --- a/server/cmd/api/api/events_test.go +++ b/server/cmd/api/api/events_test.go @@ -4,6 +4,8 @@ import ( "bufio" "context" "encoding/json" + "net/http" + "net/http/httptest" "strconv" "strings" "testing" @@ -56,7 +58,7 @@ func TestEventLifecycle(t *testing.T) { }() // Publish a custom event. Unknown types must carry an explicit category. - sys := oapi.PublishEventRequestCategorySystem + sys := oapi.TelemetryEventCategorySystem resp, err := svc.PublishTelemetryEvent(ctx, oapi.PublishTelemetryEventRequestObject{ Body: &oapi.PublishEventRequest{Type: "test.event", Category: &sys}, }) @@ -88,7 +90,7 @@ func TestPublishDroppedWhenTelemetryInactive(t *testing.T) { ctx := context.Background() svc := newTestService(t, newMockRecordManager()) - sys := oapi.PublishEventRequestCategorySystem + sys := oapi.TelemetryEventCategorySystem resp, err := svc.PublishTelemetryEvent(ctx, oapi.PublishTelemetryEventRequestObject{ Body: &oapi.PublishEventRequest{Type: "test.event", Category: &sys}, }) @@ -119,7 +121,7 @@ func TestPublishKnownTypeCategoryIsServerAuthoritative(t *testing.T) { // api_call is a known type that maps to the control category. A caller // supplying a different category must be overridden by the server. - console := oapi.PublishEventRequestCategoryConsole + console := oapi.TelemetryEventCategoryConsole resp, err := svc.PublishTelemetryEvent(ctx, oapi.PublishTelemetryEventRequestObject{ Body: &oapi.PublishEventRequest{Type: "api_call", Category: &console}, }) @@ -149,7 +151,7 @@ func TestPublishDroppedWhenCategoryDisabled(t *testing.T) { }) require.NoError(t, err) - page := oapi.PublishEventRequestCategoryPage + page := oapi.TelemetryEventCategoryPage resp, err := svc.PublishTelemetryEvent(ctx, oapi.PublishTelemetryEventRequestObject{ Body: &oapi.PublishEventRequest{Type: "test.page", Category: &page}, }) @@ -161,7 +163,7 @@ func TestPublishDroppedWhenCategoryDisabled(t *testing.T) { // telemetry session. Seqs run 1..n on a fresh stream. func publishTestEvents(ctx context.Context, t *testing.T, svc *ApiService, n int) { t.Helper() - sys := oapi.PublishEventRequestCategorySystem + sys := oapi.TelemetryEventCategorySystem for i := 0; i < n; i++ { resp, err := svc.PublishTelemetryEvent(ctx, oapi.PublishTelemetryEventRequestObject{ Body: &oapi.PublishEventRequest{Type: "test.event", Category: &sys}, @@ -296,3 +298,111 @@ func TestStreamResumeAfterLastEventIDUnchanged(t *testing.T) { id := streamFirstID(t, svc, oapi.StreamTelemetryEventsParams{LastEventID: ptrOf("5")}) assert.Equal(t, uint64(6), id, "Last-Event-ID without replay must behave as before and resume after seq 5") } + +func TestReadTelemetryEventsS2Disabled(t *testing.T) { + t.Parallel() + svc := newTestService(t, newMockRecordManager()) // s2 creds empty -> disabled + + resp, err := svc.ReadTelemetryEvents(context.Background(), oapi.ReadTelemetryEventsRequestObject{}) + require.NoError(t, err) + ok, isOK := resp.(readTelemetryEventsOKResponse) + require.True(t, isOK, "expected 200 response when S2 is disabled") + + rec := httptest.NewRecorder() + require.NoError(t, ok.VisitReadTelemetryEventsResponse(rec)) + assert.Equal(t, http.StatusOK, rec.Code) + // Empty result must serialize as [] not null, or the Python SDK chokes. + assert.JSONEq(t, `[]`, rec.Body.String()) + assert.Equal(t, "false", rec.Header().Get("X-Has-More")) + assert.Empty(t, rec.Header().Get("X-Next-Offset"), "no cursor when there is no more") +} + +func TestFilterByCategory(t *testing.T) { + t.Parallel() + mk := func(c oapi.TelemetryEventCategory) events.Envelope { + return events.Envelope{Event: events.Event{Category: c}} + } + envs := []events.Envelope{mk(events.Console), mk(events.Network), mk(events.Console)} + + assert.Len(t, filterByCategory(envs, nil), 3, "nil filter keeps everything") + + cats := []oapi.TelemetryEventCategory{events.Console} + assert.Len(t, filterByCategory(envs, &cats), 2) +} + +func TestPageSize(t *testing.T) { + t.Parallel() + assert.Equal(t, defaultPageSize, pageSize(nil), "unset defaults") + + ptr := func(n int) *int { return &n } + assert.Equal(t, 50, pageSize(ptr(50))) + assert.Equal(t, 1, pageSize(ptr(0)), "clamped up to 1") + assert.Equal(t, 1, pageSize(ptr(-5)), "clamped up to 1") + assert.Equal(t, maxPageSize, pageSize(ptr(5000)), "clamped down to max") +} + +func TestBuildReadOptions(t *testing.T) { + t.Parallel() + strp := func(s string) *string { return &s } + + // No params: start defaults to ~defaultSince ago, no end bound, default page. + opts, err := buildReadOptions(oapi.ReadTelemetryEventsParams{}) + require.NoError(t, err) + require.NotNil(t, opts.Timestamp) + assert.Nil(t, opts.SeqNum) + assert.Nil(t, opts.Until) + require.NotNil(t, opts.Count) + assert.Equal(t, uint64(defaultPageSize), *opts.Count) + + // since/until accept RFC-3339 timestamps (→ unix ms); limit bounds the page. + limit := 25 + opts, err = buildReadOptions(oapi.ReadTelemetryEventsParams{ + Since: strp("2020-01-01T00:00:00Z"), + Until: strp("2020-01-02T00:00:00Z"), + Limit: &limit, + }) + require.NoError(t, err) + require.NotNil(t, opts.Timestamp) + assert.Equal(t, uint64(1577836800000), *opts.Timestamp) + require.NotNil(t, opts.Until) + assert.Equal(t, uint64(1577923200000), *opts.Until) + require.NotNil(t, opts.Count) + assert.Equal(t, uint64(25), *opts.Count) + + // since also accepts a duration meaning "this long ago". + opts, err = buildReadOptions(oapi.ReadTelemetryEventsParams{Since: strp("5m")}) + require.NoError(t, err) + require.NotNil(t, opts.Timestamp) + assert.InDelta(t, time.Now().Add(-5*time.Minute).UnixMilli(), int64(*opts.Timestamp), 5000) + + // offset is the cursor and takes precedence over since (SeqNum start, no Timestamp). + offset := int64(4213) + opts, err = buildReadOptions(oapi.ReadTelemetryEventsParams{Offset: &offset, Since: strp("5m")}) + require.NoError(t, err) + require.NotNil(t, opts.SeqNum) + assert.Equal(t, uint64(4213), *opts.SeqNum) + assert.Nil(t, opts.Timestamp, "since is ignored when offset is set") + + // until-only: start from seqnum 0 (the beginning), not anchored at now-5m, + // so a far-past until doesn't silently yield an empty page. + opts, err = buildReadOptions(oapi.ReadTelemetryEventsParams{Until: strp("2020-01-01T00:00:00Z")}) + require.NoError(t, err) + require.NotNil(t, opts.SeqNum) + assert.Equal(t, uint64(0), *opts.SeqNum, "until-only reads from the start of the stream") + assert.Nil(t, opts.Timestamp) + require.NotNil(t, opts.Until) + assert.Equal(t, uint64(1577836800000), *opts.Until) + + // Negative offset clamps to 0 rather than wrapping into a huge uint64. + neg := int64(-1) + opts, err = buildReadOptions(oapi.ReadTelemetryEventsParams{Offset: &neg}) + require.NoError(t, err) + require.NotNil(t, opts.SeqNum) + assert.Equal(t, uint64(0), *opts.SeqNum) + + // Unparseable / negative time values are rejected (→ 400 at the handler). + _, err = buildReadOptions(oapi.ReadTelemetryEventsParams{Since: strp("nonsense")}) + assert.Error(t, err) + _, err = buildReadOptions(oapi.ReadTelemetryEventsParams{Until: strp("-5m")}) + assert.Error(t, err, "negative duration rejected") +} diff --git a/server/cmd/api/api/middleware.go b/server/cmd/api/api/middleware.go index a558199a..14508fde 100644 --- a/server/cmd/api/api/middleware.go +++ b/server/cmd/api/api/middleware.go @@ -35,6 +35,15 @@ func DisableTelemetryMiddleware() { telemetryMiddlewareEnabled.Store(false) } // TelemetryMiddlewareEnabled reports the current state. func TelemetryMiddlewareEnabled() bool { return telemetryMiddlewareEnabled.Load() } +// telemetryReadOps are the telemetry-observer endpoints that must not emit their +// own api_call event: doing so would append to the very stream they read, a +// feedback loop that (e.g. at page size 1) prevents a paginated read from ever +// catching the tail. +var telemetryReadOps = map[string]struct{}{ + "ReadTelemetryEvents": {}, + "StreamTelemetryEvents": {}, +} + // TelemetryHTTPMiddleware emits a BrowserApiCallEvent per documented operation, // capturing the final status and wall-clock duration. publish is wired to // TelemetrySession.Publish; the middleware ignores the returns. @@ -55,6 +64,9 @@ func TelemetryHTTPMiddleware(publish func(events.Event) (events.Envelope, bool)) if tc.operationID == "" { return } + if _, skip := telemetryReadOps[tc.operationID]; skip { + return + } data, _ := json.Marshal(oapi.BrowserApiCallEventData{ RequestId: chiMiddleware.GetReqID(ctx), OperationId: tc.operationID, diff --git a/server/cmd/api/api/middleware_test.go b/server/cmd/api/api/middleware_test.go index 972967f4..5e563239 100644 --- a/server/cmd/api/api/middleware_test.go +++ b/server/cmd/api/api/middleware_test.go @@ -5,6 +5,7 @@ import ( "encoding/json" "net/http" "net/http/httptest" + "reflect" "sync" "testing" @@ -140,6 +141,39 @@ func TestTelemetryMiddleware_ShortCircuitsWhenDisabled(t *testing.T) { assert.Empty(t, rp.snapshot(), "disabled middleware must not emit") } +// Reading telemetry must not emit its own api_call event, or the read would +// append to the very stream it reads (a feedback loop that, at page size 1, +// stops a paginated read from ever catching the tail). Pins the telemetryReadOps +// skip set against operationId drift. +func TestTelemetryMiddleware_SkipsTelemetryReadEndpoints(t *testing.T) { + withTelemetryMiddlewareEnabled(t) + for _, op := range []string{"ReadTelemetryEvents", "StreamTelemetryEvents"} { + t.Run(op, func(t *testing.T) { + rp := &recordingPublisher{} + chiHandler(t, rp.publish, op, http.StatusOK). + ServeHTTP(httptest.NewRecorder(), httptest.NewRequest(http.MethodGet, "/telemetry/events", nil)) + assert.Empty(t, rp.snapshot(), "telemetry read endpoints must not emit an api_call back into the stream") + }) + } +} + +// telemetryReadOps keys must be real operations, so an operationId rename can't +// leave the skip set stale and silently revive the api_call read-feedback loop. +// A rename also renames the StrictServerInterface method (the compiler forces +// ApiService to follow), so a stale key stops matching any method here. +func TestTelemetryReadOpsAreRealOperations(t *testing.T) { + iface := reflect.TypeOf((*oapi.StrictServerInterface)(nil)).Elem() + methods := make(map[string]struct{}, iface.NumMethod()) + for i := 0; i < iface.NumMethod(); i++ { + methods[iface.Method(i).Name] = struct{}{} + } + require.NotEmpty(t, telemetryReadOps) + for op := range telemetryReadOps { + _, ok := methods[op] + assert.Truef(t, ok, "telemetryReadOps key %q is not a StrictServerInterface operation; did an operationId get renamed?", op) + } +} + // Builds the same middleware stack as main.go: RequestID -> HTTP middleware -> // strict dispatch -> inner handler. func chiHandler(t *testing.T, publish func(events.Event) (events.Envelope, bool), operationID string, status int) http.Handler { diff --git a/server/cmd/api/api/telemetry_test.go b/server/cmd/api/api/telemetry_test.go index 258b6987..7b6a0b52 100644 --- a/server/cmd/api/api/telemetry_test.go +++ b/server/cmd/api/api/telemetry_test.go @@ -358,7 +358,7 @@ func (m *mockRecordManager) StopAll(_ context.Context) error func newTestService(t *testing.T, mgr recorder.RecordManager) *ApiService { t.Helper() ts, es := newTelemetrySession(t) - svc, err := New(mgr, newMockFactory(), newTestUpstreamManager(), scaletozero.NewNoopController(), newMockNekoClient(t), ts, es, 0) + svc, err := New(mgr, newMockFactory(), newTestUpstreamManager(), scaletozero.NewNoopController(), newMockNekoClient(t), ts, es, 0, nil) require.NoError(t, err) svc.cdpMonitor = &stubCdpMonitor{} return svc diff --git a/server/cmd/api/main.go b/server/cmd/api/main.go index b3500a52..8d4f13ed 100644 --- a/server/cmd/api/main.go +++ b/server/cmd/api/main.go @@ -116,9 +116,12 @@ func main() { // Optional S2 storage sink. var s2Writer *events.S2StorageWriter - if config.S2Basin != "" && config.S2AccessToken != "" && config.S2Stream != "" { + if config.S2Enabled() { slogger.Info("S2 storage enabled", "basin", config.S2Basin, "stream", config.S2Stream) - s2Writer = events.NewS2StorageWriter(eventStream, config.S2Basin, config.S2AccessToken, config.S2Stream, events.S2Config{}, slogger) + s2Writer = events.NewS2StorageWriter(eventStream, config.S2Basin, config.S2AccessToken, config.S2Stream, events.S2Config{ + BatcherLinger: config.S2BatcherLinger, + BatcherMaxRecords: config.S2BatcherMaxRecords, + }, slogger) if err := s2Writer.Start(ctx); err != nil { slogger.Error("failed to start S2 storage writer", "err", err) os.Exit(1) @@ -134,6 +137,7 @@ func main() { telemetrySession, eventStream, config.DisplayNum, + events.NewS2Reader(config.S2Basin, config.S2AccessToken, config.S2Stream), ) if err != nil { slogger.Error("failed to create api service", "err", err) diff --git a/server/cmd/config/config.go b/server/cmd/config/config.go index aae38b9e..7e458fc5 100644 --- a/server/cmd/config/config.go +++ b/server/cmd/config/config.go @@ -47,6 +47,14 @@ type Config struct { S2Basin string `envconfig:"S2_BASIN" default:""` S2AccessToken string `envconfig:"S2_ACCESS_TOKEN" default:""` S2Stream string `envconfig:"S2_STREAM" default:""` + // S2 batcher tuning for the append path. + S2BatcherLinger time.Duration `envconfig:"S2_BATCHER_LINGER" default:"100ms"` + S2BatcherMaxRecords int `envconfig:"S2_BATCHER_MAX_RECORDS" default:"50"` +} + +// S2Enabled reports whether all three S2 connection values are set. +func (c *Config) S2Enabled() bool { + return c.S2Basin != "" && c.S2AccessToken != "" && c.S2Stream != "" } // LogValue implements slog.LogValuer, redacting secret fields. @@ -73,6 +81,8 @@ func (c *Config) LogValue() slog.Value { slog.String("s2_basin", c.S2Basin), slog.String("s2_access_token", s2AccessToken), slog.String("s2_stream", c.S2Stream), + slog.Duration("s2_batcher_linger", c.S2BatcherLinger), + slog.Int("s2_batcher_max_records", c.S2BatcherMaxRecords), ) } diff --git a/server/cmd/config/config_test.go b/server/cmd/config/config_test.go index 27dd8b3d..447e7ac9 100644 --- a/server/cmd/config/config_test.go +++ b/server/cmd/config/config_test.go @@ -31,6 +31,8 @@ func TestLoad(t *testing.T) { ChromeDriverProxyPort: 9224, ChromeDriverUpstreamAddr: "127.0.0.1:9225", DevToolsProxyAddr: "127.0.0.1:9222", + S2BatcherLinger: 100 * time.Millisecond, + S2BatcherMaxRecords: 50, }, }, { @@ -48,6 +50,8 @@ func TestLoad(t *testing.T) { "SCALE_TO_ZERO_COOLDOWN": "5s", "CHROMEDRIVER_PROXY_PORT": "5432", "CHROMEDRIVER_UPSTREAM_ADDR": "127.0.0.1:9999", + "S2_BATCHER_LINGER": "250ms", + "S2_BATCHER_MAX_RECORDS": "100", }, wantCfg: &Config{ Port: 12345, @@ -63,6 +67,8 @@ func TestLoad(t *testing.T) { ChromeDriverProxyPort: 5432, ChromeDriverUpstreamAddr: "127.0.0.1:9999", DevToolsProxyAddr: "127.0.0.1:9876", + S2BatcherLinger: 250 * time.Millisecond, + S2BatcherMaxRecords: 100, }, }, { @@ -85,6 +91,8 @@ func TestLoad(t *testing.T) { ChromeDriverProxyPort: 9224, ChromeDriverUpstreamAddr: "127.0.0.1:9225", DevToolsProxyAddr: "10.0.0.1:1234", + S2BatcherLinger: 100 * time.Millisecond, + S2BatcherMaxRecords: 50, }, }, { diff --git a/server/cmd/supervisord-shim/main.go b/server/cmd/supervisord-shim/main.go index 79959520..38c44977 100644 --- a/server/cmd/supervisord-shim/main.go +++ b/server/cmd/supervisord-shim/main.go @@ -231,7 +231,7 @@ func mapEvent(header, payload map[string]string) (oapi.PublishEventRequest, bool } } - category := oapi.PublishEventRequestCategory(oapi.TelemetryEventCategorySystem) + category := oapi.TelemetryEventCategory(oapi.TelemetryEventCategorySystem) sourceEvent := "service.crashed" return oapi.PublishEventRequest{ Type: string(oapi.ServiceCrashed), diff --git a/server/cmd/supervisord-shim/main_test.go b/server/cmd/supervisord-shim/main_test.go index dc48df1f..4d938c53 100644 --- a/server/cmd/supervisord-shim/main_test.go +++ b/server/cmd/supervisord-shim/main_test.go @@ -73,7 +73,7 @@ func TestMapEventExitedUnexpectedFromRunning(t *testing.T) { require.True(t, ok) assert.Equal(t, string(oapi.ServiceCrashed), body.Type) require.NotNil(t, body.Category) - assert.Equal(t, oapi.PublishEventRequestCategory("system"), *body.Category) + assert.Equal(t, oapi.TelemetryEventCategory("system"), *body.Category) require.NotNil(t, body.Source) assert.Equal(t, oapi.LocalProcess, body.Source.Kind) require.NotNil(t, body.Source.Event) diff --git a/server/e2e/e2e_telemetry_events_read_test.go b/server/e2e/e2e_telemetry_events_read_test.go new file mode 100644 index 00000000..e23f4121 --- /dev/null +++ b/server/e2e/e2e_telemetry_events_read_test.go @@ -0,0 +1,290 @@ +package e2e + +import ( + "context" + "fmt" + "net/http" + "os" + "os/exec" + "strconv" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + instanceoapi "github.com/kernel/kernel-images/server/lib/oapi" +) + +// s2FlushWait is how long to wait after publishing for the S2 storage writer to +// flush to the durable stream (batcher linger of 100ms plus network), before +// reading the archive back. +const s2FlushWait = 2 * time.Second + +// startTelemetryReadContainer boots a headless container wired to S2 on a +// per-test stream (so tests sharing the S2_STREAM env don't pollute each other) +// and starts a telemetry session. Skips when S2 creds or docker are absent. +func startTelemetryReadContainer(t *testing.T, ctx context.Context) *instanceoapi.ClientWithResponses { + t.Helper() + basin := os.Getenv("S2_BASIN") + accessToken := os.Getenv("S2_ACCESS_TOKEN") + stream := os.Getenv("S2_STREAM") + if basin == "" || accessToken == "" || stream == "" { + t.Skip("S2_BASIN, S2_ACCESS_TOKEN, and S2_STREAM must be set to run this test") + } + if _, err := exec.LookPath("docker"); err != nil { + t.Skipf("docker not available: %v", err) + } + + c := NewTestContainer(t, headlessImage) + require.NoError(t, c.Start(ctx, ContainerConfig{ + Env: map[string]string{ + "S2_BASIN": basin, + "S2_ACCESS_TOKEN": accessToken, + "S2_STREAM": fmt.Sprintf("%s-%s", stream, t.Name()), + }, + }), "failed to start container") + t.Cleanup(func() { c.Stop(context.Background()) }) + + require.NoError(t, c.WaitReady(ctx), "api not ready") + + client, err := c.APIClient() + require.NoError(t, err) + + startResp, err := client.PutTelemetryWithResponse(ctx, instanceoapi.PutTelemetryJSONRequestBody{}) + require.NoError(t, err) + require.Equal(t, http.StatusCreated, startResp.StatusCode(), "put telemetry: %s", string(startResp.Body)) + return client +} + +// TestReadTelemetryEvents publishes a known set of events and reads them back +// through GET /telemetry/events against a real S2 stream, exercising the full +// archive read path rather than the in-memory ring buffer. +// +// Skips automatically when S2_BASIN, S2_ACCESS_TOKEN, or S2_STREAM are unset. +func TestReadTelemetryEvents(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute) + defer cancel() + + client := startTelemetryReadContainer(t, ctx) + + // Publish a deterministic set of events across two enabled categories. + const systemCount, connectionCount = 3, 2 + for i := 0; i < systemCount; i++ { + publishEvent(t, ctx, client, "test.system", instanceoapi.TelemetryEventCategorySystem) + } + for i := 0; i < connectionCount; i++ { + publishEvent(t, ctx, client, "test.connection", instanceoapi.TelemetryEventCategoryConnection) + } + + // Give the storage writer time to flush to S2 (batcher linger + network). + time.Sleep(s2FlushWait) + + // Bound every read tightly: a correct handler caps the S2 read, so these + // return promptly. A hang here means the read is unbounded. + readCtx, readCancel := context.WithTimeout(ctx, 10*time.Second) + defer readCancel() + + // Full read returns at least everything we published. + all, _, _ := readEventsPage(t, readCtx, client, &instanceoapi.ReadTelemetryEventsParams{}) + assert.GreaterOrEqual(t, len(all), systemCount+connectionCount) + + // Category filter returns only the requested category. + systemCat := []instanceoapi.TelemetryEventCategory{instanceoapi.TelemetryEventCategorySystem} + systemOnly, _, _ := readEventsPage(t, readCtx, client, &instanceoapi.ReadTelemetryEventsParams{Category: &systemCat}) + assert.GreaterOrEqual(t, len(systemOnly), systemCount) + for _, e := range systemOnly { + require.NotNil(t, e.Event.Category) + assert.Equal(t, instanceoapi.TelemetryEventCategorySystem, *e.Event.Category) + } + + // An empty window returns [] (not null) with no cursor. + pastSince, pastUntil := "2000-01-01T00:00:00Z", "2000-01-02T00:00:00Z" + empty, err := client.ReadTelemetryEventsWithResponse(readCtx, &instanceoapi.ReadTelemetryEventsParams{Since: &pastSince, Until: &pastUntil}) + require.NoError(t, err) + require.NotNil(t, empty.JSON200) + assert.Empty(t, *empty.JSON200) + assert.JSONEq(t, `[]`, string(empty.Body), "empty result must be [] not null") + assert.Equal(t, "false", empty.HTTPResponse.Header.Get("X-Has-More")) +} + +// TestReadTelemetryEventsPagination publishes more events than the page size and +// walks every page via the X-Next-Offset cursor, asserting the full set comes +// back exactly once in ascending order. +func TestReadTelemetryEventsPagination(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute) + defer cancel() + + client := startTelemetryReadContainer(t, ctx) + + const published = 5 + for i := 0; i < published; i++ { + publishEvent(t, ctx, client, "test.system", instanceoapi.TelemetryEventCategorySystem) + } + time.Sleep(s2FlushWait) + + readCtx, readCancel := context.WithTimeout(ctx, 20*time.Second) + defer readCancel() + + const pageLimit = 2 + var collected []instanceoapi.TelemetryEnvelope + var offset *int64 + pages := 0 + for { + pages++ + require.LessOrEqual(t, pages, 50, "pagination did not terminate") + limit := pageLimit + page, hasMore, next := readEventsPage(t, readCtx, client, &instanceoapi.ReadTelemetryEventsParams{Limit: &limit, Offset: offset}) + require.LessOrEqual(t, len(page), pageLimit, "a page must not exceed the limit") + collected = append(collected, page...) + if !hasMore { + break + } + offset = &next + } + + require.GreaterOrEqual(t, pages, 3, "5 events at limit 2 should span multiple pages") + require.GreaterOrEqual(t, len(collected), published) + // Strictly ascending seqs prove the cursor neither skips nor re-reads across + // page boundaries. + for i := 1; i < len(collected); i++ { + assert.Greater(t, collected[i].Seq, collected[i-1].Seq, "events must be strictly ascending with no dupes across pages") + } + + // Reads must be side-effect-free: reading telemetry must not emit an api_call + // event back into the stream, or pagination could never catch the tail. Two + // full reads must return the same count. + first, _, _ := readEventsPage(t, readCtx, client, &instanceoapi.ReadTelemetryEventsParams{}) + second, _, _ := readEventsPage(t, readCtx, client, &instanceoapi.ReadTelemetryEventsParams{}) + assert.Equal(t, len(first), len(second), "a read must not append to the stream it reads") +} + +// TestReadTelemetryEventsFilteredPagination verifies that paginating with a +// category filter still returns the complete matching set even when intermediate +// pages come back empty. The filter is applied after the cursor-bounded read, so +// a page can be empty while X-Has-More is true; a correct client follows the +// cursor rather than stopping on an empty page. +func TestReadTelemetryEventsFilteredPagination(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute) + defer cancel() + + client := startTelemetryReadContainer(t, ctx) + + // A run of connection events (each also emits a control api_call), then a few + // system events at the tail. Filtering for system means the early pages, which + // scan only connection/control records, come back empty with more remaining. + const systemCount = 3 + for i := 0; i < 15; i++ { + publishEvent(t, ctx, client, "test.connection", instanceoapi.TelemetryEventCategoryConnection) + } + for i := 0; i < systemCount; i++ { + publishEvent(t, ctx, client, "test.system", instanceoapi.TelemetryEventCategorySystem) + } + time.Sleep(s2FlushWait) + + readCtx, readCancel := context.WithTimeout(ctx, 20*time.Second) + defer readCancel() + + systemCat := []instanceoapi.TelemetryEventCategory{instanceoapi.TelemetryEventCategorySystem} + const pageLimit = 2 + var collected []instanceoapi.TelemetryEnvelope + var offset *int64 + pages, emptyPages := 0, 0 + for { + pages++ + require.LessOrEqual(t, pages, 100, "filtered pagination did not terminate") + limit := pageLimit + page, hasMore, next := readEventsPage(t, readCtx, client, &instanceoapi.ReadTelemetryEventsParams{Limit: &limit, Category: &systemCat, Offset: offset}) + if len(page) == 0 { + emptyPages++ + } + for _, e := range page { + require.NotNil(t, e.Event.Category) + assert.Equal(t, instanceoapi.TelemetryEventCategorySystem, *e.Event.Category) + } + collected = append(collected, page...) + if !hasMore { + break + } + offset = &next + } + + assert.GreaterOrEqual(t, len(collected), systemCount, "cursor walk must collect every matching event across empty pages") + assert.Positive(t, emptyPages, "the connection run should yield empty system-filtered pages (the sparse-filter edge)") +} + +// TestReadTelemetryEventsWindowedPagination verifies that an until-bounded read +// terminates even when the stream extends past the window: it returns the +// in-window events and stops, rather than chasing the physical tail. +func TestReadTelemetryEventsWindowedPagination(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute) + defer cancel() + + client := startTelemetryReadContainer(t, ctx) + + // First batch is in-window; capture the boundary; a second batch lands after + // it, so the physical tail sits past `until`. + const inWindow = 6 + for i := 0; i < inWindow; i++ { + publishEvent(t, ctx, client, "test.system", instanceoapi.TelemetryEventCategorySystem) + } + time.Sleep(s2FlushWait) + until := time.Now().UTC().Format(time.RFC3339Nano) + for i := 0; i < 6; i++ { + publishEvent(t, ctx, client, "test.system", instanceoapi.TelemetryEventCategorySystem) + } + time.Sleep(s2FlushWait) + + readCtx, readCancel := context.WithTimeout(ctx, 20*time.Second) + defer readCancel() + + // Page the window; it must terminate via X-Has-More=false rather than walk + // page after page toward the tail that lives past `until`. + const pageLimit = 2 + var collected []instanceoapi.TelemetryEnvelope + var offset *int64 + pages := 0 + for { + pages++ + require.LessOrEqual(t, pages, 50, "windowed pagination did not terminate") + limit := pageLimit + page, hasMore, next := readEventsPage(t, readCtx, client, &instanceoapi.ReadTelemetryEventsParams{Limit: &limit, Until: &until, Offset: offset}) + collected = append(collected, page...) + if !hasMore { + break + } + offset = &next + } + + // The in-window events (and their api_call pairs) come back; the run + // terminates without chasing the post-until tail. + assert.GreaterOrEqual(t, len(collected), inWindow) +} + +// readEventsPage calls the endpoint and returns the page plus its cursor state. +func readEventsPage(t *testing.T, ctx context.Context, client *instanceoapi.ClientWithResponses, params *instanceoapi.ReadTelemetryEventsParams) (page []instanceoapi.TelemetryEnvelope, hasMore bool, next int64) { + t.Helper() + resp, err := client.ReadTelemetryEventsWithResponse(ctx, params) + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode(), "read events: %s", string(resp.Body)) + require.NotNil(t, resp.JSON200) + + hasMore = resp.HTTPResponse.Header.Get("X-Has-More") == "true" + if hasMore { + nextStr := resp.HTTPResponse.Header.Get("X-Next-Offset") + require.NotEmpty(t, nextStr, "X-Next-Offset must be set when X-Has-More is true") + next, err = strconv.ParseInt(nextStr, 10, 64) + require.NoError(t, err) + } + return *resp.JSON200, hasMore, next +} + +func publishEvent(t *testing.T, ctx context.Context, client *instanceoapi.ClientWithResponses, eventType string, category instanceoapi.TelemetryEventCategory) { + t.Helper() + resp, err := client.PublishTelemetryEventWithResponse(ctx, instanceoapi.PublishTelemetryEventJSONRequestBody{ + Type: eventType, + Category: &category, + }) + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode(), "publish %s: %s", eventType, string(resp.Body)) +} diff --git a/server/lib/events/s2storage.go b/server/lib/events/s2storage.go index d10b51f5..50bc84cd 100644 --- a/server/lib/events/s2storage.go +++ b/server/lib/events/s2storage.go @@ -83,6 +83,9 @@ func newS2Storage(ctx context.Context, basin, accessToken, streamName string, cf return nil, fmt.Errorf("s2storage: open append session: %w", err) } + // Defensive defaults for a zero-value S2Config. In production these are + // already set from config (S2_BATCHER_LINGER / S2_BATCHER_MAX_RECORDS), which + // owns the canonical defaults; keep these two in sync with that layer. if cfg.BatcherLinger == 0 { cfg.BatcherLinger = 100 * time.Millisecond } @@ -226,3 +229,104 @@ func (w *S2StorageWriter) Stop(ctx context.Context) error { } return w.storage.Close(ctx) } + +// ReadOptions bounds a one-shot read. SeqNum and Timestamp set the start +// position; Count and Until bound the end. +type ReadOptions struct { + SeqNum *uint64 + Timestamp *uint64 + Count *uint64 + Until *uint64 +} + +// S2Reader reads archived telemetry envelopes from a single S2 stream. +type S2Reader struct { + streamName string + stream *s2.StreamClient +} + +// NewS2Reader returns a reader for streamName, or nil when any connection value +// is empty (durable storage not configured). The client (and its connection +// pool) is built once and reused across Read calls, mirroring the writer; a +// fresh client per request would re-handshake TLS/HTTP2 on every page. +func NewS2Reader(basin, accessToken, streamName string) *S2Reader { + if basin == "" || accessToken == "" || streamName == "" { + return nil + } + client := s2.New(accessToken, nil) + return &S2Reader{ + streamName: streamName, + stream: client.Basin(basin).Stream(s2.StreamName(streamName)), + } +} + +// S2ReadResult is one page of a paginated read. NextSeqNum is the S2 sequence +// number to resume from on the next page; it is meaningful only when HasMore. +type S2ReadResult struct { + Envelopes []Envelope + NextSeqNum uint64 + HasMore bool +} + +// Read returns one page of telemetry envelopes from the bounded range, reusing +// the reader's client. S2 errors are surfaced to the caller. +func (r *S2Reader) Read(ctx context.Context, opts ReadOptions, log *slog.Logger) (*S2ReadResult, error) { + readOpts := &s2.ReadOptions{ + Clamp: s2.Bool(true), + IgnoreCommandRecords: true, + SeqNum: opts.SeqNum, + Timestamp: opts.Timestamp, + Count: opts.Count, + Until: opts.Until, + } + // Safety net for direct callers: an unbounded read blocks waiting for new + // records, so cap it at the tail. The HTTP handler always sets Count, so this + // is unreachable from that path. + if readOpts.Count == nil && readOpts.Until == nil { + readOpts.Until = s2.Uint64(uint64(time.Now().UnixMilli())) + } + + session, err := r.stream.ReadSession(ctx, readOpts) + if err != nil { + return nil, fmt.Errorf("s2storage: open read session: %w", err) + } + defer session.Close() + + envelopes := make([]Envelope, 0) + for session.Next() { + rec := session.Record() + var env Envelope + if err := json.Unmarshal(rec.Body, &env); err != nil { + // Skip rather than fail the page: the cursor is a seqnum, so failing + // here would wedge every page that spans this record. Warn so the + // corruption is still visible. + log.WarnContext(ctx, "s2storage: skipping unparseable record", "stream", r.streamName, "seqnum", rec.SeqNum, "err", err) + continue + } + envelopes = append(envelopes, env) + } + if err := session.Err(); err != nil { + return nil, fmt.Errorf("s2storage: read session: %w", err) + } + + // Both positions ride along in the read batches, so there's no extra + // round-trip. + result := &S2ReadResult{Envelopes: envelopes} + result.NextSeqNum, result.HasMore = nextCursor(session.NextReadPosition(), session.LastObservedTail()) + + log.DebugContext(ctx, "s2storage: read complete", "stream", r.streamName, "records", len(envelopes), "has_more", result.HasMore) + return result, nil +} + +// nextCursor decides whether more records remain after a page, given the read +// session's resume position and the observed stream tail. More records remain +// only when the resume position hasn't caught up to the tail; this holds whether +// the page stopped on the count, byte, or time bound, so it never under-reports +// as a "full page" heuristic would. A nil next (empty/exhausted read) or a +// caught-up position terminates pagination. +func nextCursor(next, tail *s2.StreamPosition) (seq uint64, hasMore bool) { + if next != nil && tail != nil && next.SeqNum < tail.SeqNum { + return next.SeqNum, true + } + return 0, false +} diff --git a/server/lib/events/s2storage_test.go b/server/lib/events/s2storage_test.go new file mode 100644 index 00000000..5c2c2a13 --- /dev/null +++ b/server/lib/events/s2storage_test.go @@ -0,0 +1,37 @@ +package events + +import ( + "testing" + + "github.com/s2-streamstore/s2-sdk-go/s2" + "github.com/stretchr/testify/assert" +) + +// TestNextCursor covers the pagination-termination decision (the subtlest bit of +// the S2 read path) without needing a live S2 stream. +func TestNextCursor(t *testing.T) { + t.Parallel() + pos := func(seq uint64) *s2.StreamPosition { return &s2.StreamPosition{SeqNum: seq} } + + cases := []struct { + name string + next *s2.StreamPosition + tail *s2.StreamPosition + wantSeq uint64 + wantMore bool + }{ + {"more records remain", pos(5), pos(10), 5, true}, + {"caught up to tail terminates", pos(10), pos(10), 0, false}, + {"past tail terminates", pos(11), pos(10), 0, false}, + {"nil next (empty/exhausted read) terminates", nil, pos(10), 0, false}, + {"nil tail terminates", pos(5), nil, 0, false}, + {"both nil terminates", nil, nil, 0, false}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + seq, more := nextCursor(tc.next, tc.tail) + assert.Equal(t, tc.wantMore, more) + assert.Equal(t, tc.wantSeq, seq) + }) + } +} diff --git a/server/lib/oapi/oapi.go b/server/lib/oapi/oapi.go index 901aa0a9..ef68dff6 100644 --- a/server/lib/oapi/oapi.go +++ b/server/lib/oapi/oapi.go @@ -58,13 +58,13 @@ func (e BrowserApiCallEventType) Valid() bool { // Defines values for BrowserCaptchaSolveResultEventCategory. const ( - BrowserCaptchaSolveResultEventCategoryCaptcha BrowserCaptchaSolveResultEventCategory = "captcha" + Captcha BrowserCaptchaSolveResultEventCategory = "captcha" ) // Valid indicates whether the value is a known member of the BrowserCaptchaSolveResultEventCategory enum. func (e BrowserCaptchaSolveResultEventCategory) Valid() bool { switch e { - case BrowserCaptchaSolveResultEventCategoryCaptcha: + case Captcha: return true default: return false @@ -1409,48 +1409,6 @@ func (e ProcessStreamEventStream) Valid() bool { } } -// Defines values for PublishEventRequestCategory. -const ( - PublishEventRequestCategoryCaptcha PublishEventRequestCategory = "captcha" - PublishEventRequestCategoryConnection PublishEventRequestCategory = "connection" - PublishEventRequestCategoryConsole PublishEventRequestCategory = "console" - PublishEventRequestCategoryControl PublishEventRequestCategory = "control" - PublishEventRequestCategoryInteraction PublishEventRequestCategory = "interaction" - PublishEventRequestCategoryMonitor PublishEventRequestCategory = "monitor" - PublishEventRequestCategoryNetwork PublishEventRequestCategory = "network" - PublishEventRequestCategoryPage PublishEventRequestCategory = "page" - PublishEventRequestCategoryScreenshot PublishEventRequestCategory = "screenshot" - PublishEventRequestCategorySystem PublishEventRequestCategory = "system" -) - -// Valid indicates whether the value is a known member of the PublishEventRequestCategory enum. -func (e PublishEventRequestCategory) Valid() bool { - switch e { - case PublishEventRequestCategoryCaptcha: - return true - case PublishEventRequestCategoryConnection: - return true - case PublishEventRequestCategoryConsole: - return true - case PublishEventRequestCategoryControl: - return true - case PublishEventRequestCategoryInteraction: - return true - case PublishEventRequestCategoryMonitor: - return true - case PublishEventRequestCategoryNetwork: - return true - case PublishEventRequestCategoryPage: - return true - case PublishEventRequestCategoryScreenshot: - return true - case PublishEventRequestCategorySystem: - return true - default: - return false - } -} - // Defines values for TelemetryEventCategory. const ( TelemetryEventCategoryCaptcha TelemetryEventCategory = "captcha" @@ -3493,7 +3451,7 @@ type ProcessStreamEventStream string // PublishEventRequest Request body for publishing an event into the telemetry stream. type PublishEventRequest struct { // Category Event category. Optional and advisory: for a known event `type` the server assigns the category authoritatively and ignores this field. It is only used for unknown custom types, where it is required. - Category *PublishEventRequestCategory `json:"category,omitempty"` + Category *TelemetryEventCategory `json:"category,omitempty"` // Data Telemetry event payload. Data interface{} `json:"data,omitempty"` @@ -3505,9 +3463,6 @@ type PublishEventRequest struct { Type string `json:"type"` } -// PublishEventRequestCategory Event category. Optional and advisory: for a known event `type` the server assigns the category authoritatively and ignores this field. It is only used for unknown custom types, where it is required. -type PublishEventRequestCategory string - // RecorderInfo defines model for RecorderInfo. type RecorderInfo struct { // FinishedAt Timestamp when recording finished @@ -3850,9 +3805,27 @@ type DownloadRecordingParams struct { Id *string `form:"id,omitempty" json:"id,omitempty"` } +// ReadTelemetryEventsParams defines parameters for ReadTelemetryEvents. +type ReadTelemetryEventsParams struct { + // Offset Opaque pagination cursor: pass the `X-Next-Offset` value from the previous response to fetch the next page. When set, paging continues from this cursor and `since` is ignored, while `until` still bounds the page. It is not an event's `seq` field, so do not derive it from the response body. + Offset *int64 `form:"offset,omitempty" json:"offset,omitempty"` + + // Since Start of the window: an RFC-3339 timestamp, or a duration like `5m` meaning that long ago. Defaults to `5m`. Ignored when `offset` is set. + Since *string `form:"since,omitempty" json:"since,omitempty"` + + // Until End of the window (exclusive): an RFC-3339 timestamp, or a duration like `5m` meaning that long ago. + Until *string `form:"until,omitempty" json:"until,omitempty"` + + // Limit Maximum number of events per page. Defaults to 20. + Limit *int `form:"limit,omitempty" json:"limit,omitempty"` + + // Category Restrict results to these event categories. Repeat the parameter for multiple values. + Category *[]TelemetryEventCategory `form:"category,omitempty" json:"category,omitempty"` +} + // StreamTelemetryEventsParams defines parameters for StreamTelemetryEvents. type StreamTelemetryEventsParams struct { - // Replay Pass `all` to start from the oldest retained event. Ring buffer caps at 1024; older events are evicted and surface as a first `id` greater than 1. + // Replay Pass `all` to start from the oldest retained event. The stream's buffer is bounded, so once older events are evicted the first `id` may be greater than 1. Replay *StreamTelemetryEventsParamsReplay `form:"replay,omitempty" json:"replay,omitempty"` // LastEventID Resume after this sequence number. Omit or send 0 to start from the current position. Sequence numbers are process-monotonic, so any previous value resumes correctly from that point. Takes precedence over `replay` when both are present, so SSE auto-reconnect resumes cleanly instead of re-replaying history. @@ -5191,6 +5164,9 @@ type ClientInterface interface { PutTelemetry(ctx context.Context, body PutTelemetryJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + // ReadTelemetryEvents request + ReadTelemetryEvents(ctx context.Context, params *ReadTelemetryEventsParams, reqEditors ...RequestEditorFn) (*http.Response, error) + // PublishTelemetryEventWithBody request with any body PublishTelemetryEventWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -6196,6 +6172,18 @@ func (c *Client) PutTelemetry(ctx context.Context, body PutTelemetryJSONRequestB return c.Client.Do(req) } +func (c *Client) ReadTelemetryEvents(ctx context.Context, params *ReadTelemetryEventsParams, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewReadTelemetryEventsRequest(c.Server, params) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + func (c *Client) PublishTelemetryEventWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewPublishTelemetryEventRequestWithBody(c.Server, contentType, body) if err != nil { @@ -8342,6 +8330,119 @@ func NewPutTelemetryRequestWithBody(server string, contentType string, body io.R return req, nil } +// NewReadTelemetryEventsRequest generates requests for ReadTelemetryEvents +func NewReadTelemetryEventsRequest(server string, params *ReadTelemetryEventsParams) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/telemetry/events") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + if params != nil { + queryValues := queryURL.Query() + + if params.Offset != nil { + + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "offset", *params.Offset, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "integer", Format: "int64"}); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + if params.Since != nil { + + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "since", *params.Since, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + if params.Until != nil { + + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "until", *params.Until, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + if params.Limit != nil { + + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "limit", *params.Limit, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "integer", Format: ""}); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + if params.Category != nil { + + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "category", *params.Category, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "array", Format: ""}); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + queryURL.RawQuery = queryValues.Encode() + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + // NewPublishTelemetryEventRequest calls the generic PublishTelemetryEvent builder with application/json body func NewPublishTelemetryEventRequest(server string, body PublishTelemetryEventJSONRequestBody) (*http.Request, error) { var bodyReader io.Reader @@ -8708,6 +8809,9 @@ type ClientWithResponsesInterface interface { PutTelemetryWithResponse(ctx context.Context, body PutTelemetryJSONRequestBody, reqEditors ...RequestEditorFn) (*PutTelemetryResponse, error) + // ReadTelemetryEventsWithResponse request + ReadTelemetryEventsWithResponse(ctx context.Context, params *ReadTelemetryEventsParams, reqEditors ...RequestEditorFn) (*ReadTelemetryEventsResponse, error) + // PublishTelemetryEventWithBodyWithResponse request with any body PublishTelemetryEventWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PublishTelemetryEventResponse, error) @@ -9979,6 +10083,30 @@ func (r PutTelemetryResponse) StatusCode() int { return 0 } +type ReadTelemetryEventsResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *[]TelemetryEnvelope + JSON400 *BadRequestError + JSON500 *InternalError +} + +// Status returns HTTPResponse.Status +func (r ReadTelemetryEventsResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r ReadTelemetryEventsResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + type PublishTelemetryEventResponse struct { Body []byte HTTPResponse *http.Response @@ -10740,6 +10868,15 @@ func (c *ClientWithResponses) PutTelemetryWithResponse(ctx context.Context, body return ParsePutTelemetryResponse(rsp) } +// ReadTelemetryEventsWithResponse request returning *ReadTelemetryEventsResponse +func (c *ClientWithResponses) ReadTelemetryEventsWithResponse(ctx context.Context, params *ReadTelemetryEventsParams, reqEditors ...RequestEditorFn) (*ReadTelemetryEventsResponse, error) { + rsp, err := c.ReadTelemetryEvents(ctx, params, reqEditors...) + if err != nil { + return nil, err + } + return ParseReadTelemetryEventsResponse(rsp) +} + // PublishTelemetryEventWithBodyWithResponse request with arbitrary body returning *PublishTelemetryEventResponse func (c *ClientWithResponses) PublishTelemetryEventWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PublishTelemetryEventResponse, error) { rsp, err := c.PublishTelemetryEventWithBody(ctx, contentType, body, reqEditors...) @@ -12795,6 +12932,46 @@ func ParsePutTelemetryResponse(rsp *http.Response) (*PutTelemetryResponse, error return response, nil } +// ParseReadTelemetryEventsResponse parses an HTTP response from a ReadTelemetryEventsWithResponse call +func ParseReadTelemetryEventsResponse(rsp *http.Response) (*ReadTelemetryEventsResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &ReadTelemetryEventsResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest []TelemetryEnvelope + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: + var dest BadRequestError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON400 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: + var dest InternalError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON500 = &dest + + } + + return response, nil +} + // ParsePublishTelemetryEventResponse parses an HTTP response from a PublishTelemetryEventWithResponse call func ParsePublishTelemetryEventResponse(rsp *http.Response) (*PublishTelemetryEventResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) @@ -13005,6 +13182,9 @@ type ServerInterface interface { // Set telemetry configuration // (PUT /telemetry) PutTelemetry(w http.ResponseWriter, r *http.Request) + // Read telemetry events for a browser session + // (GET /telemetry/events) + ReadTelemetryEvents(w http.ResponseWriter, r *http.Request, params ReadTelemetryEventsParams) // Publish an event into the telemetry stream // (POST /telemetry/events) PublishTelemetryEvent(w http.ResponseWriter, r *http.Request) @@ -13335,6 +13515,12 @@ func (_ Unimplemented) PutTelemetry(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotImplemented) } +// Read telemetry events for a browser session +// (GET /telemetry/events) +func (_ Unimplemented) ReadTelemetryEvents(w http.ResponseWriter, r *http.Request, params ReadTelemetryEventsParams) { + w.WriteHeader(http.StatusNotImplemented) +} + // Publish an event into the telemetry stream // (POST /telemetry/events) func (_ Unimplemented) PublishTelemetryEvent(w http.ResponseWriter, r *http.Request) { @@ -14368,6 +14554,65 @@ func (siw *ServerInterfaceWrapper) PutTelemetry(w http.ResponseWriter, r *http.R handler.ServeHTTP(w, r) } +// ReadTelemetryEvents operation middleware +func (siw *ServerInterfaceWrapper) ReadTelemetryEvents(w http.ResponseWriter, r *http.Request) { + + var err error + + // Parameter object where we will unmarshal all parameters from the context + var params ReadTelemetryEventsParams + + // ------------- Optional query parameter "offset" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "offset", r.URL.Query(), ¶ms.Offset, runtime.BindQueryParameterOptions{Type: "integer", Format: "int64"}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "offset", Err: err}) + return + } + + // ------------- Optional query parameter "since" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "since", r.URL.Query(), ¶ms.Since, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "since", Err: err}) + return + } + + // ------------- Optional query parameter "until" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "until", r.URL.Query(), ¶ms.Until, runtime.BindQueryParameterOptions{Type: "string", Format: ""}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "until", Err: err}) + return + } + + // ------------- Optional query parameter "limit" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "limit", r.URL.Query(), ¶ms.Limit, runtime.BindQueryParameterOptions{Type: "integer", Format: ""}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "limit", Err: err}) + return + } + + // ------------- Optional query parameter "category" ------------- + + err = runtime.BindQueryParameterWithOptions("form", true, false, "category", r.URL.Query(), ¶ms.Category, runtime.BindQueryParameterOptions{Type: "array", Format: ""}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "category", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.ReadTelemetryEvents(w, r, params) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + // PublishTelemetryEvent operation middleware func (siw *ServerInterfaceWrapper) PublishTelemetryEvent(w http.ResponseWriter, r *http.Request) { @@ -14702,6 +14947,9 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl r.Group(func(r chi.Router) { r.Put(options.BaseURL+"/telemetry", wrapper.PutTelemetry) }) + r.Group(func(r chi.Router) { + r.Get(options.BaseURL+"/telemetry/events", wrapper.ReadTelemetryEvents) + }) r.Group(func(r chi.Router) { r.Post(options.BaseURL+"/telemetry/events", wrapper.PublishTelemetryEvent) }) @@ -16931,6 +17179,51 @@ func (response PutTelemetry500JSONResponse) VisitPutTelemetryResponse(w http.Res return json.NewEncoder(w).Encode(response) } +type ReadTelemetryEventsRequestObject struct { + Params ReadTelemetryEventsParams +} + +type ReadTelemetryEventsResponseObject interface { + VisitReadTelemetryEventsResponse(w http.ResponseWriter) error +} + +type ReadTelemetryEvents200ResponseHeaders struct { + XHasMore bool + XNextOffset int64 +} + +type ReadTelemetryEvents200JSONResponse struct { + Body []TelemetryEnvelope + Headers ReadTelemetryEvents200ResponseHeaders +} + +func (response ReadTelemetryEvents200JSONResponse) VisitReadTelemetryEventsResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.Header().Set("X-Has-More", fmt.Sprint(response.Headers.XHasMore)) + w.Header().Set("X-Next-Offset", fmt.Sprint(response.Headers.XNextOffset)) + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response.Body) +} + +type ReadTelemetryEvents400JSONResponse struct{ BadRequestErrorJSONResponse } + +func (response ReadTelemetryEvents400JSONResponse) VisitReadTelemetryEventsResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(400) + + return json.NewEncoder(w).Encode(response) +} + +type ReadTelemetryEvents500JSONResponse struct{ InternalErrorJSONResponse } + +func (response ReadTelemetryEvents500JSONResponse) VisitReadTelemetryEventsResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + type PublishTelemetryEventRequestObject struct { Body *PublishTelemetryEventJSONRequestBody } @@ -17183,6 +17476,9 @@ type StrictServerInterface interface { // Set telemetry configuration // (PUT /telemetry) PutTelemetry(ctx context.Context, request PutTelemetryRequestObject) (PutTelemetryResponseObject, error) + // Read telemetry events for a browser session + // (GET /telemetry/events) + ReadTelemetryEvents(ctx context.Context, request ReadTelemetryEventsRequestObject) (ReadTelemetryEventsResponseObject, error) // Publish an event into the telemetry stream // (POST /telemetry/events) PublishTelemetryEvent(ctx context.Context, request PublishTelemetryEventRequestObject) (PublishTelemetryEventResponseObject, error) @@ -18784,6 +19080,32 @@ func (sh *strictHandler) PutTelemetry(w http.ResponseWriter, r *http.Request) { } } +// ReadTelemetryEvents operation middleware +func (sh *strictHandler) ReadTelemetryEvents(w http.ResponseWriter, r *http.Request, params ReadTelemetryEventsParams) { + var request ReadTelemetryEventsRequestObject + + request.Params = params + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.ReadTelemetryEvents(ctx, request.(ReadTelemetryEventsRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "ReadTelemetryEvents") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(ReadTelemetryEventsResponseObject); ok { + if err := validResponse.VisitReadTelemetryEventsResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + // PublishTelemetryEvent operation middleware func (sh *strictHandler) PublishTelemetryEvent(w http.ResponseWriter, r *http.Request) { var request PublishTelemetryEventRequestObject @@ -18844,366 +19166,375 @@ func (sh *strictHandler) StreamTelemetryEvents(w http.ResponseWriter, r *http.Re // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "AQA=", + "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/server/openapi.yaml b/server/openapi.yaml index fcbc991a..be78e8d9 100644 --- a/server/openapi.yaml +++ b/server/openapi.yaml @@ -1379,6 +1379,87 @@ paths: description: Event accepted but filtered by the active telemetry config; not published. "400": $ref: "#/components/responses/BadRequestError" + get: + summary: Read telemetry events for a browser session + description: > + Reads a page of telemetry event data for the browser session, returned + in ascending sequence order. To page through results, pass the + `X-Next-Offset` value from the previous response as `offset` and repeat + while `X-Has-More` is true. Returns an empty list when telemetry data is + unavailable. + operationId: readTelemetryEvents + parameters: + - in: query + name: offset + required: false + description: > + Opaque pagination cursor: pass the `X-Next-Offset` value from the + previous response to fetch the next page. When set, paging continues + from this cursor and `since` is ignored, while `until` still bounds + the page. It is not an event's `seq` field, so do not derive it from + the response body. + schema: + type: integer + format: int64 + minimum: 0 + - in: query + name: since + required: false + description: > + Start of the window: an RFC-3339 timestamp, or a duration like `5m` + meaning that long ago. Defaults to `5m`. Ignored when `offset` is set. + schema: + type: string + - in: query + name: until + required: false + description: > + End of the window (exclusive): an RFC-3339 timestamp, or a duration + like `5m` meaning that long ago. + schema: + type: string + - in: query + name: limit + required: false + description: Maximum number of events per page. Defaults to 20. + schema: + type: integer + minimum: 1 + maximum: 100 + default: 20 + - in: query + name: category + required: false + description: Restrict results to these event categories. Repeat the parameter for multiple values. + schema: + type: array + items: + $ref: "#/components/schemas/TelemetryEventCategory" + style: form + explode: true + responses: + "200": + description: A page of telemetry events in ascending sequence order. + headers: + X-Has-More: + description: Whether more events are available beyond this page. + schema: + type: boolean + X-Next-Offset: + description: Cursor to pass as `offset` for the next page. Present only when X-Has-More is true. + schema: + type: integer + format: int64 + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/TelemetryEnvelope" + "400": + $ref: "#/components/responses/BadRequestError" + "500": + $ref: "#/components/responses/InternalError" /telemetry/stream: get: summary: Stream telemetry events as Server-Sent Events @@ -1417,9 +1498,9 @@ paths: enum: [all] required: false description: > - Pass `all` to start from the oldest retained event. Ring buffer - caps at 1024; older events are evicted and surface as a first - `id` greater than 1. + Pass `all` to start from the oldest retained event. The stream's + buffer is bounded, so once older events are evicted the first `id` + may be greater than 1. responses: "200": description: Live SSE stream of telemetry events. @@ -1478,19 +1559,7 @@ components: type: string description: Event type identifier. category: - type: string - description: Event category. - enum: - - console - - network - - page - - interaction - - control - - connection - - system - - screenshot - - captcha - - monitor + $ref: "#/components/schemas/TelemetryEventCategory" source: $ref: "#/components/schemas/BrowserEventSource" data: @@ -1501,6 +1570,20 @@ components: truncated: type: boolean description: Set by the server when the data field was truncated to fit the size limit. + TelemetryEventCategory: + type: string + description: Event category. + enum: + - console + - network + - page + - interaction + - control + - connection + - system + - screenshot + - captcha + - monitor BrowserEventSource: type: object description: Provenance metadata identifying which producer emitted the event. @@ -1537,22 +1620,11 @@ components: type: string description: Event type identifier. category: - type: string description: > Event category. Optional and advisory: for a known event `type` the server assigns the category authoritatively and ignores this field. It is only used for unknown custom types, where it is required. - enum: - - console - - network - - page - - interaction - - control - - connection - - system - - screenshot - - captcha - - monitor + $ref: "#/components/schemas/TelemetryEventCategory" source: $ref: "#/components/schemas/BrowserEventSource" data: