telemetry

package
v0.0.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 17, 2026 License: MIT Imports: 4 Imported by: 0

Documentation

Overview

Package telemetry is the model for browser-side diagnostic events that the hub collects so a front-end defect can be debugged from the outside (Task 20251).

Why this exists

The hub already logged uncaught JavaScript exceptions: the error boundary POSTs to /api/client-error and the structured logger writes a line. That covers the case where the page throws, and it covers nothing else.

The front-end defects this project actually spends tasks on do not throw. "Swiping right selects the first item once and then stops" (Task 20242), "the refresh resets the view" (20237), "there is no button to add a task" (20243) — each of those was a silent behavioural failure on a Meta Ray-Ban Display, a device with no developer tools, no console, no network inspector and no way for a wearer to report anything beyond a sentence of prose. Every one of them was diagnosed by building a simulator of the device and guessing until the guess reproduced the prose. That is an expensive way to find out which branch ran.

So the unit of collection here is not the exception. It is the breadcrumb: an ordered trail of what the page did — the gesture it received, the view it opened, the request it issued and the status that came back — with errors as one kind of entry among several. A trail answers "the cursor never moved past the first row" directly, because the gesture events are in it and the focus-change events are not.

Trust

Every field originates in a browser and none of it is trusted. The package therefore treats normalization as the whole of its job: kinds outside the known set collapse to KindNote, strings are clamped to fixed budgets, and Scrub strips credentials before anything is stored. The last is not hypothetical — the glasses link carries its bearer token in the page URL, so an unscrubbed `url` field would copy a live credential out of a URL bar and into a database table that a different permission can read.

Stdlib-only, so both pkg/statedb and pkg/ui can depend on it without either depending on the other.

Index

Constants

View Source
const (
	// MaxMessage bounds the human-readable summary.
	MaxMessage = 2 * 1024
	// MaxStack bounds a stack trace. The largest field by design: a truncated
	// stack is often a useless stack.
	MaxStack = 8 * 1024
	// MaxURL bounds a location or request URL.
	MaxURL = 1024
	// MaxDetail bounds the free-form structured detail blob.
	MaxDetail = 4 * 1024
	// MaxShort bounds the small identifying fields (session, view, kind
	// overflow, user agent).
	MaxShort = 256
	// MaxBatchEvents is the most events one POST may carry. A page that
	// generates more than this between flushes is looping, and the right
	// response to a loop is to drop the excess rather than to store it.
	MaxBatchEvents = 64
)

Field budgets. Generous enough for a real stack trace, small enough that the worst case of MaxBatchEvents events is a bounded write.

Variables

Kinds lists every known kind, in the order a UI should offer them.

Appended rather than slotted in beside its thematic neighbour: this order is what a filter UI renders, so adding one should not renumber the rest.

Functions

func Scrub

func Scrub(s string) string

Scrub removes credential material from a free-text field.

The load-bearing case is the glasses link. Its token is delivered in the page URL — `/glasses?token=cloop_glasses_…` — which means `location.href`, the natural value for an event's URL field, is a live credential valid for thirty days. The page lifts it out of the URL on load, but a trail is recorded from many places and one of them will eventually capture the original. Scrubbing at ingest is the boundary that holds regardless.

Deliberately conservative: it over-redacts rather than reasoning about whether a particular `key=` is sensitive. A redacted field a human can ask about is recoverable; a leaked credential in a table is not.

Types

type Batch

type Batch struct {
	Source  string      `json:"source"`
	Session string      `json:"session"`
	Release string      `json:"release"`
	Events  []WireEvent `json:"events"`
}

Batch is the wire shape of one POST. The session and source are named once per batch rather than per event because they are constant for the page's whole lifetime, and repeating them 64 times would be most of the payload.

type Context

type Context struct {
	Source    Source
	At        time.Time
	UserAgent string
	ClientIP  string
	Actor     string
}

Context is everything the server knows about a batch that the batch itself must not be allowed to claim.

type Event

type Event struct {
	ID      int64     `json:"id,omitempty"`
	At      time.Time `json:"at"`
	Source  Source    `json:"source"`
	Kind    Kind      `json:"kind"`
	Session string    `json:"session"`

	// Seq is the page's own counter for this session. It is the ordering key a
	// reader should trust: events are batched, batches race, and two events
	// written in the same millisecond are indistinguishable by any clock. A
	// gap in Seq is itself a finding — it means a batch was dropped in flight.
	Seq int64 `json:"seq"`

	// ClientMillis is Date.now() as the page saw it. Zero when unreported.
	ClientMillis int64 `json:"client_millis,omitempty"`

	Message string `json:"message,omitempty"`
	Stack   string `json:"stack,omitempty"`
	URL     string `json:"url,omitempty"`

	// View is the tab, screen or route the page was showing. The field that
	// turns "it broke" into "it broke on the task list".
	View string `json:"view,omitempty"`

	// Detail is a small JSON object the page attached, already serialised.
	// Free-form on purpose: the useful detail for a gesture (which key, which
	// cursor index) has nothing in common with the useful detail for a fetch
	// (which status, how many milliseconds), and a schema covering both would
	// be mostly empty columns.
	Detail string `json:"detail,omitempty"`

	// Server-observed context. Never client-supplied — a page that sent its
	// own Actor would be asserting an identity rather than reporting one.
	UserAgent string `json:"user_agent,omitempty"`
	ClientIP  string `json:"client_ip,omitempty"`
	Actor     string `json:"actor,omitempty"`
	Release   string `json:"release,omitempty"`
}

Event is one recorded moment in a browser session.

Two timestamps, because neither alone is sufficient. At is the hub's clock and is the only one worth sorting different sessions by. ClientMillis is the page's own clock, which is what gives the gaps *within* a session their meaning — and which on a wearable is routinely wrong in absolute terms, so it is stored as the page reported it and never used as an ordering key.

func Normalize

func Normalize(b Batch, ctx Context) []Event

Normalize converts a decoded batch into storable events.

Total, by construction: every reachable input produces a valid slice rather than an error. Telemetry that refuses malformed input is telemetry that goes silent exactly when the page is malfunctioning, which is the only time it is worth having. Unusable input is clamped, relabelled or dropped per-event, and the rest of the batch still lands.

ctx.Source wins over the batch's own claim: the route a batch arrived on knows which front end sent it, and the payload does not get a vote.

type Kind

type Kind string

Kind classifies an event. The set is closed for the same reason Source is, and deliberately small: a reader scanning a trail under time pressure needs to recognise every kind on sight.

const (
	// KindError is an uncaught exception.
	KindError Kind = "error"
	// KindRejection is an unhandled promise rejection.
	KindRejection Kind = "rejection"
	// KindGesture is an input the page received — a key, a swipe, a pinch, a
	// click on a control. The kind that makes a silent failure legible: if the
	// trail shows four gestures and one view change, the page ignored three.
	KindGesture Kind = "gesture"
	// KindView is a screen or tab the page opened.
	KindView Kind = "view"
	// KindFetch is a completed request the page issued, recorded whether it
	// succeeded or not so a trail shows both "the call never happened" and
	// "the call came back 403".
	KindFetch Kind = "fetch"
	// KindLifecycle is a page-level transition: load, visibility change,
	// WebSocket connect or drop, the terminal dead-link state.
	KindLifecycle Kind = "lifecycle"
	// KindInput is an input the page received and did *not* act on — the
	// complement of KindGesture (Task 20279). Separate rather than folded into
	// KindNote because it answers a question nothing else can: when a wearer
	// says a gesture did nothing, did anything reach the page at all? The
	// display glasses cost three tasks' worth of fixes on exactly that, because
	// a swipe arriving on a channel the page did not listen to left no trace of
	// any kind. "Show me what this page could not name" has to be one filter,
	// not a scan of every note ever recorded.
	KindInput Kind = "input"
	// KindNote is anything a caller wants recorded that is none of the above,
	// and the landing place for a kind this build does not recognise.
	KindNote Kind = "note"
)

func (Kind) Valid

func (k Kind) Valid() bool

Valid reports whether k is a known kind.

type Source

type Source string

Source names the front end an event came from. Kept to a closed set because it is the first filter a reader applies ("show me the glasses") and a free-text field would let a caller invent a spelling nobody searches for.

const (
	// SourceDashboard is the full browser dashboard.
	SourceDashboard Source = "dashboard"
	// SourceGlasses is the heads-up display page served at /glasses.
	SourceGlasses Source = "glasses"
)

func (Source) Valid

func (s Source) Valid() bool

Valid reports whether s is a known source.

type WireEvent

type WireEvent struct {
	Kind         string `json:"kind"`
	Seq          int64  `json:"seq"`
	ClientMillis int64  `json:"ts"`
	Message      string `json:"message"`
	Stack        string `json:"stack"`
	URL          string `json:"url"`
	View         string `json:"view"`
	Detail       string `json:"detail"`
}

WireEvent is one event as a browser sends it.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL