From a36ca2facb4648c6d208777fc2cd05d2bda068e7 Mon Sep 17 00:00:00 2001 From: elpatron Date: Sun, 31 May 2026 21:43:30 +0200 Subject: [PATCH] Add Plausible analytics for live journal and NMEA upload. Track Live Log Opened/Event Logged with action types, NMEA Uploaded on parse success, and align NMEA Imported properties with docs. Co-authored-by: Cursor --- client/src/components/LiveLogView.tsx | 48 +++++++++++++--------- client/src/components/NmeaImportWizard.tsx | 8 +++- client/src/services/analytics.ts | 5 ++- docs/plausible-events.md | 40 +++++++++++++++++- 4 files changed, 77 insertions(+), 24 deletions(-) diff --git a/client/src/components/LiveLogView.tsx b/client/src/components/LiveLogView.tsx index ab8638c..24c9fc2 100644 --- a/client/src/components/LiveLogView.tsx +++ b/client/src/components/LiveLogView.tsx @@ -185,6 +185,12 @@ export default function LiveLogView({ return () => { cancelled = true } }, [logbookId, refreshEntry, t]) + useEffect(() => { + if (!loading && entryId) { + trackPlausibleEvent(PlausibleEvents.LIVE_LOG_OPENED) + } + }, [loading, entryId]) + useEffect(() => { streamEndRef.current?.scrollIntoView({ behavior: 'smooth' }) }, [events.length]) @@ -226,7 +232,7 @@ export default function LiveLogView({ const runQuickAction = async ( action: () => Promise, - trackEvent?: string, + trackAction?: string, withUndo = true ) => { if (!entryId || busy) return @@ -236,7 +242,9 @@ export default function LiveLogView({ await action() await refreshEntry(entryId) if (withUndo) showUndo() - if (trackEvent) trackPlausibleEvent(PlausibleEvents.TRAVEL_DAY_SAVED, { context: trackEvent }) + if (trackAction) { + trackPlausibleEvent(PlausibleEvents.LIVE_LOG_EVENT_LOGGED, { action: trackAction }) + } } catch (err: unknown) { console.error('Live log action failed:', err) setError(err instanceof Error ? err.message : t('logs.live_action_error')) @@ -264,28 +272,28 @@ export default function LiveLogView({ const handleMotorToggle = () => { hapticPulse() + const starting = !motorRunning void runQuickAction(async () => { if (!entryId) return - const starting = !motorRunning await appendQuickEvent(logbookId, entryId, { sailsOrMotor: starting ? motorLabel : '', remarks: starting ? LIVE_EVENT_CODES.MOTOR_START : LIVE_EVENT_CODES.MOTOR_STOP }) - }, 'live_motor') + }, starting ? 'motor_start' : 'motor_stop') } const handleCastOff = () => { void runQuickAction(async () => { if (!entryId) return await appendQuickEvent(logbookId, entryId, { remarks: LIVE_EVENT_CODES.CAST_OFF }) - }, 'live_cast_off') + }, 'cast_off') } const handleMoor = () => { void runQuickAction(async () => { if (!entryId) return await appendQuickEvent(logbookId, entryId, { remarks: LIVE_EVENT_CODES.MOOR }) - }, 'live_moor') + }, 'moor') } const handleFix = () => { @@ -301,7 +309,7 @@ export default function LiveLogView({ } catch { await showAlert(t('logs.live_gps_error'), t('logs.live_fix')) } - }, 'live_fix') + }, 'fix') } const handleUndo = () => { @@ -313,7 +321,7 @@ export default function LiveLogView({ } void runQuickAction(async () => { await removeLastEvent(logbookId, entryId) - }, 'live_undo', false) + }, 'undo', false) } const confirmSails = () => { @@ -330,7 +338,7 @@ export default function LiveLogView({ sailsOrMotor: sailsLabel, remarks: liveSailsRemark(sailsLabel) }) - }, 'live_sails') + }, 'sails') } const confirmComment = () => { @@ -344,7 +352,7 @@ export default function LiveLogView({ void runQuickAction(async () => { if (!entryId) return await appendQuickEvent(logbookId, entryId, { remarks: liveCommentRemark(text) }) - }, 'live_comment') + }, 'comment') } const confirmValueModal = () => { @@ -362,7 +370,7 @@ export default function LiveLogView({ windStrength: secondary, remarks: LIVE_EVENT_CODES.WIND }) - }, 'live_wind') + }, 'wind') break case 'pressure': if (!primary) return @@ -372,21 +380,21 @@ export default function LiveLogView({ windPressure: primary, remarks: LIVE_EVENT_CODES.PRESSURE }) - }, 'live_pressure') + }, 'pressure') break case 'temp': if (!primary) return setModal('none') void runQuickAction(async () => { await appendQuickEvent(logbookId, entryId, { remarks: liveTempRemark(primary) }) - }, 'live_temp') + }, 'temp') break case 'precip': if (!primary) return setModal('none') void runQuickAction(async () => { await appendQuickEvent(logbookId, entryId, { remarks: livePrecipRemark(primary) }) - }, 'live_precip') + }, 'precip') break case 'sea_state': if (!primary) return @@ -396,7 +404,7 @@ export default function LiveLogView({ seaState: primary, remarks: LIVE_EVENT_CODES.SEA_STATE }) - }, 'live_sea_state') + }, 'sea_state') break case 'course': { const course = primary || lastCourseFromEvents(events) @@ -407,7 +415,7 @@ export default function LiveLogView({ mgk: course, remarks: LIVE_EVENT_CODES.COURSE }) - }, 'live_course') + }, 'course') break } case 'fuel': { @@ -418,7 +426,7 @@ export default function LiveLogView({ await appendTankRefill(logbookId, entryId, 'fuel', liters, { remarks: liveFuelRemark(String(liters)) }) - }, 'live_fuel') + }, 'fuel') break } case 'water': { @@ -429,7 +437,7 @@ export default function LiveLogView({ await appendTankRefill(logbookId, entryId, 'freshwater', liters, { remarks: liveWaterRemark(String(liters)) }) - }, 'live_water') + }, 'water') break } case 'sog': { @@ -440,7 +448,7 @@ export default function LiveLogView({ await appendQuickEvent(logbookId, entryId, { remarks: liveSogRemark(String(speedKn)) }) - }, 'live_sog') + }, 'sog') break } case 'stw': { @@ -451,7 +459,7 @@ export default function LiveLogView({ await appendQuickEvent(logbookId, entryId, { remarks: liveStwRemark(String(speedKn)) }) - }, 'live_stw') + }, 'stw') break } default: diff --git a/client/src/components/NmeaImportWizard.tsx b/client/src/components/NmeaImportWizard.tsx index 583b42b..2d85f39 100644 --- a/client/src/components/NmeaImportWizard.tsx +++ b/client/src/components/NmeaImportWizard.tsx @@ -101,6 +101,12 @@ export default function NmeaImportWizard({ t }).candidates setSelectedIds(new Set(generated.map((c) => c.id))) + trackPlausibleEvent(PlausibleEvents.NMEA_UPLOADED, { + duplicate: alreadyImported, + lines: result.stats.parsedLines, + candidates: generated.length, + has_position: !result.warnings.includes('no_position') + }) } catch (err) { setError(err instanceof Error ? err.message : t('logs.nmea_error_parse')) } @@ -154,7 +160,7 @@ export default function NmeaImportWizard({ } trackPlausibleEvent(PlausibleEvents.NMEA_IMPORTED, { mode, - candidates: picked.length, + events: picked.length, track: importTrack && (waypoints?.length ?? 0) > 0 }) setStep('archive') diff --git a/client/src/services/analytics.ts b/client/src/services/analytics.ts index a055bdc..ccc07a8 100644 --- a/client/src/services/analytics.ts +++ b/client/src/services/analytics.ts @@ -36,7 +36,10 @@ export const PlausibleEvents = { DEVICE_FORGOTTEN: 'Device Forgotten', RECOVERY_ROTATED: 'Recovery Rotated', LANGUAGE_CHANGED: 'Language Changed', - NMEA_IMPORTED: 'NMEA Imported' + NMEA_IMPORTED: 'NMEA Imported', + NMEA_UPLOADED: 'NMEA Uploaded', + LIVE_LOG_OPENED: 'Live Log Opened', + LIVE_LOG_EVENT_LOGGED: 'Live Log Event Logged' } as const export type PlausibleEventName = (typeof PlausibleEvents)[keyof typeof PlausibleEvents] diff --git a/docs/plausible-events.md b/docs/plausible-events.md index 597cd78..be4d3df 100644 --- a/docs/plausible-events.md +++ b/docs/plausible-events.md @@ -21,7 +21,8 @@ Kapteins Daagbok nutzt [Plausible Analytics](https://plausible.io/) mit dem Scri | Travel Day Saved | Reisetag gespeichert (`LogEntryEditor.tsx`) | — | | Entry Signed | Passkey-Signatur Skipper oder Crew (`LogEntryEditor.tsx`) | `role`: `skipper` \| `crew` | | GPS Track Uploaded | GPX/KML/GeoJSON hochgeladen (`LogEntryEditor.tsx`) | — | -| NMEA Imported | NMEA-Protokoll in Journal übernommen (`NmeaImportWizard.tsx`) | `mode`: `interval` \| `change` \| `both`, `events`, `track` (Anzahlen/Flags, keine Koordinaten) | +| NMEA Uploaded | NMEA-Datei erfolgreich gelesen und geparst (`NmeaImportWizard.tsx`) | `lines` (Anzahl Sätze), `candidates` (Vorschläge für Reisetag), `duplicate` (Datei schon importiert), `has_position` | +| NMEA Imported | NMEA-Vorschläge in Journal übernommen (`NmeaImportWizard.tsx`) | `mode`: `interval` \| `change` \| `both`, `events` (übernommene Einträge), `track` (GPS-Track mit importiert) | | Vessel Saved | Schiffsdaten gespeichert (`VesselForm.tsx`) | — | | Crew Saved | Skipper- oder Crew-Profil gespeichert (`CrewForm.tsx`) | `role`: `skipper` \| `crew`, `action`: `create` \| `update` | | Account Deleted | Konto erfolgreich gelöscht (`auth.ts`) | — | @@ -51,6 +52,33 @@ Kapteins Daagbok nutzt [Plausible Analytics](https://plausible.io/) mit dem Scri | Device Forgotten | Account aus Schnell-Login-Liste dieses Geräts entfernt (`UserProfilePage.tsx`) | — | | Recovery Rotated | Neuer 12-Wörter-Wiederherstellungsschlüssel erstellt (`UserProfilePage.tsx`) | — | | Language Changed | Sprache über UI-Wechsler gewählt (`i18nLanguages.ts` via Sprach-Button in App, Dashboard, Auth, Demo, Einladung, Share-Viewer) | `from`, `to`: ISO 639-1 (`de`, `en`, `da`, `sv`, `nb`) | +| Live Log Opened | Live-Journal-Ansicht geladen (`LiveLogView.tsx`, einmal pro Mount nach erfolgreichem Init) | — | +| Live Log Event Logged | Quick-Action erfolgreich ins heutige Journal geschrieben (`LiveLogView.tsx`) | `action`: siehe [Live-Log-Aktionen](#live-log-aktionen) | + +### Live-Log-Aktionen + +Property `action` bei **Live Log Event Logged** — stabile englische Schlüssel, keine Inhalte (kein Kurs, kein Kommentartext, keine Koordinaten): + +| `action` | Button / Auslöser | +|----------|-------------------| +| `motor_start` | Motor Start | +| `motor_stop` | Motor Stop | +| `cast_off` | Ablegen | +| `moor` | Anlegen | +| `sails` | Segel (Modal bestätigt) | +| `course` | Kurs (Dial/Modal bestätigt) | +| `sog` | SOG | +| `stw` | STW | +| `fuel` | Diesel-Tank | +| `water` | Wasser-Tank | +| `wind` | Wind (Richtung/Stärke) | +| `pressure` | Luftdruck | +| `temp` | Temperatur | +| `precip` | Niederschlag | +| `sea_state` | Seegang | +| `fix` | GPS-Fix (manuell) | +| `comment` | Kommentar | +| `undo` | Letztes Ereignis rückgängig | ## Bewusst nicht getrackt @@ -59,6 +87,10 @@ Kapteins Daagbok nutzt [Plausible Analytics](https://plausible.io/) mit dem Scri - **PII:** Keine Inhalte aus verschlüsselten Logbüchern in Properties. - **Profil-KPIs:** Statistik-Karten und User-ID-Kopieren werden nicht getrackt (reine Anzeige bzw. zu granular). - **Sprache bei Erstbesuch:** Automatische Browser-/URL-Erkennung (`i18next-browser-languagedetector`, `?lng=`) löst kein `Language Changed` aus — nur explizite Klicks auf den Sprach-Button. +- **Live-Log Auto-Position:** Hintergrund-GPS alle 3 h (`LIVE_EVENT_CODES.AUTO_POSITION`) — automatisch, best-effort, kein Nutzer-Tap. +- **Live-Log Modals:** Öffnen/Abbrechen von Dialogen ohne Speichern; Wechsel Liste ↔ Live (nur `Live Log Opened` beim erneuten Mount). +- **Live-Log Editor-Link:** Öffnen des vollständigen Editors aus der Live-Ansicht. +- **NMEA-Import:** Abbrechen, Vorschau ohne Übernahme, Archiv-Entscheid (Archivieren/Verwerfen); fehlgeschlagene Datei-Lesevorgänge. - **Kontolöschung:** `Account Deleted` bleibt in `auth.ts` — unabhängig davon, ob die Gefahrenzone auf der Profilseite oder früher in den Einstellungen genutzt wurde. ## Typische Funnels (Plausible Goals) @@ -73,7 +105,8 @@ Empfohlene Goal-Ketten für Auswertung (nur Business!): 6. **Datensicherung:** Backup Exported → Backup Restored 7. **Kontosicherheit:** Profile Opened → Passkey Added / Local PIN Set / Recovery Rotated; Last Passkey Remove Hinted → Account Deleted (selten, aber aussagekräftig) 8. **Internationalisierung:** Language Changed (Verteilung `to`, Pfade mit Übersetzungs-Feedback) -9. **NMEA-Import:** NMEA Imported (Modus, Anzahl übernommener Ereignisse, optional Track) +9. **NMEA-Import:** NMEA Uploaded → NMEA Imported (Modus, `events`, optional Track; Upload-Funnel vs. Abbruch) +10. **Live-Journal:** Live Log Opened → Live Log Event Logged (Verteilung `action`; z. B. `fix`, `course`, `motor_start`) ## Entwicklung @@ -83,6 +116,9 @@ import { PlausibleEvents, trackPlausibleEvent } from './services/analytics.js' trackPlausibleEvent(PlausibleEvents.TRAVEL_DAY_SAVED) trackPlausibleEvent(PlausibleEvents.ENTRY_SIGNED, { role: 'skipper' }) trackPlausibleEvent(PlausibleEvents.LANGUAGE_CHANGED, { from: 'de', to: 'da' }) +trackPlausibleEvent(PlausibleEvents.LIVE_LOG_EVENT_LOGGED, { action: 'course' }) +trackPlausibleEvent(PlausibleEvents.NMEA_UPLOADED, { lines: 1200, candidates: 8, duplicate: false, has_position: true }) +trackPlausibleEvent(PlausibleEvents.NMEA_IMPORTED, { mode: 'both', events: 6, track: true }) ``` Lokal ohne Plausible-Script ist `trackPlausibleEvent` ein No-Op. In Production im Browser-Netzwerk-Tab auf Requests an die Plausible-Instanz prüfen.