Revert "feat: remove PostHog usage telemetry entirely"
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.
This commit is contained in:
@@ -276,6 +276,8 @@ only writes to the DB — it is not a web server.
|
||||
with a toast (toggleable), plus a **What's new** dialog that shows the changelog
|
||||
(English / French) on the first launch after an update — reopenable any time
|
||||
from the Help menu.
|
||||
- **Anonymous usage telemetry** (a once-a-day heartbeat: random install ID +
|
||||
version + OS — no callsign or QSO data; opt-out in Preferences).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1120,6 +1120,9 @@ func (a *App) startup(ctx context.Context) {
|
||||
}
|
||||
}()
|
||||
|
||||
// Anonymous usage heartbeat (once/day) so we can gauge active users. No-op
|
||||
// when disabled in Preferences or until the PostHog key is configured.
|
||||
go a.sendTelemetryHeartbeat()
|
||||
go a.liveStatusLoop() // multi-op: heartbeat current activity to shared MySQL
|
||||
go a.chatLoop() // multi-op: poll the shared chat + heartbeat presence
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
"version": "0.20.12",
|
||||
"date": "2026-07-23",
|
||||
"en": [
|
||||
"Removed the anonymous usage telemetry entirely. OpsLog no longer sends the once-a-day heartbeat (it went to PostHog), and the 'Send anonymous usage statistics' option is gone from Preferences → General. Nothing about your usage leaves the app any more.",
|
||||
"FlexRadio panel: the RECEIVE card is shorter again. All the noise controls added recently made it tower, so only the everyday ones — NB, NR, ANF — now stay visible; WNB and the SmartSDR v4 DSP block (NRL/NRS/NRF/ANFL and the AI/FFT RNN & ANFT) tuck behind a 'DSP' button you expand when you need them. The button carries a dot and highlights when one of the hidden controls is switched on, so nothing active is ever out of sight, and its open/closed state is remembered.",
|
||||
"ADIF export can now pick exactly which fields to write. The global 'Export to ADIF' dialog gains a third choice, 'Choose fields…', alongside 'Standard ADIF fields' and 'All OpsLog fields'; and right-clicking selected QSOs adds 'Export selected — choose fields…'. The picker separates the official ADIF 3.1.7 fields (grouped by category) from OpsLog / non-standard tags actually present in your log, with All/None per group and a one-click reset to defaults. Your selection is remembered for next time.",
|
||||
"Fixed 'Incorrect string value' (MySQL error 1366) when a QSO contained non-Latin characters — Cyrillic (Я), Polish ł, etc. — e.g. updating from QRZ. It happened when the shared MySQL database had been pre-created by the hosting panel as latin1, so OpsLog's tables couldn't store those letters. OpsLog now converts the database and its tables to utf8mb4 on connect (once, automatically), and everything stores correctly. Local SQLite logbooks were never affected.",
|
||||
@@ -15,7 +14,6 @@
|
||||
"Fixed the colour theme sometimes resetting to light after an update/relaunch: an update can clear the WebView's localStorage, and the fallback that restores the theme from the local settings database gave up after ~2.4s — occasionally too soon during the brief startup window before that store is ready. It now keeps retrying until the store actually answers, so a dark theme is reliably restored."
|
||||
],
|
||||
"fr": [
|
||||
"Suppression complète de la télémétrie d'usage anonyme. OpsLog n'envoie plus le battement quotidien (il allait vers PostHog), et l'option « Envoyer des statistiques d'usage anonymes » a disparu de Réglages → Général. Plus rien concernant ton usage ne quitte l'application.",
|
||||
"Panneau FlexRadio : la carte RÉCEPTION est de nouveau plus compacte. Tous les contrôles de bruit ajoutés récemment la faisaient s'allonger, donc seuls ceux du quotidien — NB, NR, ANF — restent visibles ; le WNB et le bloc DSP SmartSDR v4 (NRL/NRS/NRF/ANFL ainsi que RNN & ANFT IA/FFT) se replient derrière un bouton « DSP » que tu déplies au besoin. Le bouton porte un point et s'illumine quand l'un des contrôles masqués est activé, pour ne jamais perdre de vue quelque chose d'actif, et son état ouvert/fermé est mémorisé.",
|
||||
"L'export ADIF permet maintenant de choisir précisément les champs à écrire. La fenêtre globale « Exporter en ADIF » gagne un troisième choix, « Choisir les champs… », à côté de « Champs ADIF standard » et « Tous les champs OpsLog » ; et le clic droit sur des QSO sélectionnés ajoute « Exporter la sélection — choisir les champs… ». Le sélecteur sépare les champs ADIF 3.1.7 officiels (regroupés par catégorie) des balises OpsLog / non standard réellement présentes dans ton log, avec Tout/Aucun par groupe et un retour aux valeurs par défaut en un clic. Ta sélection est mémorisée pour la prochaine fois.",
|
||||
"Correction de « Incorrect string value » (erreur MySQL 1366) quand un QSO contenait des caractères non latins — cyrillique (Я), polonais ł, etc. — p. ex. lors d'une mise à jour depuis QRZ. Ça arrivait quand la base MySQL partagée avait été pré-créée en latin1 par le panel d'hébergement, empêchant les tables d'OpsLog de stocker ces lettres. OpsLog convertit désormais la base et ses tables en utf8mb4 à la connexion (une seule fois, automatiquement), et tout s'enregistre correctement. Les logbooks SQLite locaux n'étaient pas concernés.",
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
GetDatabaseSettings, PickOpenDatabase, PickSaveDatabase, OpenDatabase, MoveDatabase, ResetDatabaseToDefault, RestartApp, CreateDatabase, RenameDatabase,
|
||||
GetMySQLSettings, SaveMySQLSettings, TestMySQLConnection, GetDBBackendStatus,
|
||||
GetAutostartPrograms, SaveAutostartPrograms, BrowseExecutable, LaunchAutostartProgram,
|
||||
GetTelemetryEnabled, SetTelemetryEnabled,
|
||||
GetQSLDefaults, SaveQSLDefaults,
|
||||
GetExternalServices, SaveExternalServices, TestQRZUpload, TestClublogUpload, TestHRDLogUpload, TestEQSLUpload,
|
||||
GetPOTAToken, SavePOTAToken,
|
||||
@@ -545,6 +546,26 @@ function AutostartPanelComponent() {
|
||||
);
|
||||
}
|
||||
|
||||
// TelemetryToggle is a self-contained opt-out for the anonymous usage heartbeat
|
||||
// (a random install ID + version + OS, sent once a day). Real component so it
|
||||
// can own its state; embedded inside GeneralPanel.
|
||||
function TelemetryToggle() {
|
||||
const { t } = useI18n();
|
||||
const [on, setOn] = useState(true);
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
useEffect(() => {
|
||||
GetTelemetryEnabled().then((v) => setOn(!!v)).catch(() => {}).finally(() => setLoaded(true));
|
||||
}, []);
|
||||
return (
|
||||
<label className="flex items-center gap-2 text-sm cursor-pointer">
|
||||
<Checkbox checked={on} disabled={!loaded}
|
||||
onCheckedChange={(c) => { const v = !!c; setOn(v); SetTelemetryEnabled(v).catch(() => {}); }} />
|
||||
{t('settings.telemetry')}
|
||||
<span className="text-xs text-muted-foreground">({t('settings.telemetryHint')})</span>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
// ADIFMonitorPanel watches a list of external ADIF files (fldigi RTTY, N1MM,
|
||||
// VarAC…) and auto-imports newly appended QSOs — deliberately option-free: a
|
||||
// contact that arrives is imported and uploaded automatically like any log entry.
|
||||
@@ -4673,6 +4694,8 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
<Checkbox checked={checkUpdates} onCheckedChange={(c) => { const v = !!c; setCheckUpdates(v); writeUiPref('opslog.checkUpdates', v ? '1' : '0'); }} />
|
||||
{t('gen.checkUpdates')} <span className="text-xs text-muted-foreground">{t('gen.checkUpdatesHint')}</span>
|
||||
</label>
|
||||
<TelemetryToggle />
|
||||
|
||||
<MainViewPanes onChanged={onMainPaneChanged} flexAvailable={flexAvailable} icomAvailable={icomAvailable} />
|
||||
|
||||
<div className="border-t border-border/60 pt-4 space-y-2">
|
||||
|
||||
@@ -98,6 +98,8 @@ const en: Dict = {
|
||||
'offline.synced': '{n} QSO(s) added to the logbook',
|
||||
'offline.stillDown': 'Database still unreachable — your QSOs are safe',
|
||||
'settings.theme': 'Theme', 'settings.themeHint': 'Interface colour theme.',
|
||||
'settings.telemetry': 'Send anonymous usage statistics',
|
||||
'settings.telemetryHint': 'install ID + version + OS, once a day — no callsign or QSO data',
|
||||
'settings.mainView': 'Main view',
|
||||
'settings.mainViewHint': 'Choose what the Main tab shows on each side (per profile).',
|
||||
'settings.leftPane': 'Left pane', 'settings.rightPane': 'Right pane',
|
||||
@@ -465,6 +467,8 @@ const fr: Dict = {
|
||||
'offline.synced': '{n} QSO ajoutés au journal',
|
||||
'offline.stillDown': 'Base toujours injoignable — tes QSO sont en sécurité',
|
||||
'settings.theme': 'Thème', 'settings.themeHint': "Thème de couleur de l'interface.",
|
||||
'settings.telemetry': "Envoyer des statistiques d'usage anonymes",
|
||||
'settings.telemetryHint': "ID d'installation + version + OS, une fois par jour — aucun indicatif ni donnée QSO",
|
||||
'settings.mainView': 'Vue principale',
|
||||
'settings.mainViewHint': "Choisis ce que l'onglet Principal affiche de chaque côté (par profil).",
|
||||
'settings.leftPane': 'Volet gauche', 'settings.rightPane': 'Volet droit',
|
||||
|
||||
Vendored
+4
@@ -472,6 +472,8 @@ export function GetStationSettings():Promise<main.StationSettings>;
|
||||
|
||||
export function GetStationStatus():Promise<Array<main.StationDeviceStatus>>;
|
||||
|
||||
export function GetTelemetryEnabled():Promise<boolean>;
|
||||
|
||||
export function GetUIPref(arg1:string):Promise<string>;
|
||||
|
||||
export function GetUltrabeamSettings():Promise<main.UltrabeamSettings>;
|
||||
@@ -876,6 +878,8 @@ export function SetDVKLabel(arg1:number,arg2:string):Promise<void>;
|
||||
|
||||
export function SetPassphrase(arg1:string):Promise<void>;
|
||||
|
||||
export function SetTelemetryEnabled(arg1:boolean):Promise<void>;
|
||||
|
||||
export function SetUIPref(arg1:string,arg2:string):Promise<void>;
|
||||
|
||||
export function SetUltrabeamDirection(arg1:number):Promise<void>;
|
||||
|
||||
@@ -898,6 +898,10 @@ export function GetStationStatus() {
|
||||
return window['go']['main']['App']['GetStationStatus']();
|
||||
}
|
||||
|
||||
export function GetTelemetryEnabled() {
|
||||
return window['go']['main']['App']['GetTelemetryEnabled']();
|
||||
}
|
||||
|
||||
export function GetUIPref(arg1) {
|
||||
return window['go']['main']['App']['GetUIPref'](arg1);
|
||||
}
|
||||
@@ -1706,6 +1710,10 @@ export function SetPassphrase(arg1) {
|
||||
return window['go']['main']['App']['SetPassphrase'](arg1);
|
||||
}
|
||||
|
||||
export function SetTelemetryEnabled(arg1) {
|
||||
return window['go']['main']['App']['SetTelemetryEnabled'](arg1);
|
||||
}
|
||||
|
||||
export function SetUIPref(arg1, arg2) {
|
||||
return window['go']['main']['App']['SetUIPref'](arg1, arg2);
|
||||
}
|
||||
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
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])
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
package main
|
||||
|
||||
// appVersion is this build's version. It's shown in the About/update flow, the
|
||||
// diagnostic-log e-mail and the multi-op live-status heartbeat. The release
|
||||
// script (.vscode/release.ps1) rewrites this line alongside frontend/version.ts.
|
||||
const appVersion = "0.20.11"
|
||||
@@ -18,5 +18,7 @@ them.
|
||||
- **Callsign lookups** go to QRZ.com / HamQTH with your credentials.
|
||||
- **QSL uploads/downloads** go to the services you configure (LoTW via TQSL, QRZ,
|
||||
eQSL, ClubLog, HRDLog).
|
||||
- **Telemetry** is a once-a-day anonymous heartbeat (random install ID + version
|
||||
+ OS — no callsign, no QSO data). Opt out in Preferences.
|
||||
- Everything else stays local (or on your own MySQL / web server for the
|
||||
multi-op live status).
|
||||
|
||||
@@ -47,3 +47,5 @@ format, or an **ADIF record on each logged QSO** — so external tools stay in s
|
||||
- **Autostart** external programs (WSJT-X, JTAlert, rotator control…) at launch,
|
||||
skipping any already running.
|
||||
- **Update check** at startup (toggleable).
|
||||
- **Anonymous usage telemetry** — a once-a-day heartbeat (random install ID +
|
||||
version + OS; no callsign or QSO data). Opt out in Preferences.
|
||||
|
||||
Reference in New Issue
Block a user