Documentation
¶
Index ¶
- Constants
- Variables
- func BucketTokens(n int) string
- func DetectEnvKind(env map[string]string, fs FileProber, osName string, tty TTYChecker) (EnvKind, EnvMarkers)
- func DetectEnvKindOnce() (EnvKind, EnvMarkers)
- func DetectEnvKindOnceWith(detector func() (EnvKind, EnvMarkers)) (EnvKind, EnvMarkers)
- func EffectiveTelemetryEnabled(cfg *config.Config) bool
- func EnsureActivationBucket(db *bbolt.DB) error
- func EnsureDiagnosticsCountersBucket(db *bbolt.DB) error
- func EnsureFunnelBucket(db *bbolt.DB) error
- func IsBuiltinTool(name string) bool
- func IsValidEnvKind(v EnvKind) bool
- func IsValidErrorCategory(c ErrorCategory) bool
- func IsValidLaunchSource(v LaunchSource) bool
- func MaybePrintFirstRunNotice(cfg *config.Config, w io.Writer) bool
- func PopulateBlockedValues()
- func RecordBuiltinToolOn(reg *CounterRegistry, name string)
- func RecordErrorOn(reg *CounterRegistry, c ErrorCategory)
- func RecordRESTRequestOn(reg *CounterRegistry, method, template, statusClass string)
- func RecordSurfaceOn(reg *CounterRegistry, s Surface)
- func RecordUpstreamToolOn(reg *CounterRegistry)
- func ResetEnvKindForTest()
- func ScanForPII(payloadJSON []byte) error
- func SortedOAuthProviderTypes(types []string) []string
- func TelemetryDisableTransition(prior, next *config.Config) bool
- func ValidateCategory(category string) bool
- func ValidateMessage(message string) error
- type ActivationState
- type ActivationStore
- type AnonymityViolation
- type AutostartReader
- type CounterRegistry
- func (r *CounterRegistry) AnonymityViolationsTotal() int64
- func (r *CounterRegistry) RecordAnonymityViolation()
- func (r *CounterRegistry) RecordBuiltinTool(name string)
- func (r *CounterRegistry) RecordDoctorRun(results []DoctorCheckResult)
- func (r *CounterRegistry) RecordError(c ErrorCategory)
- func (r *CounterRegistry) RecordRESTRequest(method, template, statusClass string)
- func (r *CounterRegistry) RecordSurface(s Surface)
- func (r *CounterRegistry) RecordUpstreamTool()
- func (r *CounterRegistry) Reset()
- func (r *CounterRegistry) Snapshot() RegistrySnapshot
- type DiagnosticsCounterStore
- type DiagnosticsCounters
- type DoctorCheckResult
- type DoctorCounts
- type EnvDisabledReason
- type EnvKind
- type EnvMarkers
- type ErrorCategory
- type FeatureFlagSnapshot
- type FeedbackContext
- type FeedbackRequest
- type FeedbackResponse
- type FileProber
- type FunnelState
- type FunnelStore
- type HandshakeChecker
- type HeartbeatPayload
- type LaunchSource
- type OnboardingSnapshot
- type OptOutBeacon
- type PPIDChecker
- type PreChurnStore
- type RateLimiter
- type RegistrySnapshot
- type RuntimeStats
- type Service
- func (s *Service) ActivationDB() *bbolt.DB
- func (s *Service) ActivationStore() ActivationStore
- func (s *Service) BuildPayload() HeartbeatPayload
- func (s *Service) DiagnosticsCounterDB() *bbolt.DB
- func (s *Service) DiagnosticsCounterStore() DiagnosticsCounterStore
- func (s *Service) EmitOptOutBeacon(ctx context.Context) bool
- func (s *Service) EnvDisabledReason() EnvDisabledReason
- func (s *Service) FunnelDB() *bbolt.DB
- func (s *Service) FunnelStore() FunnelStore
- func (s *Service) NotifyConfigChanged(newCfg *config.Config)
- func (s *Service) RecordWebUIOpen()
- func (s *Service) Registry() *CounterRegistry
- func (s *Service) SendOptOutBeacon(ctx context.Context) error
- func (s *Service) SetActivationStore(store ActivationStore, db *bbolt.DB)
- func (s *Service) SetAutostartReader(r *AutostartReader)
- func (s *Service) SetConfiguredIDECountProvider(fn func() int)
- func (s *Service) SetDiagnosticsCounterStore(store DiagnosticsCounterStore, db *bbolt.DB)
- func (s *Service) SetFunnelStore(store FunnelStore, db *bbolt.DB)
- func (s *Service) SetOnboardingProvider(fn func() *OnboardingSnapshot)
- func (s *Service) SetPreChurn(previousShutdown string, store PreChurnStore, db *bbolt.DB)
- func (s *Service) SetRuntimeStats(stats RuntimeStats)
- func (s *Service) Start(ctx context.Context)
- func (s *Service) Stop()
- func (s *Service) SubmitFeedback(ctx context.Context, req *FeedbackRequest) (*FeedbackResponse, error)
- type Surface
- type TTYChecker
Constants ¶
const ( // PreviousShutdownClean: the previous instance completed the graceful // shutdown path. PreviousShutdownClean = "clean" // PreviousShutdownCrash: the previous instance armed the marker but never // resolved it (SIGKILL, panic, power loss). PreviousShutdownCrash = "crash" // PreviousShutdownUnknown: no prior marker exists (first-ever run). Empty // so omitempty drops the field — a fresh install is never misreported as // a crash (FR-013). PreviousShutdownUnknown = "" )
Wire enum for the heartbeat previous_shutdown field (FR-010).
const ActivationBucketName = "activation"
ActivationBucketName is the BBolt bucket that stores retention activation state (spec 044). Keys inside the bucket are fixed at compile time; missing keys default to their zero value.
const DiagnosticsCountersBucketName = "diagnostics_counters"
DiagnosticsCountersBucketName is the BBolt bucket that stores Phase H diagnostics counters (spec 044). Keys inside are defined as constants below.
const FirstRunNoticeText = `` /* 192-byte string literal not displayed */
FirstRunNoticeText is the one-time banner printed to stderr the first time `mcpproxy serve` runs against a config that has no telemetry_notice_shown flag. Spec 042 User Story 10.
const FunnelBucketName = "telemetry_funnel"
FunnelBucketName is the BBolt bucket that stores Spec 080 US2 funnel observability state: the lifetime web-UI open counter, the first-install day stamp, and the trailing 30-day active-day set. Only derived integers (counter value, whole-day age, day-set cardinality) ever leave the machine; the raw structures below are local-only (FR-006..FR-008).
const MaxMCPClientsSeen = 16
MaxMCPClientsSeen bounds the cardinality of the mcp_clients_seen_ever list. 17th insertion is dropped.
const OptOutEvent = "telemetry_disabled"
OptOutEvent is the distinguishing field value carried by the one-time opt-out beacon. Receivers route a payload to the opt-out funnel by the presence of this `event` value on the existing /heartbeat ingest path — no new endpoint is required (MCP-2482).
const PreChurnBucketName = "telemetry_prechurn"
PreChurnBucketName is the BBolt bucket that stores the Spec 080 US3 pre-churn snapshot state: the armed/resolved shutdown marker behind previous_shutdown, and the single most-recent MCPX_* code behind last_error_code (FR-010..FR-013). Both persisted values are fixed-enum strings — never free text, paths, or server names.
const SchemaVersion = 7
SchemaVersion is the heartbeat payload schema version. v1 payloads have no such field; receivers can route by absence vs presence.
v3 (schema bump from 2): adds feature_flags.docker_available, server_protocol_counts, and server_docker_isolated_count.
v4 (schema bump from 3): adds onboarding-funnel fields per Spec 046 — connected_client_count, connected_client_ids, wizard_engaged, wizard_connect_step, wizard_server_step. Forward-compatible: existing v3 consumers ignore the new fields.
v5 (schema bump from 4): adds docker-isolation visibility per MCP-2745 — feature_flags.docker_isolation_enabled, feature_flags.docker_cli_source (4-way enum: path|bundled|login_shell|absent, the #696 fleet signal), and three new diagnostics codes surfaced via error_code_counts_24h (MCPX_DOCKER_CLI_NOT_FOUND / MCPX_DOCKER_EXEC_NOT_FOUND / MCPX_DOCKER_OCI_RUNTIME). Additive only — v3/v4 consumers ignore them.
v6 (schema bump from 5): adds machine_id — a stable, non-reversible hash of the OS machine id (HMAC-SHA256 keyed by the OS machine id, scoped by an app-specific key). It lets the dashboard dedup installs whose anonymous_id churns every run (ephemeral Docker layers, throwaway HOMEs, CI). Additive and forward-compatible: v3/v4/v5 consumers ignore it, and the ingest worker stores payload_json wholesale without rejecting unknown fields or higher schema versions.
v7 (schema bump from 6): Spec 080 — honest activation funnel + churn instrumentation. Additive only; v6-and-earlier consumers ignore every addition:
- wizard_connect_step enum widened with "completed_external" (connected outside the wizard — CLI/ConnectModal/manual config — detected at dismissal). Consumers switching on completed|skipped must treat unknown values as "other/engaged".
- wizard_shown (bool): wizard rendered at least once, making "shown but ignored" observable alongside wizard_engaged.
- web_ui_opened (int64): lifetime count of embedded Web UI index-document serves, independent of surface_requests.webui.
- days_since_install (*int): whole-day UTC install age from a persisted first-install day stamp; 0 is transmitted, nil (store not wired) omits.
- active_days_30d (int): count of distinct active UTC days in the trailing 30-day window; the per-day set never leaves the machine.
- previous_shutdown (enum "clean"|"crash", absent = unknown/first run): how the PREVIOUS process instance ended, from a persisted marker.
- last_error_code (enum MCPX_*): most recent stable diagnostic code, persisted across restarts; never message text, names, or paths.
All v7 fields are omitempty (zero-valued payloads stay shape-compatible with v6), fixed-enum/boolean/non-negative-integer only (enforced by ScanForPII), and ride the existing opt-out gate.
Variables ¶
var AnonymityBlockedPrefixes = []string{
"/Users/",
"/home/",
`C:\\Users\\`,
"/var/folders/",
}
AnonymityBlockedPrefixes is the fixed list of byte-prefix substrings that MUST NOT appear anywhere in a serialized telemetry payload. These patterns catch accidental leaks of user home directories, macOS temp folders, and Windows user profile paths.
Additional runtime-detected values (current hostname, home-dir basename, env-var values from a blocked set) are appended by the telemetry service at startup via BlockedValues — see T025 / spec 044. NOTE on backslash form: telemetry payloads are JSON, and JSON encodes a single backslash as two bytes (`\\`). So the Windows user-profile prefix appears on the wire as `C:\\Users\\`. We match the JSON-escaped form here.
var BlockedValues []string
BlockedValues is the runtime-populated list of literal substrings that MUST NOT appear in a payload. Populated at startup by the telemetry service from:
- os.Hostname() (if non-empty)
- last path component of os.UserHomeDir() (if non-empty)
- values of env vars in the blocked set (GITHUB_TOKEN, GITLAB_TOKEN, OPENAI_API_KEY, ANTHROPIC_API_KEY, GOOGLE_API_KEY), when non-empty.
The package-level var is intentionally mutable so tests can inject fake values. Production code should call SetBlockedValues (added in T025) once at startup; after that, treat the slice as read-only.
var ErrAnonymityViolation = errors.New("telemetry anonymity violation")
ErrAnonymityViolation is a sentinel returned (wrapped) by ScanForPII when a violation is found. Tests use errors.As against *AnonymityViolation for richer detail; errors.Is against ErrAnonymityViolation remains valid for callers that only care about the category.
Functions ¶
func BucketTokens ¶ added in v0.25.0
BucketTokens maps a raw token count to the fixed bucket enum. Buckets: "0", "1_100", "100_1k", "1k_10k", "10k_100k", "100k_plus".
Boundaries (inclusive upper edge matches the bucket label):
- 0 -> "0"
- 1..100 -> "1_100"
- 101..1000 -> "100_1k"
- 1001..10000 -> "1k_10k"
- 10001..100000 -> "10k_100k"
- >100000 -> "100k_plus"
func DetectEnvKind ¶ added in v0.25.0
func DetectEnvKind(env map[string]string, fs FileProber, osName string, tty TTYChecker) (EnvKind, EnvMarkers)
DetectEnvKind is the pure classifier that decides env_kind + env_markers from an injected environment map, filesystem prober, OS name, and TTY checker. Ordering is authoritative: cloud_ide wins over CI wins over container.
NOTE: This ordering deviates from the original design doc (§4.2 / research.md R1) which put CI first. The change is motivated by gemini P1 cross-review: GitHub Codespaces and Gitpod routinely set CI=true alongside their cloud-IDE markers (CODESPACES, GITPOD_WORKSPACE_ID). Real humans in those ephemeral cloud sessions were being classified as `ci`, artificially deflating the Cloud IDE retention funnel. Prioritising cloud_ide over CI keeps interactive human sessions in the right bucket while still catching ordinary CI runners.
Inputs:
- env: map of env-var name → value (caller builds this from os.Environ).
- fs: filesystem prober for /.dockerenv + /run/.containerenv.
- osName: runtime.GOOS value ("darwin", "linux", "windows", …).
- tty: stdin-is-a-TTY checker.
Outputs:
- EnvKind: one of the canonical constants (never empty).
- EnvMarkers: booleans reflecting every observation feeding the decision, including ones that were irrelevant to the final verdict (e.g. on CI a container marker may still be true).
func DetectEnvKindOnce ¶ added in v0.25.0
func DetectEnvKindOnce() (EnvKind, EnvMarkers)
DetectEnvKindOnce runs the real detector against os.Environ / runtime.GOOS / os.Stdin exactly once per process lifetime and caches the result. Subsequent callers get the cached value. Safe for concurrent use.
func DetectEnvKindOnceWith ¶ added in v0.25.0
func DetectEnvKindOnceWith(detector func() (EnvKind, EnvMarkers)) (EnvKind, EnvMarkers)
DetectEnvKindOnceWith is the test-only entry point: it runs the given detector function at most once per reset cycle and caches the result. Production code should always use DetectEnvKindOnce. This helper exists so the concurrency test can inject a counting fake detector.
func EffectiveTelemetryEnabled ¶ added in v0.39.0
EffectiveTelemetryEnabled resolves whether telemetry is ACTUALLY active for cfg, honoring both the config bool (IsTelemetryEnabled — nil means enabled per MCP-2477) AND the startup env overrides (DO_NOT_TRACK / CI / MCPPROXY_TELEMETRY via IsDisabledByEnv). This must match the same effective resolution startup uses: telemetry that was never enabled (e.g. DO_NOT_TRACK=1 or CI=true) must never be considered "enabled", so disabling it produces no opt-out beacon.
func EnsureActivationBucket ¶ added in v0.25.0
ensureBucket ensures the activation bucket exists (useful for migration). Exported so callers that wire up storage can pre-create it to avoid races on first write. Safe to call multiple times.
func EnsureDiagnosticsCountersBucket ¶ added in v0.30.0
EnsureDiagnosticsCountersBucket pre-creates the bucket to avoid write-races on first use. Safe to call multiple times.
func EnsureFunnelBucket ¶ added in v0.47.0
EnsureFunnelBucket proactively creates the funnel bucket so concurrent first writes (index serve + heartbeat) never race on bucket creation.
func IsBuiltinTool ¶ added in v0.24.0
IsBuiltinTool reports whether the given tool name is in the fixed enum.
func IsValidEnvKind ¶ added in v0.25.0
IsValidEnvKind reports whether v is one of the canonical EnvKind constants.
func IsValidErrorCategory ¶ added in v0.24.0
func IsValidErrorCategory(c ErrorCategory) bool
IsValidErrorCategory reports whether the given category is in the fixed enum.
func IsValidLaunchSource ¶ added in v0.25.0
func IsValidLaunchSource(v LaunchSource) bool
IsValidLaunchSource reports whether v is one of the canonical LaunchSource constants.
func MaybePrintFirstRunNotice ¶ added in v0.24.0
MaybePrintFirstRunNotice prints the first-run telemetry notice to w if it has not been shown before. It mutates cfg.Telemetry.NoticeShown so the caller can persist the change. Returns true if the notice was printed.
Spec 042 User Story 10. Idempotent: subsequent calls with the same config are no-ops.
func PopulateBlockedValues ¶ added in v0.25.0
func PopulateBlockedValues()
PopulateBlockedValues (Spec 044 T025) scans the current process's OS-level identity (hostname, user home dir, sensitive env vars) and appends any non-empty literal to BlockedValues. Safe to call multiple times — only the first call has effect.
Inputs are read through function pointers so tests can inject fakes without reshelling os.Hostname / os.Getenv.
func RecordBuiltinToolOn ¶ added in v0.24.0
func RecordBuiltinToolOn(reg *CounterRegistry, name string)
RecordBuiltinToolOn calls reg.RecordBuiltinTool(name) if reg is non-nil.
func RecordErrorOn ¶ added in v0.24.0
func RecordErrorOn(reg *CounterRegistry, c ErrorCategory)
RecordErrorOn calls reg.RecordError(c) if reg is non-nil.
func RecordRESTRequestOn ¶ added in v0.24.0
func RecordRESTRequestOn(reg *CounterRegistry, method, template, statusClass string)
RecordRESTRequestOn calls reg.RecordRESTRequest(...) if reg is non-nil.
func RecordSurfaceOn ¶ added in v0.24.0
func RecordSurfaceOn(reg *CounterRegistry, s Surface)
RecordSurfaceOn calls reg.RecordSurface(s) if reg is non-nil.
func RecordUpstreamToolOn ¶ added in v0.24.0
func RecordUpstreamToolOn(reg *CounterRegistry)
RecordUpstreamToolOn calls reg.RecordUpstreamTool() if reg is non-nil.
func ResetEnvKindForTest ¶ added in v0.25.0
func ResetEnvKindForTest()
ResetEnvKindForTest resets the cached env_kind result so tests can rerun DetectEnvKindOnce with fresh inputs. MUST NOT be called from production code — calling it would re-detect on config reload and potentially re-classify a CI runner that set env vars late. Guard calls behind _test.go files only.
func ScanForPII ¶ added in v0.25.0
ScanForPII scans a serialized telemetry payload (v3+) for PII leaks and structural violations. Returns nil when the payload is clean; otherwise returns an *AnonymityViolation. The returned error satisfies errors.Is(err, ErrAnonymityViolation).
Rules (first-match wins):
- Any substring from AnonymityBlockedPrefixes appears in the payload.
- Any substring from BlockedValues appears in the payload.
- env_markers, if present, fails to unmarshal into a strict all-bool struct — meaning a field widened to a string/number/null.
- Any Spec 080 v7 field present breaks its documented shape: booleans (wizard_shown), non-negative integers (web_ui_opened, days_since_install, active_days_30d), or fixed enums (wizard_connect_step, previous_shutdown, last_error_code = MCPX_*).
The implementation never logs the payload — it only reports which rule tripped and the offending pattern (a small literal). Callers should log at error level and skip the heartbeat.
func SortedOAuthProviderTypes ¶ added in v0.24.0
SortedOAuthProviderTypes returns a sorted, deduplicated list. Helper used by feature_flags.go but defined here to avoid an extra file.
func TelemetryDisableTransition ¶ added in v0.39.0
TelemetryDisableTransition reports whether the resolved telemetry state moved from enabled to disabled between prior and next, using the SAME effective resolution as startup (config bool + env overrides). This is the single source of truth for the enabled->disabled flip, shared by the running daemon (REST / reload paths) and the `mcpproxy telemetry disable` CLI.
func ValidateCategory ¶
ValidateCategory checks if the feedback category is valid.
func ValidateMessage ¶
ValidateMessage checks if the feedback message meets length requirements.
Types ¶
type ActivationState ¶ added in v0.25.0
type ActivationState struct {
FirstConnectedServerEver bool `json:"first_connected_server_ever"`
FirstMCPClientEver bool `json:"first_mcp_client_ever"`
FirstRetrieveToolsCallEver bool `json:"first_retrieve_tools_call_ever"`
MCPClientsSeenEver []string `json:"mcp_clients_seen_ever"`
RetrieveToolsCalls24h int `json:"retrieve_tools_calls_24h"`
EstimatedTokensSaved24hBucket string `json:"estimated_tokens_saved_24h_bucket"`
ConfiguredIDECount int `json:"configured_ide_count"`
}
ActivationState is the in-memory / on-the-wire representation of the activation bucket. Instances are loaded with ActivationStore.Load and saved with ActivationStore.Save.
type ActivationStore ¶ added in v0.25.0
type ActivationStore interface {
// Load reads the full activation state. Missing bucket or keys yield
// zero values.
Load(db *bbolt.DB) (ActivationState, error)
// Save writes the full activation state, enforcing monotonic flags
// (true cannot revert to false).
Save(db *bbolt.DB, st ActivationState) error
// MarkFirstConnectedServer sets first_connected_server_ever=true if not
// already set. No-op if already true.
MarkFirstConnectedServer(db *bbolt.DB) error
// MarkFirstMCPClient sets first_mcp_client_ever=true if not already set.
MarkFirstMCPClient(db *bbolt.DB) error
// MarkFirstRetrieveToolsCall sets first_retrieve_tools_call_ever=true
// if not already set.
MarkFirstRetrieveToolsCall(db *bbolt.DB) error
// RecordMCPClient adds sanitized client name to the seen list. Dedups
// on insert; drops when cap (16) is reached.
RecordMCPClient(db *bbolt.DB, name string) error
// IncrementRetrieveToolsCall bumps the 24h window counter by 1,
// rolling the window if it has expired.
IncrementRetrieveToolsCall(db *bbolt.DB) error
// AddTokensSaved adds n to the 24h token-savings estimator counter.
AddTokensSaved(db *bbolt.DB, n int) error
// SetInstallerPending writes the installer_heartbeat_pending flag.
SetInstallerPending(db *bbolt.DB, v bool) error
// IsInstallerPending reports whether installer_heartbeat_pending is
// currently set.
IsInstallerPending(db *bbolt.DB) (bool, error)
}
ActivationStore is the persistence contract for the activation bucket. Implementations back onto a BBolt database; a fake is used in tests.
All methods are safe for concurrent use by the caller's discretion — the BBolt implementation uses transactional updates so individual method calls are atomic. Callers that need multi-step atomicity should serialize through a single goroutine.
func NewActivationStore ¶ added in v0.25.0
func NewActivationStore() ActivationStore
NewActivationStore returns a BBolt-backed ActivationStore.
type AnonymityViolation ¶ added in v0.25.0
type AnonymityViolation struct {
// Rule is a short stable identifier ("blocked_prefix", "blocked_value",
// "env_markers_non_bool") suitable for metrics labels.
Rule string
// Pattern is the literal substring or env-var name that matched.
Pattern string
// Reason is a human-readable summary (no payload contents).
Reason string
}
AnonymityViolation identifies which rule the payload tripped. The Pattern field is the offending substring (never the full payload: logging the whole payload would defeat the purpose of the scan).
func (*AnonymityViolation) Error ¶ added in v0.25.0
func (v *AnonymityViolation) Error() string
Error implements the error interface.
func (*AnonymityViolation) Is ¶ added in v0.25.0
func (v *AnonymityViolation) Is(target error) bool
Is allows AnonymityViolation to match ErrAnonymityViolation via errors.Is.
type AutostartReader ¶ added in v0.25.0
type AutostartReader struct {
// Path is the filesystem location of the tray-written sidecar. Tests
// override this; production uses DefaultAutostartReader.
Path string
// contains filtered or unexported fields
}
AutostartReader reads the tray-written autostart state. Instances are safe for concurrent use and cache the result for autostartCacheTTL. A nil return value means "state unknown" — this is expected on Linux (no tray today), when the tray is not running, or when the tray sidecar is absent / malformed.
func DefaultAutostartReader ¶ added in v0.25.0
func DefaultAutostartReader() *AutostartReader
DefaultAutostartReader returns the production reader targeting ~/.mcpproxy/tray-autostart.json. On Linux the sidecar is never written (by design), so the returned reader will simply always yield nil — the heartbeat's AutostartEnabled field stays JSON-null, matching data-model.md.
func (*AutostartReader) Read ¶ added in v0.25.0
func (r *AutostartReader) Read() *bool
Read returns the current autostart state (true=enabled, false=disabled, nil=unknown). Result is cached for autostartCacheTTL after the first successful read. Errors and missing files are logged nowhere (the caller — the telemetry service — already logs pipeline failures).
type CounterRegistry ¶ added in v0.24.0
type CounterRegistry struct {
// contains filtered or unexported fields
}
CounterRegistry aggregates Tier 2 telemetry counters in memory. All methods are safe for concurrent use. Counters are zeroed only by Reset(), which the telemetry service calls after a successful heartbeat send.
func NewCounterRegistry ¶ added in v0.24.0
func NewCounterRegistry() *CounterRegistry
NewCounterRegistry creates an empty registry. All counters start at zero.
func (*CounterRegistry) AnonymityViolationsTotal ¶ added in v0.25.0
func (r *CounterRegistry) AnonymityViolationsTotal() int64
AnonymityViolationsTotal returns the lifetime count of anonymity-scanner rejections. Not transmitted; for test + internal observability only.
func (*CounterRegistry) RecordAnonymityViolation ¶ added in v0.25.0
func (r *CounterRegistry) RecordAnonymityViolation()
RecordAnonymityViolation increments the anonymity-violation counter (Spec 044). Called by the telemetry service when ScanForPII rejects a payload.
func (*CounterRegistry) RecordBuiltinTool ¶ added in v0.24.0
func (r *CounterRegistry) RecordBuiltinTool(name string)
RecordBuiltinTool increments the counter for the named built-in tool. Unknown names (i.e., upstream tool names) are silently dropped.
func (*CounterRegistry) RecordDoctorRun ¶ added in v0.24.0
func (r *CounterRegistry) RecordDoctorRun(results []DoctorCheckResult)
RecordDoctorRun aggregates the structured doctor check results into the registry's doctor counter. Each result increments either Pass or Fail for its check name.
func (*CounterRegistry) RecordError ¶ added in v0.24.0
func (r *CounterRegistry) RecordError(c ErrorCategory)
RecordError increments the counter for the given error category. Unknown categories are silently dropped.
func (*CounterRegistry) RecordRESTRequest ¶ added in v0.24.0
func (r *CounterRegistry) RecordRESTRequest(method, template, statusClass string)
RecordRESTRequest increments the counter for the given route template and status class. Both inputs are expected to be from a fixed enum (Chi route template + "2xx"/"3xx"/"4xx"/"5xx") so no sanitization is needed here.
func (*CounterRegistry) RecordSurface ¶ added in v0.24.0
func (r *CounterRegistry) RecordSurface(s Surface)
RecordSurface increments the counter for the given surface.
func (*CounterRegistry) RecordUpstreamTool ¶ added in v0.24.0
func (r *CounterRegistry) RecordUpstreamTool()
RecordUpstreamTool increments the upstream tool call counter. The tool name itself is intentionally not accepted: only an aggregate count is recorded.
func (*CounterRegistry) Reset ¶ added in v0.24.0
func (r *CounterRegistry) Reset()
Reset zeros all counters. Called only after a successful heartbeat send.
func (*CounterRegistry) Snapshot ¶ added in v0.24.0
func (r *CounterRegistry) Snapshot() RegistrySnapshot
Snapshot returns an immutable view of all counters. The registry is NOT reset; call Reset() after a successful flush.
type DiagnosticsCounterStore ¶ added in v0.30.0
type DiagnosticsCounterStore interface {
// RecordErrorCode increments the 24h sliding counter for the given MCPX_
// code and adds it to the unique_codes_ever set (idempotent on second add).
// Only values that match the MCPX_ prefix are accepted; others are silently
// dropped to prevent free-text from leaking into telemetry.
RecordErrorCode(db *bbolt.DB, code string) error
// RecordFixAttempt increments fix_attempted_24h. If outcome == "success",
// also increments fix_succeeded_24h. Unknown outcomes are counted as
// attempted-only (graceful: new outcome strings from future code won't panic).
RecordFixAttempt(db *bbolt.DB, outcome string) error
// Snapshot loads the current counter state, applying 24h decay at now.
Snapshot(db *bbolt.DB) (DiagnosticsCounters, error)
}
DiagnosticsCounterStore is the persistence contract for Phase H counters. Implementations back onto BBolt; a fake is used in tests. All methods are individually atomic via bbolt transactions.
func NewDiagnosticsCounterStore ¶ added in v0.30.0
func NewDiagnosticsCounterStore() DiagnosticsCounterStore
NewDiagnosticsCounterStore returns a BBolt-backed DiagnosticsCounterStore.
type DiagnosticsCounters ¶ added in v0.30.0
type DiagnosticsCounters struct {
// ErrorCodeCounts24h maps stable MCPX_ code strings to 24h occurrence counts.
// Safe: only MCPX_* enum constants are stored here, never free text, paths,
// server names, or user-entered values.
ErrorCodeCounts24h map[string]int `json:"error_code_counts_24h,omitempty"`
// FixAttempted24h counts POST /api/v1/diagnostics/fix calls in the last 24h.
FixAttempted24h int `json:"fix_attempted_24h"`
// FixSucceeded24h counts fix invocations with outcome="success" in the last 24h.
FixSucceeded24h int `json:"fix_succeeded_24h"`
// UniqueCodesEver is the cardinality of the all-time error code set.
// Bounded by the MCPX_ catalog size (~30 codes), never approaches PII risk.
UniqueCodesEver int `json:"unique_codes_ever"`
}
DiagnosticsCounters holds the Phase H counter snapshot. MarshalJSON caps ErrorCodeCounts24h to the top-20 entries (by count desc, code asc on ties) so the wire payload stays bounded regardless of MCPX_ catalog growth.
func (DiagnosticsCounters) MarshalJSON ¶ added in v0.30.0
func (d DiagnosticsCounters) MarshalJSON() ([]byte, error)
MarshalJSON caps ErrorCodeCounts24h to top-20 entries before serialising.
type DoctorCheckResult ¶ added in v0.24.0
DoctorCheckResult is the minimal interface the registry needs to record a doctor check outcome. It is satisfied by internal/doctor.CheckResult without importing that package (avoiding an import cycle).
type DoctorCounts ¶ added in v0.24.0
DoctorCounts holds pass/fail counts for a single doctor check.
type EnvDisabledReason ¶ added in v0.24.0
type EnvDisabledReason string
EnvDisabledReason explains why telemetry was disabled by an environment variable, if any. The empty string means env did not disable telemetry.
const ( EnvDisabledNone EnvDisabledReason = "" EnvDisabledByDoNotTrack EnvDisabledReason = "DO_NOT_TRACK" EnvDisabledByCI EnvDisabledReason = "CI" EnvDisabledByMCPProxy EnvDisabledReason = "MCPPROXY_TELEMETRY=false" )
func IsDisabledByEnv ¶ added in v0.24.0
func IsDisabledByEnv() (bool, EnvDisabledReason)
IsDisabledByEnv evaluates the env var precedence chain for telemetry disablement. Precedence (highest first):
- DO_NOT_TRACK set to any non-empty, non-"0" value (consoledonottrack.com)
- CI=true or CI=1
- MCPPROXY_TELEMETRY=false
Returns true and the reason if telemetry should be disabled.
type EnvKind ¶ added in v0.25.0
type EnvKind string
EnvKind classifies the process's runtime environment for retention telemetry (spec 044). The value is computed once at process startup from environment variables and filesystem probes, then cached for the lifetime of the process via DetectEnvKindOnce.
Serialization: always lowercase string. An invalid / empty value indicates a programming error; the payload builder will reject it.
const ( // EnvKindInteractive: real human on a desktop OS (macOS/Windows) or // Linux with DISPLAY/TTY present. EnvKindInteractive EnvKind = "interactive" // EnvKindCI: any known CI runner env var set (GitHub Actions, GitLab CI, // Jenkins, CircleCI, etc.). EnvKindCI EnvKind = "ci" // EnvKindCloudIDE: Codespaces, Gitpod, Replit, StackBlitz, Daytona, Coder. EnvKindCloudIDE EnvKind = "cloud_ide" // EnvKindContainer: /.dockerenv or /run/.containerenv present, or // $container env var set — with no CI/cloud-IDE markers. EnvKindContainer EnvKind = "container" // EnvKindHeadless: Linux with no DISPLAY/TTY and none of the above. EnvKindHeadless EnvKind = "headless" // EnvKindUnknown: classifier fell through all rules. EnvKindUnknown EnvKind = "unknown" )
func AllEnvKinds ¶ added in v0.25.0
func AllEnvKinds() []EnvKind
AllEnvKinds returns the canonical ordered list of EnvKind values. Used by tests for exhaustiveness checks and by the payload builder for validation.
type EnvMarkers ¶ added in v0.25.0
type EnvMarkers struct {
// HasCIEnv is true when any known CI env var is set (e.g., CI=true,
// GITHUB_ACTIONS, GITLAB_CI, JENKINS_URL, CIRCLECI).
HasCIEnv bool `json:"has_ci_env"`
// HasCloudIDEEnv is true when a cloud-IDE env var is set (e.g.,
// CODESPACES, GITPOD_WORKSPACE_ID, REPL_ID, STACKBLITZ_WORKER,
// DAYTONA_WORKSPACE_ID, CODER_AGENT_TOKEN).
HasCloudIDEEnv bool `json:"has_cloud_ide_env"`
// IsContainer is true when /.dockerenv or /run/.containerenv exists, or
// the $container env var is set.
IsContainer bool `json:"is_container"`
// HasTTY is true when stdin is attached to a terminal.
HasTTY bool `json:"has_tty"`
// HasDisplay is true when DISPLAY or WAYLAND_DISPLAY env var is set
// (Linux desktop session indicator).
HasDisplay bool `json:"has_display"`
}
EnvMarkers carries the raw boolean observations feeding the EnvKind decision tree (spec 044). All fields MUST remain Go bool values: the anonymity scanner re-asserts this on the serialized JSON to prevent a future refactor from accidentally widening any of these to a string or number.
Only booleans are allowed here. Do NOT add fields that carry env-var values, hostnames, usernames, or paths — those are PII by policy.
type ErrorCategory ¶ added in v0.24.0
type ErrorCategory string
ErrorCategory is a typed enum of error categories that telemetry will count. Only values defined here may be recorded; unknown categories are silently dropped by RecordError to prevent free-text error messages from leaking into telemetry.
const ( ErrCatOAuthRefreshFailed ErrorCategory = "oauth_refresh_failed" ErrCatOAuthTokenExpired ErrorCategory = "oauth_token_expired" ErrCatUpstreamConnectTimeout ErrorCategory = "upstream_connect_timeout" ErrCatUpstreamConnectRefused ErrorCategory = "upstream_connect_refused" ErrCatUpstreamHandshakeFailed ErrorCategory = "upstream_handshake_failed" ErrCatToolQuarantineBlocked ErrorCategory = "tool_quarantine_blocked" ErrCatDockerPullFailed ErrorCategory = "docker_pull_failed" ErrCatDockerRunFailed ErrorCategory = "docker_run_failed" ErrCatIndexRebuildFailed ErrorCategory = "index_rebuild_failed" ErrCatConfigReloadFailed ErrorCategory = "config_reload_failed" ErrCatSocketBindFailed ErrorCategory = "socket_bind_failed" )
type FeatureFlagSnapshot ¶ added in v0.24.0
type FeatureFlagSnapshot struct {
EnableSocket bool `json:"enable_socket"`
EnableWebUI bool `json:"enable_web_ui"`
EnablePrompts bool `json:"enable_prompts"`
RequireMCPAuth bool `json:"require_mcp_auth"`
EnableCodeExecution bool `json:"enable_code_execution"`
QuarantineEnabled bool `json:"quarantine_enabled"`
SensitiveDataDetectionEnabled bool `json:"sensitive_data_detection_enabled"`
OAuthProviderTypes []string `json:"oauth_provider_types"`
// Schema v3: DockerAvailable reports whether the host has a reachable
// Docker daemon, as observed by the runtime's checkDockerDaemon probe.
// Populated by the telemetry service at heartbeat time (not by
// BuildFeatureFlagSnapshot) so the snapshot helper stays side-effect-free
// and doesn't shell out to `docker info`.
DockerAvailable bool `json:"docker_available"`
// Schema v5 (MCP-2745): DockerIsolationEnabled is the global
// docker_isolation.enabled flag. Set by BuildFeatureFlagSnapshot (pure,
// config-only). Together with DockerCLISource it lets the dashboard tell
// "isolation on, 0 matching servers" apart from "isolation off".
DockerIsolationEnabled bool `json:"docker_isolation_enabled"`
// Schema v5 (MCP-2745): DockerCLISource is the coarse, fixed-enum branch
// that resolved the `docker` CLI — one of "path" | "bundled" |
// "login_shell" | "absent". This is the direct #696 fleet signal (docker
// installed but not on the spawn PATH). NEVER the path string itself.
// Populated by the telemetry service at heartbeat time (the resolution is a
// runtime concern) — mirrors DockerAvailable.
DockerCLISource string `json:"docker_cli_source,omitempty"`
}
FeatureFlagSnapshot captures the boolean / enum feature flags reported in the daily heartbeat. Spec 042 User Story 4.
func BuildFeatureFlagSnapshot ¶ added in v0.24.0
func BuildFeatureFlagSnapshot(cfg *config.Config) *FeatureFlagSnapshot
BuildFeatureFlagSnapshot returns a snapshot of the current feature flag state. It records boolean flags and a sorted, deduplicated list of OAuth provider TYPES (not URLs, client IDs, or tenant identifiers). The empty list is returned if no upstream servers have OAuth configured.
type FeedbackContext ¶
type FeedbackContext struct {
Version string `json:"version"`
Edition string `json:"edition"`
OS string `json:"os"`
Arch string `json:"arch"`
ServerCount int `json:"server_count"`
ConnectedServerCount int `json:"connected_server_count"`
RoutingMode string `json:"routing_mode"`
}
FeedbackContext provides automatic system context alongside feedback.
type FeedbackRequest ¶
type FeedbackRequest struct {
Category string `json:"category"` // bug, feature, other
Message string `json:"message"`
Email string `json:"email,omitempty"`
Context FeedbackContext `json:"context"`
}
FeedbackRequest is the user-submitted feedback payload.
type FeedbackResponse ¶
type FeedbackResponse struct {
Success bool `json:"success"`
IssueURL string `json:"issue_url,omitempty"`
Error string `json:"error,omitempty"`
}
FeedbackResponse is the response from the telemetry backend.
type FileProber ¶ added in v0.25.0
type FileProber interface {
// Exists returns true if the file at path exists (any type: file, dir,
// symlink). Errors other than "not exists" are treated as "exists" to
// avoid mis-classifying a container due to permission issues.
Exists(path string) bool
}
FileProber abstracts filesystem existence checks so the env_kind detector can be unit-tested with a fake FS. Production uses defaultFileProber.
type FunnelState ¶ added in v0.47.0
type FunnelState struct {
// WebUIOpened is the lifetime count of embedded Web UI index serves.
WebUIOpened int64
// DaysSinceInstall is the non-negative whole-day age of the install,
// clamped at 0 on clock skew. Only meaningful when HasInstallDay is true.
DaysSinceInstall int
// HasInstallDay reports whether a first-install stamp exists yet.
HasInstallDay bool
// ActiveDays30d is the number of distinct active UTC days in the
// trailing 30-day window (0..30; 0 only before any activity is recorded).
ActiveDays30d int
}
FunnelState is the read-side snapshot of the funnel bucket, already reduced to the payload-safe integers (FR-006..FR-008). The per-day set itself is intentionally not part of this struct.
type FunnelStore ¶ added in v0.47.0
type FunnelStore interface {
// IncrementWebUIOpened bumps the lifetime index-serve counter by 1.
IncrementWebUIOpened(db *bbolt.DB) error
// RecordActivity stamps the first-install day if absent and adds now's
// UTC day to the active-day set, pruning members that have aged out of
// the trailing window. Out-of-order days are tolerated: a day is a set
// member, not a delta.
RecordActivity(db *bbolt.DB, now time.Time) error
// Snapshot reads the payload-safe reduction of the bucket at `now`.
// Aging is applied at read time as well, so a stale on-disk set never
// inflates the surfaced count. A missing bucket yields a zero state.
Snapshot(db *bbolt.DB, now time.Time) (FunnelState, error)
}
FunnelStore is the persistence contract for the funnel bucket. The BBolt implementation uses transactional updates so individual calls are atomic.
func NewFunnelStore ¶ added in v0.47.0
func NewFunnelStore() FunnelStore
NewFunnelStore returns a BBolt-backed FunnelStore.
type HandshakeChecker ¶ added in v0.25.0
type HandshakeChecker interface {
LaunchedViaTray() bool
}
HandshakeChecker abstracts the tray-socket handshake that signals launched_via=tray. Tests inject a constant answer; production wiring is a no-op (defaultHandshakeChecker) until tray→core handshake is added.
type HeartbeatPayload ¶
type HeartbeatPayload struct {
// v1 fields (preserved unchanged)
AnonymousID string `json:"anonymous_id"`
// MachineID (schema v6) is a stable, non-reversible hash of the OS machine
// id — HMAC-SHA256 keyed by the OS machine id, scoped by an app-specific key
// (see machine_id.go). Unlike anonymous_id (a UUID persisted in the config
// file, which is regenerated on every run in ephemeral environments —
// throwaway HOMEs, Docker layers, CI), this value is stable per physical
// machine, letting the dashboard dedup ephemeral installs. Empty/omitted
// when the OS machine id cannot be read (containers without /etc/machine-id,
// permission errors, exotic platforms); the backend treats empty as
// "unknown". The raw machine id is NEVER transmitted — only the salted hash.
// It rides the same opt-out gate as every other field (the whole heartbeat
// is suppressed when telemetry is disabled).
MachineID string `json:"machine_id,omitempty"`
Version string `json:"version"`
Edition string `json:"edition"`
OS string `json:"os"`
Arch string `json:"arch"`
GoVersion string `json:"go_version"`
ServerCount int `json:"server_count"`
ConnectedServerCount int `json:"connected_server_count"`
ToolCount int `json:"tool_count"`
UptimeHours int `json:"uptime_hours"`
RoutingMode string `json:"routing_mode"`
QuarantineEnabled bool `json:"quarantine_enabled"`
Timestamp string `json:"timestamp"`
// Spec 042 (Tier 2) additions
SchemaVersion int `json:"schema_version,omitempty"`
AnonymousIDCreatedAt string `json:"anonymous_id_created_at,omitempty"`
CurrentVersion string `json:"current_version,omitempty"`
PreviousVersion string `json:"previous_version"`
LastStartupOutcome string `json:"last_startup_outcome,omitempty"`
SurfaceRequests map[string]int64 `json:"surface_requests,omitempty"`
BuiltinToolCalls map[string]int64 `json:"builtin_tool_calls,omitempty"`
UpstreamToolCallCountBucket string `json:"upstream_tool_call_count_bucket,omitempty"`
RESTEndpointCalls map[string]map[string]int64 `json:"rest_endpoint_calls,omitempty"`
FeatureFlags *FeatureFlagSnapshot `json:"feature_flags,omitempty"`
ErrorCategoryCounts map[string]int64 `json:"error_category_counts,omitempty"`
DoctorChecks map[string]DoctorCounts `json:"doctor_checks,omitempty"`
// Schema v3 additions.
// ServerProtocolCounts is a fixed-enum histogram over cfg.Servers by
// Protocol. Keys are exactly: stdio, http, sse, streamable_http, auto.
// Never contains server names, URLs, or unknown values (unknown/empty
// protocols bucket into "auto").
ServerProtocolCounts map[string]int `json:"server_protocol_counts,omitempty"`
// ServerDockerIsolatedCount is the number of configured servers the
// runtime actually wraps in Docker isolation. Distinct from "has Docker
// available" — an install can have Docker but never use it for isolation.
ServerDockerIsolatedCount int `json:"server_docker_isolated_count,omitempty"`
// Spec 044 additions. schema_version stays at 3 (set by spec 042); these
// fields are additive and forward-compatible.
//
// EnvKind: ground-truth classification of the process environment, computed
// once at startup by DetectEnvKindOnce. One of the EnvKind* constants.
EnvKind string `json:"env_kind,omitempty"`
// EnvMarkers: raw boolean observations feeding EnvKind. Fields are ALL
// booleans — the anonymity scanner re-asserts this on the serialized form.
EnvMarkers *EnvMarkers `json:"env_markers,omitempty"`
// Activation is the retention funnel snapshot: monotonic first-ever flags,
// 24h sliding counters, and bucketed token-savings estimate. Loaded from
// the BBolt activation bucket at heartbeat build time. nil when the store
// is not wired (e.g. in short-lived CLI commands).
Activation *ActivationState `json:"activation,omitempty"`
// LaunchSource: how the process was launched. One of "installer", "tray",
// "login_item", "cli", "unknown". Detected once at startup via
// DetectLaunchSourceOnce, with a one-shot "installer" override driven by
// the installer_heartbeat_pending BBolt flag (cleared after first heartbeat).
LaunchSource string `json:"launch_source,omitempty"`
// AutostartEnabled: tri-state login-item status.
// - *true : tray reports the app IS registered as a login item
// - *false : tray reports the app is NOT registered
// - nil : unknown (tray not running, Linux, or sidecar absent)
// Pointer intentional so JSON null is distinguishable from false —
// receivers need this to separate "user disabled" from "we don't know".
AutostartEnabled *bool `json:"autostart_enabled"`
// ConnectedClientCount is the number of supported AI clients in which
// mcpproxy is currently registered (i.e. has the URL/auth in their
// native config file). Computed by connect.Service from the existing
// per-client adapter table.
ConnectedClientCount int `json:"connected_client_count,omitempty"`
// ConnectedClientIDs are the identifiers of supported clients currently
// pointing at mcpproxy. Drawn ONLY from the fixed adapter table
// (e.g. "claude-code", "cursor", "vscode", "windsurf", "codex",
// "gemini"). User-entered values, paths, and arbitrary strings MUST
// NEVER appear in this field — the anonymity scanner asserts this.
ConnectedClientIDs []string `json:"connected_client_ids,omitempty"`
// WizardEngaged is true once the user completed or explicitly skipped
// the first-run onboarding wizard. Once true, the wizard does not
// auto-show again, even if state regresses.
WizardEngaged bool `json:"wizard_engaged,omitempty"`
// WizardConnectStep is the per-step status for "Connect an AI client":
// one of "" (not shown to this install), "completed",
// "completed_external" (Spec 080: connected outside the wizard —
// CLI/ConnectModal/manual config — detected at dismissal), or "skipped".
// Consumers switching on completed|skipped must treat unknown values as
// "other/engaged".
WizardConnectStep string `json:"wizard_connect_step,omitempty"`
// WizardServerStep is the per-step status for "Add an MCP server":
// one of "" (not shown to this install), "completed", or "skipped".
WizardServerStep string `json:"wizard_server_step,omitempty"`
// WizardShown is true once the onboarding wizard has rendered at least
// once for this install (OnboardingState.FirstShownAt set). Combined
// with WizardEngaged it makes "shown but ignored" observable:
// wizard_shown=true with wizard_engaged absent/false.
WizardShown bool `json:"wizard_shown,omitempty"`
// WebUIOpened is the lifetime count of embedded Web UI index-document
// serves (the UI entrypoint), persisted in BBolt. Independent of the
// X-MCPProxy-Client-header-based surface_requests.webui counter: it
// counts opening the UI, not SPA API traffic. Asset and API requests
// never increment it. Coarse by design (health checkers fetching /
// count too); documented as "index serves".
WebUIOpened int64 `json:"web_ui_opened,omitempty"`
// DaysSinceInstall is the whole-day UTC age of the install, from a
// persisted first-install day stamp independent of anonymous_id.
// Non-negative (clamped at 0 on clock skew). Pointer so day 0 (install
// day) is transmitted while "store not wired" is omitted — the same
// nil-safety as Activation. No install timestamp is ever transmitted.
DaysSinceInstall *int `json:"days_since_install,omitempty"`
// ActiveDays30d is the number of distinct active UTC days in the
// trailing 30-day window (1..30 once any activity is recorded). Only
// this cardinality leaves the machine; the per-day set stays local.
ActiveDays30d int `json:"active_days_30d,omitempty"`
// PreviousShutdown reports how the PREVIOUS process instance ended:
// "clean" (graceful-shutdown path resolved the persisted marker) or
// "crash" (marker armed at startup but never resolved — SIGKILL, panic,
// power loss). Absent on a first-ever run (no prior marker) or when the
// store is not wired — a fresh install is never misreported as a crash
// (FR-010/FR-013). Computed once at startup and stable across all
// heartbeats of the instance (FR-011).
PreviousShutdown string `json:"previous_shutdown,omitempty"`
// LastErrorCode is the most recently observed stable MCPX_* diagnostic
// code (same fixed code set as diagnostics.error_code_counts_24h),
// persisted across restarts so the post-crash heartbeat carries the
// pre-crash code. Enum code only — never message text, stack traces,
// server names, or paths. Absent when no error was ever recorded
// (FR-012).
LastErrorCode string `json:"last_error_code,omitempty"`
// Spec 044 Phase H: diagnostics counter snapshot. Omitted entirely when
// all counters are zero (omitempty on the pointer). No PII: only stable
// MCPX_* enum strings, non-negative int counts.
Diagnostics *DiagnosticsCounters `json:"diagnostics,omitempty"`
}
HeartbeatPayload is the anonymous telemetry payload sent periodically. Spec 042 expanded the payload with Tier 2 fields; v1 fields are preserved.
type LaunchSource ¶ added in v0.25.0
type LaunchSource string
LaunchSource identifies how the current mcpproxy process was launched, for retention telemetry (spec 044). Detection happens once at process startup (via DetectLaunchSourceOnce, added in a later task), with a one-shot "installer" override driven by the installer_heartbeat_pending BBolt flag.
Serialization: always lowercase string. The payload builder rejects values outside the canonical set.
const ( // LaunchSourceInstaller: MCPPROXY_LAUNCHED_BY=installer env var at // startup. One-shot: cleared after first heartbeat. LaunchSourceInstaller LaunchSource = "installer" // LaunchSourceTray: the tray socket handshake signalled launched_via=tray. LaunchSourceTray LaunchSource = "tray" // LaunchSourceLoginItem: OS launched the process as a registered login // item (parent = launchd on macOS, explorer.exe on Windows via the Run // registry key). LaunchSourceLoginItem LaunchSource = "login_item" // LaunchSourceCLI: stdin is a TTY and no other rule matched. LaunchSourceCLI LaunchSource = "cli" // LaunchSourceUnknown: none of the above. LaunchSourceUnknown LaunchSource = "unknown" )
func AllLaunchSources ¶ added in v0.25.0
func AllLaunchSources() []LaunchSource
AllLaunchSources returns the canonical ordered list of LaunchSource values.
func DetectLaunchSource ¶ added in v0.25.0
func DetectLaunchSource(env map[string]string, handshake HandshakeChecker, ppid PPIDChecker, tty TTYChecker) LaunchSource
DetectLaunchSource is the pure classifier implementing the precedence rules from research.md R3:
- MCPPROXY_LAUNCHED_BY=installer env var → installer
- handshake.LaunchedViaTray() → tray
- ppid.IsLoginItemParent() → login_item
- tty.IsTerminal() → cli
- fallthrough → unknown
All parameters are nil-safe: a nil HandshakeChecker / PPIDChecker / TTYChecker simply contributes "false" to its respective branch.
func DetectLaunchSourceOnce ¶ added in v0.25.0
func DetectLaunchSourceOnce() LaunchSource
DetectLaunchSourceOnce classifies the current process's launch source exactly once per process lifetime and caches the result.
type OnboardingSnapshot ¶ added in v0.29.0
type OnboardingSnapshot struct {
ConnectedClientCount int
ConnectedClientIDs []string
WizardEngaged bool
WizardConnectStep string
WizardServerStep string
// WizardShown (Spec 080 US2): the wizard rendered at least once for
// this install — derived from OnboardingState.FirstShownAt != nil.
WizardShown bool
}
OnboardingSnapshot is the data the telemetry service needs to populate Spec 046 fields on each heartbeat. Built fresh per heartbeat so changes (e.g. user connects another client between heartbeats) are reflected.
type OptOutBeacon ¶ added in v0.39.0
OptOutBeacon is the minimal payload sent exactly once when a user disables telemetry. It carries ONLY the anonymous install ID (for unique opt-out counting / dedup) and the distinguishing event marker. It deliberately omits every usage field — sending data after an opt-out is only defensible because this payload contains nothing but the dedup ID.
type PPIDChecker ¶ added in v0.25.0
type PPIDChecker interface {
IsLoginItemParent() bool
}
PPIDChecker abstracts the OS-specific "is my parent a login-item launcher?" check. On macOS the production impl verifies parent process name == "launchd"; on Windows parent == "explorer.exe"; otherwise false.
type PreChurnStore ¶ added in v0.47.0
type PreChurnStore interface {
// ArmShutdownMarker derives the previous instance's shutdown outcome from
// the persisted marker — PreviousShutdownClean (marker resolved),
// PreviousShutdownCrash (marker armed but unresolved), or
// PreviousShutdownUnknown (no marker; first-ever run) — and re-arms the
// marker for the current instance in the same transaction. Call once,
// early in startup, so crash loops are visible (FR-010 edge case).
ArmShutdownMarker(db *bbolt.DB) (string, error)
// ResolveCleanShutdown marks the current instance's shutdown as graceful.
// Called from the runtime shutdown path while the DB is still open.
// Idempotent.
ResolveCleanShutdown(db *bbolt.DB) error
// RecordLastErrorCode persists code as the most recent diagnostic code,
// overwriting any prior value. Only codes registered in the diagnostics
// catalog (the fixed set behind error_code_counts_24h) are accepted;
// anything else is silently dropped (FR-012).
RecordLastErrorCode(db *bbolt.DB, code string) error
// LastErrorCode returns the most recently recorded MCPX_* code, or ""
// when none was ever recorded. Values are re-validated at read time so a
// corrupt on-disk value is never surfaced.
LastErrorCode(db *bbolt.DB) (string, error)
}
PreChurnStore is the persistence contract for the pre-churn snapshot (Spec 080 US3). The BBolt implementation uses transactional updates so individual calls are atomic. Single-writer safety comes from the BBolt file lock: a second process instance fails to open the DB (exit code 3) and therefore can never touch the marker (FR-013).
func NewPreChurnStore ¶ added in v0.47.0
func NewPreChurnStore() PreChurnStore
NewPreChurnStore returns a BBolt-backed PreChurnStore.
type RateLimiter ¶
type RateLimiter struct {
// contains filtered or unexported fields
}
RateLimiter enforces a maximum number of requests per hour.
func NewRateLimiter ¶
func NewRateLimiter(maxPerHour int) *RateLimiter
NewRateLimiter creates a rate limiter with the given max requests per hour.
func (*RateLimiter) Allow ¶
func (rl *RateLimiter) Allow() bool
Allow returns true if the request is within the rate limit.
type RegistrySnapshot ¶ added in v0.24.0
type RegistrySnapshot struct {
SurfaceCounts map[string]int64 `json:"surface_requests"`
BuiltinToolCalls map[string]int64 `json:"builtin_tool_calls"`
UpstreamToolCallCountBucket string `json:"upstream_tool_call_count_bucket"`
RESTEndpointCalls map[string]map[string]int64 `json:"rest_endpoint_calls"`
ErrorCategoryCounts map[string]int64 `json:"error_category_counts"`
DoctorChecks map[string]DoctorCounts `json:"doctor_checks"`
}
RegistrySnapshot is an immutable view of the registry built by Snapshot(). It is safe to mutate the maps in a snapshot — they are copies.
type RuntimeStats ¶
type RuntimeStats interface {
GetServerCount() int
GetConnectedServerCount() int
GetToolCount() int
GetRoutingMode() string
IsQuarantineEnabled() bool
// Schema v3 additions.
// IsDockerAvailable reports whether the host has a reachable Docker
// daemon. Implementations should memoize the probe result (running
// `docker info` on every heartbeat has cost) and return the cached value.
IsDockerAvailable() bool
// GetDockerIsolatedServerCount returns how many currently-configured
// servers the runtime is actually wrapping in a Docker container.
GetDockerIsolatedServerCount() int
// GetDockerCLISource returns the coarse, fixed-enum branch that resolved
// the docker CLI — "path" | "bundled" | "login_shell" | "absent" (schema
// v5, MCP-2745). Implementations should memoize the resolution (it shares
// the shellwrap docker-path cache) and NEVER return the path string.
GetDockerCLISource() string
}
RuntimeStats is an interface to decouple from the runtime package.
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
Service manages anonymous telemetry heartbeats and feedback submission.
func (*Service) ActivationDB ¶ added in v0.25.0
ActivationDB returns the BBolt DB handle associated with the activation store (or nil). Callers pair this with ActivationStore() to perform writes.
func (*Service) ActivationStore ¶ added in v0.25.0
func (s *Service) ActivationStore() ActivationStore
ActivationStore returns the wired store (or nil). Used by MCP and runtime integration points that need to increment counters.
func (*Service) BuildPayload ¶ added in v0.24.0
func (s *Service) BuildPayload() HeartbeatPayload
BuildPayload renders the heartbeat payload at the current point in time. It is exported so the `mcpproxy telemetry show-payload` command can render the same payload that would next be sent, without making a network call.
func (*Service) DiagnosticsCounterDB ¶ added in v0.30.0
DiagnosticsCounterDB returns the BBolt DB handle associated with the diagnostics counter store (or nil).
func (*Service) DiagnosticsCounterStore ¶ added in v0.30.0
func (s *Service) DiagnosticsCounterStore() DiagnosticsCounterStore
DiagnosticsCounterStore returns the wired counter store (or nil).
func (*Service) EmitOptOutBeacon ¶ added in v0.39.0
EmitOptOutBeacon applies the shared eligibility guards and, if eligible, sends the opt-out beacon synchronously. It is the single guarded send entry point used by BOTH the daemon (fire-and-forget in a goroutine from NotifyConfigChanged) and the `mcpproxy telemetry disable` CLI — so neither duplicates the send nor bypasses a guard.
Guards (all must hold, mirroring the heartbeat eligibility):
- valid semver build (dev builds never emit telemetry),
- telemetry not disabled by environment (DO_NOT_TRACK / CI / MCPPROXY_TELEMETRY),
- an anonymous install ID exists (nothing to dedup on otherwise).
Returns true if a send was attempted (regardless of HTTP outcome). Best-effort: callers disable telemetry whether or not this succeeds.
func (*Service) EnvDisabledReason ¶ added in v0.24.0
func (s *Service) EnvDisabledReason() EnvDisabledReason
EnvDisabledReason returns the env-var reason telemetry is disabled, if any.
func (*Service) FunnelDB ¶ added in v0.47.0
FunnelDB returns the BBolt DB handle associated with the funnel store (or nil).
func (*Service) FunnelStore ¶ added in v0.47.0
func (s *Service) FunnelStore() FunnelStore
FunnelStore returns the wired funnel store (or nil).
func (*Service) NotifyConfigChanged ¶ added in v0.39.0
NotifyConfigChanged informs the telemetry service that the live configuration has been swapped. It is the single server-side hook for the opt-out beacon: the running daemon calls it from every config-write path (REST apply, disk reload) so web UI, macOS app, and CLI-driven changes are all covered by one implementation.
On a resolved enabled->disabled transition it:
- immediately marks telemetry opted-out (no further heartbeats are sent),
- fires exactly one fire-and-forget opt-out beacon with a short timeout.
The send is best-effort: a failure does not re-enable telemetry. On a disabled->enabled transition it clears the opt-out latch so a later disable flip emits its own beacon (exactly once per flip). It never blocks the caller.
func (*Service) RecordWebUIOpen ¶ added in v0.47.0
func (s *Service) RecordWebUIOpen()
RecordWebUIOpen increments the lifetime web_ui_opened counter (Spec 080 FR-006). Called by the embedded Web UI handler whenever the index document is served. nil-safe: a no-op when the funnel store is not wired, and a persistence error never propagates to the HTTP path (logged at debug).
func (*Service) Registry ¶ added in v0.24.0
func (s *Service) Registry() *CounterRegistry
Registry returns the counter registry for Tier 2 telemetry events. Always non-nil after New, even if telemetry is disabled — that way callers can always Record* without nil checks; the data simply never leaves the process.
func (*Service) SendOptOutBeacon ¶ added in v0.39.0
SendOptOutBeacon posts a single opt-out beacon to the configured telemetry endpoint, reusing the existing /heartbeat ingest path. It is best-effort: callers MUST disable telemetry regardless of the returned error, and supply a context with a short timeout so the send never blocks a config save.
The destination is taken from the service's own resolved endpoint/config (the exact indirection the heartbeat and feedback senders use) rather than a caller-supplied URL, so this never sends to an arbitrary, request-derived host.
func (*Service) SetActivationStore ¶ added in v0.25.0
func (s *Service) SetActivationStore(store ActivationStore, db *bbolt.DB)
SetActivationStore wires the BBolt-backed activation store and the shared DB handle (Spec 044). Optional; when unset, heartbeat payloads omit the activation object entirely. Safe to call once during startup.
func (*Service) SetAutostartReader ¶ added in v0.25.0
func (s *Service) SetAutostartReader(r *AutostartReader)
SetAutostartReader overrides the default autostart reader (for tests). In production the first heartbeat lazy-initializes DefaultAutostartReader.
func (*Service) SetConfiguredIDECountProvider ¶ added in v0.25.0
SetConfiguredIDECountProvider wires a function that returns the number of IDE client config files mcpproxy has registered itself into (Spec 044). Typically supplied by internal/connect.Service.
func (*Service) SetDiagnosticsCounterStore ¶ added in v0.30.0
func (s *Service) SetDiagnosticsCounterStore(store DiagnosticsCounterStore, db *bbolt.DB)
SetDiagnosticsCounterStore wires the BBolt-backed diagnostics counter store (Spec 044 Phase H). Optional; when unset, heartbeat payloads omit the diagnostics object. Safe to call once during startup.
func (*Service) SetFunnelStore ¶ added in v0.47.0
func (s *Service) SetFunnelStore(store FunnelStore, db *bbolt.DB)
SetFunnelStore wires the BBolt-backed funnel observability store (Spec 080 US2). Optional; when unset, heartbeat payloads omit web_ui_opened, days_since_install, and active_days_30d. Safe to call once during startup.
func (*Service) SetOnboardingProvider ¶ added in v0.29.0
func (s *Service) SetOnboardingProvider(fn func() *OnboardingSnapshot)
SetOnboardingProvider wires a function that returns an onboarding-funnel snapshot for the next heartbeat (Spec 046). Each call should return fresh data — connected-client count and wizard engagement state both change between heartbeats. Returning nil omits the onboarding fields entirely.
func (*Service) SetPreChurn ¶ added in v0.47.0
func (s *Service) SetPreChurn(previousShutdown string, store PreChurnStore, db *bbolt.DB)
SetPreChurn wires the Spec 080 US3 pre-churn snapshot: the startup-derived previous_shutdown value (stable for the life of this instance, FR-011) and the BBolt-backed store used to read last_error_code at heartbeat build time. Optional; when unset, both fields are omitted from the payload. Safe to call once during startup, before the heartbeat loop begins.
func (*Service) SetRuntimeStats ¶
func (s *Service) SetRuntimeStats(stats RuntimeStats)
SetRuntimeStats sets the runtime stats provider (called after runtime is fully initialized).
func (*Service) Start ¶
Start begins the telemetry heartbeat loop. This is a blocking call; run in a goroutine. Single-shot: a second Start is refused, and a Start that lost the race against Stop (fast shutdown) is a no-op. See Stop.
func (*Service) Stop ¶ added in v0.47.0
func (s *Service) Stop()
Stop terminally stops the heartbeat loop and waits for it to exit, including any in-flight tick — buildHeartbeat's BBolt writes and sendHeartbeat's HTTP send. The send carries the loop context, so the runtime's appCancel aborts it promptly and the wait is bounded by the HTTP client's timeout; the BBolt writes are never interrupted mid-transaction, only awaited. Callers must cancel the context passed to Start before (or concurrently with) Stop, or Stop blocks until the loop exits on its own. Stop returns immediately when Start never ran, is idempotent, and makes a Start goroutine that has not yet been scheduled a permanent no-op — Runtime.Close relies on this so no telemetry BBolt write can land after the clean-shutdown marker resolves or after the DB closes (Spec 080 FR-010, review round 6).
func (*Service) SubmitFeedback ¶
func (s *Service) SubmitFeedback(ctx context.Context, req *FeedbackRequest) (*FeedbackResponse, error)
SubmitFeedback sends feedback to the telemetry backend.
type Surface ¶ added in v0.24.0
type Surface int
Surface identifies which client surface originated a request.
func ParseClientSurface ¶ added in v0.24.0
ParseClientSurface maps the X-MCPProxy-Client header value to a Surface enum. The expected format is "<surface>/<version>"; unknown prefixes and missing headers map to SurfaceUnknown.
type TTYChecker ¶ added in v0.25.0
type TTYChecker interface {
IsTerminal() bool
}
TTYChecker abstracts stdin-is-a-terminal detection so the env_kind detector can be unit-tested without attaching a real TTY.