// Package apperr is the pure half of the Errors tab (spec // 2026-09-07-errors-and-analytics-design.md §3.1–3.2): the wire record, // sanitisation, fingerprinting, the box-side coalescing sink, the chunk // read and the grouping fold. No I/O and no clock of its own — the same // split internal/applog keeps: internal/host ships, internal/edge and the // activator produce, internal/console reads. package apperr import ( "time" "github.com/carlosframework/platform/internal/applog" ) // Sources an error can come from (spec §3.1). log is never stored in the // errors/ family — it is derived on read from logs/ (§3.2). const ( SourceLog = "log" SourceEdge = "edge" SourceServer = "server" SourceBrowser = "browser" ) // Kinds the platform itself produces. A server or browser caller sets its // own kind (an exception class, typically). const ( KindHTTP = "http" // an edge-observed 5xx KindLog = "log" // a log line at or above the configured level ) // FPVersion stamps the fingerprint algorithm so it can change without // merging old groups into new ones. const FPVersion = 1 // Record is one coalesced error on the wire — one JSONL line inside an // errors/ chunk. Additive shape (AGENTS.md rule): every field but TS, // Source, Name and FP is omitempty, unknown keys are ignored on read. // Count, First and Last are the coalescing state: one record stands for // Count occurrences of FP on Host under Release between First and Last; // the other fields are the first occurrence's sample. type Record struct { TS time.Time `json:"ts"` Box string `json:"box,omitempty"` Host string `json:"host,omitempty"` Source string `json:"source"` Kind string `json:"kind,omitempty"` Name string `json:"name"` Level string `json:"level,omitempty"` // log source: warn | error Path string `json:"path,omitempty"` Status int `json:"status,omitempty"` Cause string `json:"cause,omitempty"` // Availability: why the edge could not get an answer (availability.go) Stream string `json:"stream,omitempty"` // log source only: the applog stream, derived on read and never stored Release string `json:"release,omitempty"` Stack string `json:"stack,omitempty"` Fields map[string]string `json:"fields,omitempty"` FP string `json:"fp"` FPV int `json:"fpv,omitempty"` Count int `json:"count,omitempty"` First time.Time `json:"first,omitempty"` Last time.Time `json:"last,omitempty"` Seq uint64 `json:"seq,omitempty"` } // MaxStack bounds a stored stack (spec §3.1). const MaxStack = 8 << 10 // FromLog derives the log-source record for one applog line (spec §3.2: // read-derived, never stored twice). Fingerprinted on the normalised // message alone — a log line has no frame. func FromLog(r applog.Record) Record { name := Sanitize(r.Msg) return Record{ TS: r.TS, Box: r.Box, Host: r.Host, Source: SourceLog, Kind: KindLog, Name: name, Level: r.Level, Stream: r.Stream, Fields: r.Fields, FP: Fingerprint(SourceLog, KindLog, name, ""), FPV: FPVersion, Count: 1, First: r.TS, Last: r.TS, Seq: r.Seq, } }