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 <cursoragent@cursor.com>
This commit is contained in:
@@ -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<void>,
|
||||
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:
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user