Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
a03a5bd
feat: add failing test stubs for events package
archandatta Mar 20, 2026
42f415f
feat: add BrowserEvent struct, CategoryFor, and truncateIfNeeded
archandatta Mar 20, 2026
fa67dff
feat: add RingBuffer with closed-channel broadcast fan-out
archandatta Mar 20, 2026
115c720
feat: add FileWriter per-category JSONL appender
archandatta Mar 20, 2026
f07e40d
feat: add Pipeline glue type sequencing truncation, file write, and r…
archandatta Mar 20, 2026
18fdb6d
review: fix truncateIfNeeded branch split, atomic.Pointer[string], Re…
archandatta Mar 27, 2026
997edb4
review: remove dead RingBuffer count field, fix FileWriter mutex doc,…
archandatta Mar 27, 2026
e5153da
chore: clean up maxS2RecordBytes comment
archandatta Mar 27, 2026
1644fe7
fix: serialise Pipeline.Publish to guarantee monotonic seq delivery o…
archandatta Mar 27, 2026
36cff2d
review
archandatta Mar 30, 2026
339d7d3
refactor: rename BrowserEvent to Event, DetailDefault to DetailStandard
archandatta Mar 31, 2026
b370416
refactor: restructure Source as nested object with Kind, Event, Metadata
archandatta Mar 31, 2026
41a7aee
refactor: extract Envelope wrapper, move seq and capture_session_id o…
archandatta Mar 31, 2026
9f4c808
refactor: unify seq as universal cursor, add NewReader(afterSeq)
archandatta Mar 31, 2026
6c82459
refactor: return ReadResult instead of synthetic drop events
archandatta Mar 31, 2026
6506ed7
test: add NewReader resume tests for mid-stream, at-latest, and evict…
archandatta Mar 31, 2026
8ecd492
review: fmt
archandatta Apr 1, 2026
e572e7b
fix: guard against nil marshal data and oversized non-data envelopes
archandatta Apr 1, 2026
6fc54c5
review: address naming & constructor feedback
archandatta Apr 1, 2026
d791a9e
fix: file rename
archandatta Apr 1, 2026
bf091f5
review: remove redundant atomic and dead branch in events package
archandatta Apr 2, 2026
5d958df
Merge branch 'main' into archand/kernel-1116/browser-logging
archandatta Apr 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions server/lib/events/event.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package events

import (
"encoding/json"
)

// maxS2RecordBytes is the maximum record size for the S2 event pipeline (1 MB).
const maxS2RecordBytes = 1_000_000

// EventCategory is a first-class envelope field that determines log file routing.
type EventCategory string

const (
CategoryConsole EventCategory = "console"
CategoryNetwork EventCategory = "network"
CategoryPage EventCategory = "page"
CategoryInteraction EventCategory = "interaction"
CategoryLiveview EventCategory = "liveview"
CategoryCaptcha EventCategory = "captcha"
CategorySystem EventCategory = "system"
)

// SourceKind identifies the provenance of an event — which subsystem produced it.
type SourceKind string

const (
SourceCDP SourceKind = "cdp"
SourceKernelAPI SourceKind = "kernel_api"
SourceExtension SourceKind = "extension"
SourceLocalProcess SourceKind = "local_process"
)

// DetailLevel controls the verbosity of the event payload.
type DetailLevel string

const (
DetailMinimal DetailLevel = "minimal"
DetailDefault DetailLevel = "default"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: if this is meant to be a concrete level, i'd consider renaming default to standard. minimal/standard/verbose/raw reads like an actual ladder, whereas default feels more like a policy alias whose meaning could vary by producer.

DetailVerbose DetailLevel = "verbose"
DetailRaw DetailLevel = "raw"
)

// BrowserEvent is the canonical event structure for the browser capture pipeline.
//
// The envelope is designed so that capture config and subscription selectors
// can operate on stable, first-class fields (Category, SourceKind, DetailLevel)
// without parsing the Type string. Type carries semantic identity (e.g.
// "console.log", "network.request"); SourceEvent carries the raw upstream
// event name (e.g. "Runtime.consoleAPICalled") for diagnostics.
//
// DetailLevel is always serialised (no omitempty). Pipeline.Publish defaults it
// to DetailDefault; callers constructing events outside a Pipeline should set it
// explicitly.
type BrowserEvent struct {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: i wonder if Event would be a cleaner name than BrowserEvent. it feels more future-proof if extension/local-process/server-native producers end up as first-class peers.

CaptureSessionID string `json:"capture_session_id"`
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is capture_session_id intended to identify? this reads more like pipeline/native-producer lifecycle metadata than part of the portable event schema. if resume/subscription are meant to work across heterogeneous producers, i'd be careful about baking a producer-owned session id into every event.

Seq uint64 `json:"seq"`
Ts int64 `json:"ts"`
Type string `json:"type"`
Category EventCategory `json:"category"`
SourceKind SourceKind `json:"source_kind"`
SourceEvent string `json:"source_event,omitempty"`
DetailLevel DetailLevel `json:"detail_level"`
TargetID string `json:"target_id,omitempty"`
CDPSessionID string `json:"cdp_session_id,omitempty"`
FrameID string `json:"frame_id,omitempty"`
ParentFrameID string `json:"parent_frame_id,omitempty"`
URL string `json:"url,omitempty"`
Data json.RawMessage `json:"data,omitempty"`
Truncated bool `json:"truncated,omitempty"`
}

// truncateIfNeeded returns a copy of ev with Data replaced with json.RawMessage("null")
// and Truncated set to true if the marshaled size exceeds maxS2RecordBytes.
// Never attempt byte-slice truncation of the Data field — partial JSON is invalid.
func truncateIfNeeded(ev BrowserEvent) BrowserEvent {
candidate, err := json.Marshal(ev)
if err != nil {
// Marshal should never fail for BrowserEvent (all fields are JSON-safe),
// but if it does return ev unchanged rather than silently nulling Data.
return ev
}
if len(candidate) <= maxS2RecordBytes {
return ev
}
ev.Data = json.RawMessage("null")
ev.Truncated = true
return ev
}
Comment thread
cursor[bot] marked this conversation as resolved.
Loading
Loading