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.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])
|
|
}
|