This reverts commit 6a28344. Restore the once-a-day anonymous PostHog heartbeat
and its opt-out toggle: telemetry.go (sendTelemetryHeartbeat, GetTelemetryEnabled/
SetTelemetryEnabled, PostHog host + API key), the startup goroutine, the Settings
→ General toggle + i18n keys, and the README/wiki mentions. version.go is dropped
again (appVersion moves back into telemetry.go). release.ps1 (untracked) pointed
back at telemetry.go by hand.
130 lines
4.1 KiB
Go
130 lines
4.1 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/rand"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"runtime"
|
|
"strings"
|
|
"time"
|
|
|
|
"hamlog/internal/applog"
|
|
)
|
|
|
|
// Anonymous usage telemetry - a once-a-day "app_opened" heartbeat to PostHog so
|
|
// the OpsLog author can see how many people actively use it. Privacy by design:
|
|
// only a random install ID + app version + OS are sent (no callsign, no QSO
|
|
// data, no IP beyond what any HTTP request reveals). Users can disable it in
|
|
// Preferences -> General. See [[user-analytics-posthog]] notes in MEMORY.
|
|
|
|
const (
|
|
// appVersion is stamped on every heartbeat (and could feed the About box).
|
|
appVersion = "0.20.11"
|
|
|
|
// posthogHost is the PostHog ingestion endpoint. EU cloud by default; change
|
|
// to https://us.i.posthog.com for a US project.
|
|
posthogHost = "https://eu.i.posthog.com"
|
|
|
|
// posthogAPIKey is the PostHog PROJECT API key ("phc_..."). It's a public,
|
|
// write-only ingestion key (safe to ship in the binary, like the ClubLog app
|
|
// key). Until it's filled in, telemetry is a no-op.
|
|
posthogAPIKey = "phc_vumvN7XTERNhmRzMZHNgY5DncZfFibTbomiE9epZvUJ4"
|
|
|
|
keyTelemetryEnabled = "telemetry.enabled" // "0" disables; default on
|
|
keyTelemetryInstallID = "telemetry.install_id" // random, stable per install
|
|
keyTelemetryLastSent = "telemetry.last_sent" // YYYY-MM-DD of last heartbeat
|
|
)
|
|
|
|
// GetTelemetryEnabled reports whether anonymous usage stats are on (default on).
|
|
func (a *App) GetTelemetryEnabled() bool {
|
|
if a.settings == nil {
|
|
return true
|
|
}
|
|
v, _ := a.settings.GetGlobal(a.ctx, keyTelemetryEnabled)
|
|
return strings.TrimSpace(v) != "0"
|
|
}
|
|
|
|
// SetTelemetryEnabled turns anonymous usage stats on or off.
|
|
func (a *App) SetTelemetryEnabled(on bool) error {
|
|
if a.settings == nil {
|
|
return fmt.Errorf("db not initialized")
|
|
}
|
|
val := "1"
|
|
if !on {
|
|
val = "0"
|
|
}
|
|
return a.settings.SetGlobal(a.ctx, keyTelemetryEnabled, val)
|
|
}
|
|
|
|
// telemetryInstallID returns this install's stable anonymous ID, generating and
|
|
// persisting one on first use.
|
|
func (a *App) telemetryInstallID() string {
|
|
id, _ := a.settings.GetGlobal(a.ctx, keyTelemetryInstallID)
|
|
if id = strings.TrimSpace(id); id != "" {
|
|
return id
|
|
}
|
|
id = randomUUID()
|
|
_ = a.settings.SetGlobal(a.ctx, keyTelemetryInstallID, id)
|
|
return id
|
|
}
|
|
|
|
// sendTelemetryHeartbeat sends at most one "app_opened" event per calendar day
|
|
// (UTC). Best effort: any failure is logged and ignored. Runs in a goroutine at
|
|
// startup.
|
|
func (a *App) sendTelemetryHeartbeat() {
|
|
if a.settings == nil || !a.GetTelemetryEnabled() {
|
|
return
|
|
}
|
|
if strings.Contains(posthogAPIKey, "REPLACE") || strings.TrimSpace(posthogAPIKey) == "" {
|
|
return // not configured yet
|
|
}
|
|
today := time.Now().UTC().Format("2006-01-02")
|
|
if last, _ := a.settings.GetGlobal(a.ctx, keyTelemetryLastSent); strings.TrimSpace(last) == today {
|
|
return // already counted today
|
|
}
|
|
|
|
payload := map[string]any{
|
|
"api_key": posthogAPIKey,
|
|
"event": "app_opened",
|
|
"distinct_id": a.telemetryInstallID(),
|
|
"properties": map[string]any{
|
|
"version": appVersion,
|
|
"os": runtime.GOOS,
|
|
"arch": runtime.GOARCH,
|
|
"$lib": "opslog",
|
|
},
|
|
"timestamp": time.Now().UTC().Format(time.RFC3339),
|
|
}
|
|
body, err := json.Marshal(payload)
|
|
if err != nil {
|
|
return
|
|
}
|
|
client := &http.Client{Timeout: 15 * time.Second}
|
|
resp, err := client.Post(posthogHost+"/capture/", "application/json", bytes.NewReader(body))
|
|
if err != nil {
|
|
applog.Printf("telemetry: heartbeat failed: %v", err)
|
|
return
|
|
}
|
|
resp.Body.Close()
|
|
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
|
|
_ = a.settings.SetGlobal(a.ctx, keyTelemetryLastSent, today)
|
|
applog.Printf("telemetry: heartbeat ok (%s)", today)
|
|
} else {
|
|
applog.Printf("telemetry: heartbeat HTTP %d", resp.StatusCode)
|
|
}
|
|
}
|
|
|
|
// randomUUID returns a random RFC-4122 v4 UUID string.
|
|
func randomUUID() string {
|
|
var b [16]byte
|
|
if _, err := rand.Read(b[:]); err != nil {
|
|
// Extremely unlikely; fall back to a time-based id so we still get one.
|
|
return fmt.Sprintf("ts-%d", time.Now().UnixNano())
|
|
}
|
|
b[6] = (b[6] & 0x0f) | 0x40 // version 4
|
|
b[8] = (b[8] & 0x3f) | 0x80 // variant 10
|
|
return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:16])
|
|
}
|