telemetry

package
v0.35.0 Latest Latest
Warning

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

Go to latest
Published: Oct 6, 2026 License: Apache-2.0 Imports: 20 Imported by: 0

Documentation

Overview

Package telemetry is ovdb's opt-in usage statistics (spec/features/ telemetry-consent, decision 0009): the closed event set, the consent state stored in config.yaml, the forced-off conditions evaluated in the sending process, channel detection and a small synchronous PostHog (EU) batch sender. Nothing is sent unless a person turned it on.

Every outbound telemetry request originates here. Events are values with unexported fields, built only by the constructors below, which map every input onto an allowlisted enum, boolean or number — so no database id, path, URL, query, error message or other user-entered text can reach the wire (REQ:closed-event-set, REQ:never-collected).

Index

Constants

View Source
const (
	StateNotAsked = "not_asked"
	StateEnabled  = "enabled"
	StateDisabled = "disabled"
)

Consent states (REQ:opt-in-state). The stored empty state is not_asked.

View Source
const (
	ReasonEnvOVDB      = "OVDB_TELEMETRY"
	ReasonDoNotTrack   = "DO_NOT_TRACK"
	ReasonCI           = "CI"
	ReasonUnavailable  = "unavailable"
	ReasonNotAsked     = StateNotAsked
	ReasonDisabled     = StateDisabled
	ReasonConfigBroken = "config_unreadable"
)

Reasons telemetry does not send. The forced-off ones name the variable that forced it (REQ:sender-process-decides).

View Source
const EnableCommand = "ovdb telemetry enable"

EnableCommand turns telemetry on in a terminal, asking the person first. The relay flag is named only in AgentGuidance (review F6).

View Source
const EnvTelemetry = "OVDB_TELEMETRY"

EnvTelemetry is OVDB's own switch; only "0" means anything (off). No variable can turn telemetry on (REQ:enable-requires-a-person).

View Source
const IPPlaceholder = "0.0.0.0"

IPPlaceholder is sent as $ip on every event: PostHog uses the client IP address only when $ip isn't passed in the properties (posthog.com tutorials/web-redact-properties, docs/product-analytics/privacy), so it stores the placeholder. The address still reaches PostHog's servers; the project's "Discard client IP data" setting stays a release precondition in case a transformation reads the connection address.

View Source
const MaxBuffered = 100

MaxBuffered is the pre-consent buffer's size (REQ:pre-consent-buffer).

View Source
const MaxQueuedBatches = 16

MaxQueuedBatches bounds the background sender's queue; a batch that finds it full is dropped, never blocking the caller.

View Source
const Other = "other"

Other replaces any value outside an enum.

View Source
const Timeout = 2 * time.Second

Timeout bounds one batch send, connection included (REQ:bounded-synchronous-sender).

View Source
const UserAgent = "ovdb"

UserAgent is the only client header ovdb sets: no version, OS user or host name.

Variables

View Source
var ConsentStates = []string{StateEnabled}

ConsentStates are the telemetry_consent_changed states (enabled only: turning it off sends nothing).

View Source
var Engines = []string{"ingitdb", "sqlite", "firestore", "mysql", "postgres"}

Engines are the storage engine ids ovdb offers.

View Source
var Harnesses = func() []string {
	ids := make([]string, 0, len(cobracmd.DefaultHarnesses))
	for _, h := range cobracmd.DefaultHarnesses {
		ids = append(ids, h.ID)
	}
	return ids
}()

Harnesses are the skillsync harness ids skill installs target (cobracmd.DefaultHarnesses, the list `ovdb skills install --harness` accepts); a custom --dir is "other".

Names lists every event, in the spec's order.

View Source
var Options = []string{"demo", "create", "connect", "server", "browse", "explore", "skills", "settings", "databases"}

Options are the onboarding choices, also used as error and completion steps.

View Source
var Skills = []string{"openvaultdb", "todo-demo"}

Skills are the skills ovdb installs.

View Source
var Targets = []string{"datatug_cli", "datatug_web"}

Targets are the Explore data choices.

Functions

func AgentGuidance

func AgentGuidance() string

AgentGuidance is the JSON-only instruction for AI agents.

func Available

func Available() bool

Available reports whether this build can send at all.

func Collected

func Collected() []string

Collected is what is collected, as every interface lists it.

func ForcedOff

func ForcedOff(getenv func(string) string) string

ForcedOff is the forced-off reason in the environment getenv reads, or "" (the opt-out precedence of specscore-cli's ResolveOptOut, rungs 2 and 3: explicit variables, then CI). DO_NOT_TRACK counts when set to anything but "0" or "false" (telemetry-consent States table).

func Key

func Key() string

Key is the build's PostHog project key, "" when unavailable.

func NeverCollected

func NeverCollected() []string

NeverCollected is what is never collected.

func NewInstallID

func NewInstallID() string

NewInstallID is a random UUID v4, unrelated to anything on the machine.

func OptOutVariables

func OptOutVariables() []string

OptOutVariables lists every variable ForcedOff reads.

func Payload

func Payload(key string, events []Event, meta Meta) []byte

Payload is the PostHog batch body for events.

func ReasonText

func ReasonText(reason string) string

ReasonText is the sentence for reason, "" when it needs none.

func ValidInstallID

func ValidInstallID(id string) bool

ValidInstallID reports whether id is an install id ovdb generated.

func Version

func Version(v string) string

Version is v as sent: a semver, or "dev" for anything else.

Types

type Change

type Change struct {
	State string `json:"state"` // enabled or disabled
	// ConfirmedByUser must be true to enable: the person said yes
	// (REQ:enable-requires-a-person).
	ConfirmedByUser bool `json:"confirmed_by_user,omitempty"`
	// Channel is the deciding interface: cli, tui or agent, declared by the
	// owner's local process with the instance secret and validated by the
	// server; a console session is always web (review M1).
	Channel string `json:"channel,omitempty"`
}

Change is the body of PUT /api/local/v1/telemetry.

type Channel

type Channel string

Channel is the interface an event or a decision came from.

const (
	ChannelCLI   Channel = "cli"
	ChannelTUI   Channel = "tui"
	ChannelWeb   Channel = "web"
	ChannelAgent Channel = "agent"
)

Channels (REQ:channel-detection).

func DetectChannel

func DetectChannel(getenv func(string) string, environ func() []string) Channel

DetectChannel is agent when a known agent harness variable is present, otherwise cli (REQ:channel-detection). environ lists KEY=VALUE pairs, os.Environ when nil.

func ParseChannel

func ParseChannel(c string) Channel

ParseChannel is c when it is a channel, otherwise cli.

type Consent struct {
	State     string `yaml:"state,omitempty"`
	DecidedAt string `yaml:"decided_at,omitempty"`
	Channel   string `yaml:"channel,omitempty"`
	// InstallID exists only while enabled: created on enable, removed on
	// disable.
	InstallID string `yaml:"install_id,omitempty"`
}

Consent is the telemetry section of config.yaml.

func LoadConsent

func LoadConsent(home string) (Consent, error)

LoadConsent reads the telemetry section of <home>/config.yaml; a missing file is not_asked.

func (Consent) EffectiveState

func (c Consent) EffectiveState() string

EffectiveState is c's state, not_asked when unset or unrecognised.

type Decision

type Decision struct {
	Consent Consent
	State   string
	Sending bool
	Reason  string // "" when Sending
}

Decision is whether this process sends, and why not.

func Decide

func Decide(home string, getenv func(string) string, key string) Decision

Decide evaluates consent in home for the process whose environment getenv reads, with key the build's PostHog key.

type Document

type Document struct {
	Schema    int    `json:"schema"`
	Telemetry Status `json:"telemetry"`
	Changed   *bool  `json:"changed,omitempty"`
	// Backup is where an unreadable config.yaml was saved before disable
	// rewrote it.
	Backup string          `json:"backup,omitempty"`
	Next   []envelope.Next `json:"next"`
}

Document is the body of GET/PUT /api/local/v1/telemetry and the --json output of `ovdb telemetry status|enable|disable`.

func NewDocument

func NewDocument(d Decision, available bool) Document

NewDocument describes d for a process whose build has key.

type Event

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

Event is one closed event. Build it with the constructors.

func FromWire

func FromWire(w Wire) (Event, bool)

FromWire rebuilds w through the constructors; false for an unknown event.

func NewConsentEnabled

func NewConsentEnabled() Event

NewConsentEnabled is telemetry_consent_changed, which is only ever sent for turning telemetry on.

func NewDatabaseConnected

func NewDatabaseConnected(engine string, success bool, d time.Duration) Event

NewDatabaseConnected is existing_database_connected.

func NewDatabaseCreated

func NewDatabaseCreated(engine string, success bool, d time.Duration) Event

NewDatabaseCreated is database_created.

func NewDemoInstalled

func NewDemoInstalled(success, alreadyInstalled bool) Event

NewDemoInstalled is demo_installed.

func NewDemoOpened

func NewDemoOpened(success bool) Event

NewDemoOpened is demo_opened.

func NewEngineSelected

func NewEngineSelected(engine string) Event

NewEngineSelected is engine_selected.

func NewExploreDataSelected

func NewExploreDataSelected(target string, datatugFound bool) Event

NewExploreDataSelected is explore_data_selected.

func NewOnboardingCompleted

func NewOnboardingCompleted(step string) Event

NewOnboardingCompleted is onboarding_completed.

func NewOnboardingError

func NewOnboardingError(step string, err error) Event

NewOnboardingError is onboarding_error; err's envelope code is the only thing taken from it.

func NewOnboardingStarted

func NewOnboardingStarted() Event

NewOnboardingStarted is onboarding_started.

func NewOptionSelected

func NewOptionSelected(option string) Event

NewOptionSelected is onboarding_option_selected.

func NewServerStarted

func NewServerStarted(success, portIsDefault bool, d time.Duration) Event

NewServerStarted is server_started.

func NewSkillInstalled

func NewSkillInstalled(skill, harness string, success bool) Event

NewSkillInstalled is skill_installed.

func (Event) Name

func (e Event) Name() Name

Name is the event's name.

func (Event) Properties

func (e Event) Properties(meta Meta) map[string]any

Properties is e's allowlisted property map, with meta. PostHog's distinct_id is the install id, and GeoIP enrichment is disabled per event.

type Meta

type Meta struct {
	Channel   Channel
	Version   string
	InstallID string
}

Meta is what every event carries besides its own properties.

type Name

type Name string

Name is one event of the closed set.

const (
	OnboardingStarted         Name = "onboarding_started"
	OnboardingOptionSelected  Name = "onboarding_option_selected"
	EngineSelected            Name = "engine_selected"
	DatabaseCreated           Name = "database_created"
	ExistingDatabaseConnected Name = "existing_database_connected"
	ServerStarted             Name = "server_started"
	DemoInstalled             Name = "demo_installed"
	DemoOpened                Name = "demo_opened"
	SkillInstalled            Name = "skill_installed"
	ExploreDataSelected       Name = "explore_data_selected"
	TelemetryConsentChanged   Name = "telemetry_consent_changed"
	OnboardingCompleted       Name = "onboarding_completed"
	OnboardingError           Name = "onboarding_error"
)

The closed event set (REQ:closed-event-set). Adding one is a spec change.

type Recorder

type Recorder struct {
	// Channel is the sending interface; detected from Getenv and Environ
	// at send time when empty (cli or agent).
	Channel Channel
	Environ func() []string // os.Environ when nil
	Home    string
	Version string
	Getenv  func(string) string // os.Getenv when nil
	// Buffer keeps events in memory while not_asked (the TUI process),
	// for Flush after Turn on or Discard after No thanks. The CLI and the
	// server never buffer.
	Buffer bool
	// Key and Endpoint default to the build's values; tests set them.
	Key, Endpoint string
	Client        *http.Client
	// contains filtered or unexported fields
}

Recorder collects one process's events and sends them in one batch per command or step. A nil *Recorder records nothing.

func (*Recorder) Available

func (r *Recorder) Available() bool

Available reports whether this recorder has a key to send with.

func (*Recorder) Consented

func (r *Recorder) Consented()

Consented is this session's own Turn on: the events it buffered while not_asked may now be sent, by the next Flush.

func (*Recorder) Decide

func (r *Recorder) Decide() Decision

Decide is this recorder's process decision.

func (*Recorder) Discard

func (r *Recorder) Discard()

Discard drops every pending event (No thanks, dismissal, exit).

func (*Recorder) Drain

func (r *Recorder) Drain(ctx context.Context)

Drain waits for queued background batches, at most until ctx ends (the server's shutdown).

func (*Recorder) Exit

func (r *Recorder) Exit(ctx context.Context)

Exit ends a buffering process's session (No thanks, dismissal and exit all end the same way): events buffered without this session's Turn on are dropped, even when another process enabled telemetry meanwhile; the rest is sent when this process may send.

func (*Recorder) Flush

func (r *Recorder) Flush(ctx context.Context)

Flush sends the pending events in one synchronous POST bounded by Timeout, when this process may send. A buffering recorder keeps its events while still not_asked; otherwise they are dropped. Failures are silent.

func (*Recorder) FlushInBackground

func (r *Recorder) FlushInBackground()

FlushInBackground is Flush without waiting: the batch goes to a bounded queue that one worker sends, each batch within Timeout (review M5). The server uses it so a console action never waits for PostHog.

func (*Recorder) Pending

func (r *Recorder) Pending() int

Pending is how many events wait for a flush.

func (*Recorder) Record

func (r *Recorder) Record(events ...Event)

Record keeps e when it may be sent, or buffers it while not_asked when buffering; anything else is dropped on the spot.

type Status

type Status struct {
	State string `json:"state"` // not_asked, enabled, disabled
	// Sending is whether the process that answered sends events now.
	Sending bool `json:"sending"`
	// Reason is why it does not send: a forced-off variable, unavailable,
	// not_asked or disabled.
	Reason     string `json:"reason,omitempty"`
	ReasonText string `json:"reason_text,omitempty"`
	// Available is false in a build without a PostHog key.
	Available    bool   `json:"available"`
	Provider     string `json:"provider"`
	DecidedAt    string `json:"decided_at,omitempty"`
	Channel      string `json:"channel,omitempty"`
	HasInstallID bool   `json:"has_install_id"`
	// AgentGuidance tells AI agents how to relay a person's answer; JSON
	// only, never printed for people.
	AgentGuidance  string   `json:"agent_guidance,omitempty"`
	Collected      []string `json:"collected"`
	NeverCollected []string `json:"never_collected"`
}

Status is the shared telemetry state every interface shows (REQ:parity-of-controls).

type Wire

type Wire struct {
	Event            string `json:"event"`
	Option           string `json:"option,omitempty"`
	Engine           string `json:"engine,omitempty"`
	Skill            string `json:"skill,omitempty"`
	Harness          string `json:"harness,omitempty"`
	Target           string `json:"target,omitempty"`
	Step             string `json:"step,omitempty"`
	ErrorCode        string `json:"error_code,omitempty"`
	Success          bool   `json:"success,omitempty"`
	PortIsDefault    bool   `json:"port_is_default,omitempty"`
	AlreadyInstalled bool   `json:"already_installed,omitempty"`
	DataTugFound     bool   `json:"datatug_found,omitempty"`
	DurationMS       int64  `json:"duration_ms,omitempty"`
}

Wire is an event as the web page posts it (POST /api/local/v1/telemetry/ events). It is decoded, then rebuilt through the constructors, so its strings never reach the wire unchecked.

Jump to

Keyboard shortcuts

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