incident

package
v0.13.13 Latest Latest
Warning

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

Go to latest
Published: Jun 1, 2026 License: Apache-2.0 Imports: 16 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

View Source
var DefaultBucketConfigs = map[Severity]BucketConfig{
	SeverityError:   {Size: 20, RefillRate: 10},
	SeverityWarning: {Size: 10, RefillRate: 5},
	SeverityInfo:    {Size: 5, RefillRate: 2},
}

DefaultBucketConfigs are the production token-bucket parameters. Critical severity is not listed — it is always allowed (infinite bucket).

View Source
var DefaultPingDelays = PingDelays{
	Initial:    500 * time.Millisecond,
	Max:        10 * time.Second,
	ResetAfter: 30 * time.Second,
}

DefaultPingDelays are the production backoff parameters.

View Source
var ErrBlobEvicted = errors.New("blob evicted from store")

ErrBlobEvicted is returned by Read when the requested blob was evicted.

Functions

func Canonicalize

func Canonicalize(msg string) string

Canonicalize strips volatile parts of msg (timestamps, addresses, UUIDs, line numbers on non-app frames) so that two occurrences of the same logical error produce the same fingerprint regardless of when or where they fired.

func NextPingDelay

func NextPingDelay(occurrence int, delays PingDelays) time.Duration

NextPingDelay computes the ping delay for the nth occurrence of a fingerprint. occurrence is 1-based. The sequence doubles from Initial each occurrence until Max.

func PingPayloadSize

func PingPayloadSize(p PingPayload) int

PingPayloadSize returns the JSON-marshalled byte count of a payload for size assertions in tests.

Types

type ActivityDetector

type ActivityDetector struct {
	// contains filtered or unexported fields
}

ActivityDetector tracks agent hook activity to defer non-critical pings while the agent is busy making tool calls. When the agent goes idle (no hook events for idleWindow), onIdle is called so pending pings can flush.

Critical-severity events bypass the detector entirely — callers should not gate critical pings on IsActive().

func NewActivityDetector

func NewActivityDetector(activeWindow, idleWindow time.Duration, onIdle func()) *ActivityDetector

NewActivityDetector creates a detector. activeWindow is how long after the last hook event the agent is considered active (default 500ms). idleWindow is how long without activity before onIdle fires (default 3s). onIdle must not block.

func (*ActivityDetector) IsActive

func (ad *ActivityDetector) IsActive() bool

IsActive returns true if a hook event was seen within the last activeWindow.

func (*ActivityDetector) RecordHook

func (ad *ActivityDetector) RecordHook()

RecordHook records a hook event (pre-tool-use, post-tool-use, etc.). Resets the idle timer so onIdle fires idleWindow from now.

func (*ActivityDetector) Stop

func (ad *ActivityDetector) Stop()

Stop cancels the idle timer without firing it. Call on shutdown.

type BlobRef

type BlobRef struct {
	Hash string // sha256 hex of raw payload
	Size int
	MIME string // "text/plain", "application/json", "text/html"
}

BlobRef points to a payload stored in a BlobStore.

type BlobStore

type BlobStore struct {
	// contains filtered or unexported fields
}

BlobStore is a bytes-bounded content-addressed in-memory store with LRU eviction. Write computes the sha256 hash, deduplicates identical payloads, and evicts the least-recently-used blob when the budget is exceeded.

Writes are dispatched via a bounded channel (256 entries) to a background goroutine so callers never block on lock contention. Close drains the queue and stops the goroutine.

WriteAsync returns a BlobRef immediately (hash computed synchronously) and defers the actual storage. If the async queue is full the content may never land — readers fall back to IncidentEvent.Summary in that case.

func NewBlobStore

func NewBlobStore(maxBytes int64) *BlobStore

NewBlobStore creates a BlobStore limited to maxBytes of payload storage. Pass 0 for the default (16MB).

func (*BlobStore) Close

func (bs *BlobStore) Close()

Close stops the background goroutine after draining pending writes.

func (*BlobStore) Read

func (bs *BlobStore) Read(hash string) ([]byte, string, error)

Read retrieves the payload for hash. Returns ErrBlobEvicted if the blob was evicted since writing.

func (*BlobStore) Stats

func (bs *BlobStore) Stats() (used, max int64, count int)

Stats returns current usage for observability.

func (*BlobStore) Write

func (bs *BlobStore) Write(content []byte, mime string) (BlobRef, error)

Write stores content and returns a BlobRef. Identical content (same sha256) is stored only once. If the store is over budget the oldest entry is evicted. Write never blocks the caller: it is enqueued to a background goroutine. The returned channel receives the result when the write completes.

func (*BlobStore) WriteAsync

func (bs *BlobStore) WriteAsync(content []byte, mime string) BlobRef

WriteAsync computes the sha256 hash synchronously and returns a BlobRef immediately, then enqueues the actual content write to a background goroutine. If the async queue is full the ref is still returned but the content may never land — readers should fall back to IncidentEvent.Summary in that case.

type BucketConfig

type BucketConfig struct {
	Size       float64 // maximum tokens (burst capacity)
	RefillRate float64 // tokens added per second
}

BucketConfig configures a token bucket for one severity level.

type Bus

type Bus interface {
	Publish(IncidentEvent)
}

Bus is the central sink for all incident events. MPSCBus is the production implementation; NopBus discards events.

type ChannelNotifyFn

type ChannelNotifyFn func(content string, payload PingPayload) error

ChannelNotifyFn sends a claude/channel notification. content is compact text.

type Coalescer

type Coalescer struct {
	// contains filtered or unexported fields
}

Coalescer schedules ping emissions with exponential backoff per fingerprint. The first occurrence fires after Initial; repeats are delayed more. Severity escalation (e.g. warning → error on the same fingerprint) immediately cancels the pending timer and fires the ping.

func NewCoalescer

func NewCoalescer(delays PingDelays, pingFn func(IncidentEvent)) *Coalescer

NewCoalescer creates a Coalescer that calls pingFn when a ping should emit. pingFn must not block.

func (*Coalescer) ForceFlush

func (c *Coalescer) ForceFlush(fp string)

ForceFlush immediately emits a pending ping for fp, if any, and resets the slot.

func (*Coalescer) Schedule

func (c *Coalescer) Schedule(ev IncidentEvent)

Schedule registers an event for coalesced ping emission. If a pending ping exists for the same fingerprint, its timer is rescheduled (potentially with a longer delay) unless the new event's severity is higher — in that case the pending timer is cancelled and the ping fires immediately.

func (*Coalescer) Stop

func (c *Coalescer) Stop()

Stop cancels all pending timers without firing them. Call on shutdown.

type Context

type Context struct {
	ProcessID   string
	ProxyID     string
	SessionID   string
	ProjectPath string
	URL         string
	PID         int
	PGID        int
	Port        int
}

Context carries identifiers for the resources involved in the incident.

type DedupEntry

type DedupEntry struct {
	First IncidentEvent
	Last  IncidentEvent
	Count int
	// contains filtered or unexported fields
}

DedupEntry tracks merged occurrences of a recurring incident fingerprint within a sliding deduplication window.

type Deduplicator

type Deduplicator struct {
	// contains filtered or unexported fields
}

Deduplicator merges repeated IncidentEvents that share a fingerprint into one DedupEntry, incrementing Count on each duplicate. The window is sliding: each duplicate resets the expiry. A fingerprint seen after its window expires is treated as a new occurrence.

func NewDeduplicator

func NewDeduplicator(window time.Duration) *Deduplicator

NewDeduplicator creates a Deduplicator with the given sliding window. Use 30s in production; tests can use shorter values.

func (*Deduplicator) Ingest

func (d *Deduplicator) Ingest(sessionID string, ev IncidentEvent) (merged bool, entry *DedupEntry)

Ingest adds ev to the deduplicator. Returns (true, existing entry) when ev deduplicates against a live entry; returns (false, new entry) on first occurrence or after the window has expired.

func (*Deduplicator) Len

func (d *Deduplicator) Len() int

Len returns the number of currently tracked fingerprints.

func (*Deduplicator) Trim

func (d *Deduplicator) Trim()

Trim removes expired entries, reclaiming memory.

type FlowController

type FlowController struct {
	// contains filtered or unexported fields
}

FlowController is a per-severity token bucket that rate-limits ping emissions. Critical severity is never throttled. Over-budget events should still be inboxed — only their ping emission is suppressed.

func NewFlowController

func NewFlowController(configs map[Severity]BucketConfig) *FlowController

NewFlowController creates a FlowController using the provided per-severity configs. Any severity not in configs (including SeverityCritical) is unthrottled.

func (*FlowController) TryPing

func (fc *FlowController) TryPing(sev Severity) bool

TryPing returns true if a ping for the given severity is allowed. Critical always returns true. Other severities consume a token; if the bucket is empty TryPing returns false (caller should inbox but not ping).

type Inbox

type Inbox struct {
	SessionID string
	// contains filtered or unexported fields
}

Inbox is a bounded per-session incident inbox with 4 severity bands (critical, error, warning, info). Concurrent-safe.

func NewInbox

func NewInbox(sessionID string) *Inbox

NewInbox creates a session inbox with default 100-entry band capacities.

func (*Inbox) Cursor

func (inbox *Inbox) Cursor() time.Time

Cursor returns the last acknowledged position (time of last MarkRead).

func (*Inbox) GC

func (inbox *Inbox) GC()

GC removes Read entries older than staleReadAge from all bands.

func (*Inbox) Ingest

func (inbox *Inbox) Ingest(entry *InboxEntry) InboxDelta

Ingest adds or merges an InboxEntry into the appropriate severity band. If the fingerprint already exists in any band, Count is incremented and LastSeenAt updated; severity escalation moves the entry to a higher band. Returns the resulting InboxDelta.

func (*Inbox) LoadCritical

func (inbox *Inbox) LoadCritical(path string) error

LoadCritical loads previously saved critical entries. Silently ignores missing file (first session).

func (*Inbox) MarkRead

func (inbox *Inbox) MarkRead(fingerprints []string, advanceCursor bool)

MarkRead marks entries as read and optionally advances the cursor.

func (*Inbox) Query

func (inbox *Inbox) Query(filter QueryFilter) ([]InboxEntry, Stats)

Query returns entries matching filter. Results are sorted newest first.

func (*Inbox) SaveCritical

func (inbox *Inbox) SaveCritical(path string) error

SaveCritical persists unread critical entries to path as JSON.

func (*Inbox) Stats

func (inbox *Inbox) Stats() Stats

Stats returns a snapshot of band counts and inbox health.

func (*Inbox) Subscribe

func (inbox *Inbox) Subscribe() (<-chan InboxDelta, func())

Subscribe returns a buffered channel delivering InboxDelta on each Ingest. Slow consumers receive dropped silently. Call cancel when done.

type InboxDelta

type InboxDelta struct {
	Entry     *InboxEntry
	IsNew     bool // false = fingerprint-merge
	Escalated bool // severity moved entry to a higher band
}

InboxDelta is delivered to Subscribe subscribers on each Ingest.

type InboxEntry

type InboxEntry struct {
	Fingerprint string         `json:"fingerprint"`
	FirstSeenAt time.Time      `json:"first_seen_at"`
	LastSeenAt  time.Time      `json:"last_seen_at"`
	Count       int            `json:"count"`
	Sample      *IncidentEvent `json:"sample,omitempty"`
	Severity    Severity       `json:"severity"`
	Read        bool           `json:"read"`
}

InboxEntry is a deduplicated, severity-banded record of an incident fingerprint.

type IncidentEvent

type IncidentEvent struct {
	ID          string
	Fingerprint string // sha256(source|category|canonical_msg|location)[:16]
	ReceivedAt  time.Time
	Source      Source
	Severity    Severity
	Category    string // finer-grained, e.g. "TypeError", "ECONNREFUSED"
	Summary     string // ≤200 bytes, single line
	PayloadRef  *BlobRef
	Ctx         Context
	Remediation Remediation
}

IncidentEvent is the canonical envelope for all 11 signal sources. ID is a time-ordered UUID v7 string — monotonic, sortable, cursor-friendly. Summary is capped at 200 bytes; oversized payloads live in BlobStore via PayloadRef.

func FromAlertMatch

func FromAlertMatch(m *overlay.AlertMatch, processID string) IncidentEvent

FromAlertMatch converts a pattern-matched line from the AlertScanner into an IncidentEvent. Build-failure patterns (webpack, vite, nextjs, rebuild, or any pattern whose description contains "build"/"compile") use SourceBuildFail; all others use SourceProcessAlert.

func FromFrontendError

func FromFrontendError(fe proxy.FrontendError, proxyID string) IncidentEvent

FromFrontendError converts a browser JS error captured by the proxy into an IncidentEvent. The error type name is extracted as the category.

func FromHTTPEntry

func FromHTTPEntry(he proxy.HTTPLogEntry, proxyID string) (IncidentEvent, bool)

FromHTTPEntry converts a logged HTTP request/response into an IncidentEvent. Returns (event, true) for 4xx/5xx responses; returns (zero, false) for 2xx/3xx which do not warrant incidents.

func FromHookStopFailure

func FromHookStopFailure(sessionID, errMsg, errDetails string) IncidentEvent

FromHookStopFailure creates an IncidentEvent for a Claude Code StopFailure hook event (the agent turn ended due to an API error). Uses plain primitives to avoid importing the daemon package.

sessionID is the Claude Code session that failed. errMsg is the short error label. errDetails carries the full error body (may be empty).

func FromPortConflict

func FromPortConflict(port, pid int, processName string) IncidentEvent

FromPortConflict converts a port-conflict detection record into an IncidentEvent. Uses plain primitives to avoid importing the daemon package.

port is the blocked port number. pid is the blocking process's OS PID. processName is a human-readable label for the blocking process (may be empty).

func FromProcessExit

func FromProcessExit(processID, reason string, exitCode int, stderrTail string) IncidentEvent

FromProcessExit converts a process death record into an IncidentEvent. Uses plain primitive params so the incident package stays free of daemon imports (avoiding a cycle when daemon wires the bus in L8).

reason is "stopped" | "crash" | "signal". exitCode is the OS exit code. stderrTail is the last N lines of stderr captured on exit (may be empty).

func FromProxyDiagnostic

func FromProxyDiagnostic(d proxy.ProxyDiagnostic, proxyID string) (IncidentEvent, bool)

FromProxyDiagnostic converts a proxy diagnostic event into an IncidentEvent. Info-level diagnostics are filtered out (returns zero, false). Transport-layer events (connection refused/timeout) use SourceTransportErr; all others use SourceProxyDiag.

func FromShutdown

func FromShutdown(reason string) IncidentEvent

FromShutdown creates an IncidentEvent signalling daemon/session shutdown.

func NewIncidentEvent

func NewIncidentEvent(src Source, sev Severity, category, msg string, ctx Context, store *BlobStore) IncidentEvent

NewIncidentEvent builds an IncidentEvent from the raw message and optional full payload. If payload is non-nil and longer than 1KB it is stored in store (if non-nil) and referenced via PayloadRef; otherwise it is truncated into Summary.

type IncidentView

type IncidentView struct {
	ID          string      `json:"id"`
	Fingerprint string      `json:"fingerprint"`
	FirstSeen   time.Time   `json:"first_seen"`
	LastSeen    time.Time   `json:"last_seen"`
	Count       int         `json:"count"`
	Severity    Severity    `json:"severity"`
	Source      Source      `json:"source"`
	Category    string      `json:"category,omitempty"`
	Summary     string      `json:"summary,omitempty"`
	Payload     *string     `json:"payload,omitempty"` // hydrated when detail="full"
	Ctx         Context     `json:"context,omitempty"`
	Remediation Remediation `json:"remediation,omitempty"`
	Read        bool        `json:"read"`
}

IncidentView is the pull-side projection of an InboxEntry, enriched with remediation guidance. Used by get_incidents output and AggregateRemediation.

type MCPNotifyFn

type MCPNotifyFn func(level string, payload PingPayload) error

MCPNotifyFn sends an MCP Log notification. level is "error", "warning", or "info".

type MPSCBus

type MPSCBus struct {
	// contains filtered or unexported fields
}

MPSCBus is a backpressure-safe, non-blocking event bus. Multiple producers call Fire or Publish; a single dispatch goroutine fans events to all active session pipelines. If the inbound channel is full, the newest event is dropped and the drop counter incremented. Every 100th drop synthesises one meta:bus_overflow warning incident so the AI agent sees bus pressure.

Session lifecycles:

  • AddSession: creates sessionPipeline and starts its dispatch goroutine.
  • RemoveSession: signals the goroutine to stop and waits up to 2s.
  • Close: removes all sessions and stops the bus.

func NewMPSCBus

func NewMPSCBus(pingConfig *PingConfig) *MPSCBus

NewMPSCBus creates a ready-to-use MPSCBus. pingConfig may be nil for a headless bus (no pinger). The bus starts the central dispatch goroutine immediately.

func (*MPSCBus) AddSession

func (b *MPSCBus) AddSession(sessionID string, mcpNotify MCPNotifyFn, channelNotify ChannelNotifyFn, ptyInject PTYInjectFn)

AddSession registers a new session pipeline. It is idempotent — calling again with the same sessionID replaces the old pipeline (the old one is torn down first).

mcpNotify, channelNotify, and ptyInject may all be nil (pinger channels are skipped). pingConfig may be nil; DefaultPingConfig() is used in that case.

func (*MPSCBus) Close

func (b *MPSCBus) Close()

Close shuts down the bus and all session pipelines.

func (*MPSCBus) Dropped

func (b *MPSCBus) Dropped() int64

Dropped returns the cumulative count of dropped events.

func (*MPSCBus) Fire

func (b *MPSCBus) Fire(ev *IncidentEvent)

Fire is the single non-blocking entry point for signal producers. It must complete in <1μs even when the bus is saturated.

func (*MPSCBus) HasSession added in v0.13.3

func (b *MPSCBus) HasSession(sessionID string) bool

HasSession reports whether a pipeline exists for the given session.

func (*MPSCBus) MarkReadSession added in v0.13.3

func (b *MPSCBus) MarkReadSession(sessionID string, fingerprints []string, advanceCursor bool)

MarkReadSession marks entries as read in the given session's inbox. No-op if the session has no pipeline.

func (*MPSCBus) Publish

func (b *MPSCBus) Publish(ev IncidentEvent)

Publish implements the Bus interface. It wraps Fire for callers that own the event value (not a pointer).

func (*MPSCBus) QuerySession added in v0.13.3

func (b *MPSCBus) QuerySession(sessionID string, filter QueryFilter) ([]InboxEntry, Stats)

QuerySession queries the incident inbox for the given session. Returns nil entries and empty stats if no pipeline exists for the session.

func (*MPSCBus) RemoveSession

func (b *MPSCBus) RemoveSession(sessionID string)

RemoveSession tears down the session pipeline for sessionID and waits up to sessionDrainWindow for in-flight events to finish.

type NopBus

type NopBus struct{}

NopBus discards every event published to it.

func (NopBus) Publish

func (NopBus) Publish(IncidentEvent)

type PTYInjectFn

type PTYInjectFn func(line string) error

PTYInjectFn injects a single-line status string into PTY stdin.

type PingConfig

type PingConfig struct {
	MCPNotifications bool
	ChannelEnabled   bool
	PTYInjection     bool
	MaxTopFPs        int        // 0 → defaultMaxTopFingerprints
	IncludeSummary   bool       // include short summary text per top entry
	Delays           PingDelays // coalesce backoff; zero → DefaultPingDelays
}

PingConfig controls which channels emit pings and their content density.

type PingDelays

type PingDelays struct {
	Initial    time.Duration // first ping delay (default 500ms)
	Max        time.Duration // backoff ceiling (default 10s)
	ResetAfter time.Duration // inactivity window before counter resets (default 30s)
}

PingDelays configures the exponential backoff for coalesced ping emission. Initial is the first-occurrence delay; each repeat doubles it up to Max. After ResetAfter of no activity on a fingerprint the counter resets to Initial.

type PingEmitter

type PingEmitter struct {
	// contains filtered or unexported fields
}

PingEmitter subscribes to an Inbox, applies flow + activity gating, and fans pings out to up to 3 channels (MCP log, claude/channel, PTY stdin).

Critical events bypass the activity deferral and coalesce timer. Non-critical events are debounced via a session-level Coalescer slot.

func NewPingEmitter

func NewPingEmitter(
	inbox *Inbox,
	config PingConfig,
	flow *FlowController,
	activity *ActivityDetector,
	mcpNotify MCPNotifyFn,
	channelNotify ChannelNotifyFn,
	ptyInject PTYInjectFn,
) *PingEmitter

NewPingEmitter creates and starts a PingEmitter. Stop() must be called to release the subscription goroutine. Any of mcpNotify, channelNotify, or ptyInject may be nil (that channel is skipped).

activity.onIdle is not set by NewPingEmitter — the caller should wire it with: `ForceFlush(sessionPingFP)` on the returned emitter's coalescer if activity-triggered flushing is desired (typically in L8 wiring).

func (*PingEmitter) ForceFlushCoalesce

func (pe *PingEmitter) ForceFlushCoalesce()

ForceFlushCoalesce immediately emits any pending coalesced ping. Intended for activity-idle callbacks wired by L8.

func (*PingEmitter) Stop

func (pe *PingEmitter) Stop()

Stop unsubscribes from the inbox and cancels all pending pings.

type PingPayload

type PingPayload struct {
	Type    string     `json:"type"`    // "agnt.incident_ping"
	Version int        `json:"version"` // 1
	Session string     `json:"session"`
	Summary PingStats  `json:"summary"`
	Top     []TopEntry `json:"top,omitempty"`
}

PingPayload is the stable schema for all channels. Target size < 2KB.

type PingStats

type PingStats struct {
	Critical int `json:"critical"`
	Error    int `json:"error"`
	Warning  int `json:"warning"`
	Info     int `json:"info"`
	New      int `json:"new"` // unread entries at time of ping
}

PingStats is the compact summary inside PingPayload.

type QueryFilter

type QueryFilter struct {
	Severities []Severity // empty = all
	Since      time.Time  // zero = no lower bound
	UnreadOnly bool
	Limit      int // 0 = all
}

QueryFilter narrows which entries Query returns.

type Remediation

type Remediation struct {
	PrimaryTool  string         `json:"primary_tool,omitempty"`
	PrimaryArgs  map[string]any `json:"primary_args,omitempty"`
	FallbackTool string         `json:"fallback_tool,omitempty"`
	FallbackArgs map[string]any `json:"fallback_args,omitempty"`
	SkillHint    string         `json:"skill_hint,omitempty"`
}

Remediation hints which MCP tool and skill should address this incident. Populated by the L7 routing table; zero value means "no hint".

func Resolve

func Resolve(ev *IncidentEvent) Remediation

Resolve returns remediation guidance for ev. Never returns a zero Remediation.

type Severity

type Severity string

Severity orders incidents by urgency.

const (
	SeverityCritical Severity = "critical"
	SeverityError    Severity = "error"
	SeverityWarning  Severity = "warning"
	SeverityInfo     Severity = "info"
)

type Source

type Source string

Source identifies where the incident originated.

const (
	SourceBrowserJS     Source = "browser_js"
	SourceHTTP5xx       Source = "http_5xx"
	SourceHTTP4xx       Source = "http_4xx"
	SourceTransportErr  Source = "transport_err"
	SourceProxyDiag     Source = "proxy_diag"
	SourceProcessAlert  Source = "process_alert"
	SourceProcessOutput Source = "process_output"
	SourceProcessCrash  Source = "process_crash"
	SourceBuildFail     Source = "build_fail"
	SourcePortConflict  Source = "port_conflict"
	SourceShutdown      Source = "shutdown"
	SourceHookStopFail  Source = "hook_stop_failure"
)

type Stats

type Stats struct {
	Critical     int       `json:"critical"`
	Error        int       `json:"error"`
	Warning      int       `json:"warning"`
	Info         int       `json:"info"`
	Dropped      int64     `json:"dropped"`
	OldestUnread time.Time `json:"oldest_unread,omitempty"`
	Cursor       time.Time `json:"cursor,omitempty"`
}

Stats is a point-in-time snapshot of inbox health.

type ToolSuggestion

type ToolSuggestion struct {
	Tool      string         `json:"tool"`
	Args      map[string]any `json:"args,omitempty"`
	Rationale string         `json:"rationale"`
}

ToolSuggestion is a pre-filled tool call with a one-line rationale.

func AggregateRemediation

func AggregateRemediation(incidents []IncidentView) (skill string, tools []ToolSuggestion)

AggregateRemediation picks the dominant skill+tools across a slice of incidents, weighted by occurrence count.

type TopEntry

type TopEntry struct {
	Fingerprint string   `json:"fp"`
	Category    string   `json:"category,omitempty"`
	Count       int      `json:"count"`
	Severity    Severity `json:"severity"`
	Summary     string   `json:"summary,omitempty"`
}

TopEntry is one fingerprint row in PingPayload.Top.

Jump to

Keyboard shortcuts

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