feat(analytics): Plausible-Events für Profilseite

Trackt Profilaufruf, Passkey-/PIN-Aktionen und Gerät vergessen;
Dokumentation in docs/plausible-events.md ergänzt.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-31 09:47:12 +02:00
co-authored by Cursor
parent d4538ec06e
commit 3698c6fbca
3 changed files with 35 additions and 1 deletions
+15
View File
@@ -50,6 +50,7 @@ import {
type AccountStatsSummary type AccountStatsSummary
} from '../services/statsAggregation.js' } from '../services/statsAggregation.js'
import { db } from '../services/db.js' import { db } from '../services/db.js'
import { PlausibleEvents, trackPlausibleEvent } from '../services/analytics.js'
interface UserProfilePageProps { interface UserProfilePageProps {
onBack: () => void onBack: () => void
@@ -154,6 +155,10 @@ export default function UserProfilePage({ onBack, onLogout }: UserProfilePagePro
void loadData() void loadData()
}, [loadData]) }, [loadData])
useEffect(() => {
trackPlausibleEvent(PlausibleEvents.PROFILE_OPENED)
}, [])
useEffect(() => { useEffect(() => {
const handleOnline = () => setOnline(true) const handleOnline = () => setOnline(true)
const handleOffline = () => setOnline(false) const handleOffline = () => setOnline(false)
@@ -198,9 +203,11 @@ export default function UserProfilePage({ onBack, onLogout }: UserProfilePagePro
setPasskeyBusy(true) setPasskeyBusy(true)
setError(null) setError(null)
try { try {
const hadLabel = Boolean(newPasskeyLabel.trim())
await addPasskey(newPasskeyLabel) await addPasskey(newPasskeyLabel)
setNewPasskeyLabel('') setNewPasskeyLabel('')
await loadData() await loadData()
trackPlausibleEvent(PlausibleEvents.PASSKEY_ADDED, { labeled: hadLabel })
showAlert(t('profile.add_passkey_success')) showAlert(t('profile.add_passkey_success'))
} catch (err: unknown) { } catch (err: unknown) {
setError(err instanceof Error ? err.message : t('profile.add_passkey_failed')) setError(err instanceof Error ? err.message : t('profile.add_passkey_failed'))
@@ -215,6 +222,7 @@ export default function UserProfilePage({ onBack, onLogout }: UserProfilePagePro
try { try {
await renamePasskey(credentialId, passkeyLabels[credentialId] ?? '') await renamePasskey(credentialId, passkeyLabels[credentialId] ?? '')
await loadData() await loadData()
trackPlausibleEvent(PlausibleEvents.PASSKEY_RENAMED)
showAlert(t('profile.passkey_rename_success')) showAlert(t('profile.passkey_rename_success'))
} catch (err: unknown) { } catch (err: unknown) {
setError(err instanceof Error ? err.message : t('profile.add_passkey_failed')) setError(err instanceof Error ? err.message : t('profile.add_passkey_failed'))
@@ -234,10 +242,12 @@ export default function UserProfilePage({ onBack, onLogout }: UserProfilePagePro
forgetUsername(username) forgetUsername(username)
setIsKnownDevice(false) setIsKnownDevice(false)
trackPlausibleEvent(PlausibleEvents.DEVICE_FORGOTTEN)
} }
const handleRemovePasskey = async (credentialId: string) => { const handleRemovePasskey = async (credentialId: string) => {
if (profile && profile.credentials.length <= 1) { if (profile && profile.credentials.length <= 1) {
trackPlausibleEvent(PlausibleEvents.LAST_PASSKEY_REMOVE_HINTED)
await showAlert( await showAlert(
t('profile.remove_passkey_last_desc'), t('profile.remove_passkey_last_desc'),
t('profile.remove_passkey_last_title') t('profile.remove_passkey_last_title')
@@ -258,6 +268,7 @@ export default function UserProfilePage({ onBack, onLogout }: UserProfilePagePro
try { try {
await removePasskey(credentialId) await removePasskey(credentialId)
await loadData() await loadData()
trackPlausibleEvent(PlausibleEvents.PASSKEY_REMOVED)
} catch (err: unknown) { } catch (err: unknown) {
setError(err instanceof Error ? err.message : t('profile.remove_passkey_failed')) setError(err instanceof Error ? err.message : t('profile.remove_passkey_failed'))
} finally { } finally {
@@ -282,6 +293,8 @@ export default function UserProfilePage({ onBack, onLogout }: UserProfilePagePro
return return
} }
const pinAction = pinActive ? 'change' : 'set'
setPinBusy(true) setPinBusy(true)
setError(null) setError(null)
try { try {
@@ -289,6 +302,7 @@ export default function UserProfilePage({ onBack, onLogout }: UserProfilePagePro
setPinActive(true) setPinActive(true)
setPinInput('') setPinInput('')
setPinConfirm('') setPinConfirm('')
trackPlausibleEvent(PlausibleEvents.LOCAL_PIN_SET, { action: pinAction })
showAlert(t('profile.pin_saved')) showAlert(t('profile.pin_saved'))
} catch (err: unknown) { } catch (err: unknown) {
setError(err instanceof Error ? err.message : t('profile.pin_save_failed')) setError(err instanceof Error ? err.message : t('profile.pin_save_failed'))
@@ -310,6 +324,7 @@ export default function UserProfilePage({ onBack, onLogout }: UserProfilePagePro
setPinActive(false) setPinActive(false)
setPinInput('') setPinInput('')
setPinConfirm('') setPinConfirm('')
trackPlausibleEvent(PlausibleEvents.LOCAL_PIN_REMOVED)
} }
return ( return (
+9 -1
View File
@@ -25,7 +25,15 @@ export const PlausibleEvents = {
DEMO_OPENED: 'Demo Opened', DEMO_OPENED: 'Demo Opened',
PUSH_ENABLED: 'Push Enabled', PUSH_ENABLED: 'Push Enabled',
PUSH_DISABLED: 'Push Disabled', PUSH_DISABLED: 'Push Disabled',
FOOTER_LINK_CLICKED: 'Footer Link Clicked' FOOTER_LINK_CLICKED: 'Footer Link Clicked',
PROFILE_OPENED: 'Profile Opened',
PASSKEY_ADDED: 'Passkey Added',
PASSKEY_REMOVED: 'Passkey Removed',
PASSKEY_RENAMED: 'Passkey Renamed',
LAST_PASSKEY_REMOVE_HINTED: 'Last Passkey Remove Hinted',
LOCAL_PIN_SET: 'Local PIN Set',
LOCAL_PIN_REMOVED: 'Local PIN Removed',
DEVICE_FORGOTTEN: 'Device Forgotten'
} as const } as const
export type PlausibleEventName = (typeof PlausibleEvents)[keyof typeof PlausibleEvents] export type PlausibleEventName = (typeof PlausibleEvents)[keyof typeof PlausibleEvents]
+11
View File
@@ -40,12 +40,22 @@ Kapteins Daagbok nutzt [Plausible Analytics](https://plausible.io/) mit dem Scri
| Push Enabled | Crew-Änderungs-Push aktiviert (`PushNotificationSettings.tsx`) | — | | Push Enabled | Crew-Änderungs-Push aktiviert (`PushNotificationSettings.tsx`) | — |
| Push Disabled | Crew-Änderungs-Push deaktiviert (`PushNotificationSettings.tsx`) | — | | Push Disabled | Crew-Änderungs-Push deaktiviert (`PushNotificationSettings.tsx`) | — |
| Footer Link Clicked | Klick auf Autoren-Link im App-Footer (`AppFooter.tsx`) | — | | Footer Link Clicked | Klick auf Autoren-Link im App-Footer (`AppFooter.tsx`) | — |
| Profile Opened | Profilseite geöffnet (`UserProfilePage.tsx`, einmal pro Mount) | — |
| Passkey Added | Passkey erfolgreich registriert (`UserProfilePage.tsx`) | `labeled`: `true` \| `false` (optionaler Name gesetzt) |
| Passkey Removed | Passkey entfernt, mindestens ein Key verbleibt (`UserProfilePage.tsx`) | — |
| Passkey Renamed | Passkey-Name gespeichert (`UserProfilePage.tsx`) | — |
| Last Passkey Remove Hinted | Löschen des einzigen Passkeys abgebrochen — Hinweisdialog zur Kontolöschung (`UserProfilePage.tsx`) | — |
| Local PIN Set | Lokaler PIN gesetzt oder geändert (`UserProfilePage.tsx`) | `action`: `set` \| `change` |
| Local PIN Removed | Lokaler PIN entfernt (`UserProfilePage.tsx`) | — |
| Device Forgotten | Account aus Schnell-Login-Liste dieses Geräts entfernt (`UserProfilePage.tsx`) | — |
## Bewusst nicht getrackt ## Bewusst nicht getrackt
- **Demo-Logbuch:** Beim automatischen Seed (`demoLogbook.ts`) werden keine Events ausgelöst — nur echte Nutzeraktionen zählen. - **Demo-Logbuch:** Beim automatischen Seed (`demoLogbook.ts`) werden keine Events ausgelöst — nur echte Nutzeraktionen zählen.
- **Manuelle Signaturen:** Nur Passkey-Signaturen lösen `Entry Signed` aus. - **Manuelle Signaturen:** Nur Passkey-Signaturen lösen `Entry Signed` aus.
- **PII:** Keine Inhalte aus verschlüsselten Logbüchern in Properties. - **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).
- **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) ## Typische Funnels (Plausible Goals)
@@ -57,6 +67,7 @@ Empfohlene Goal-Ketten für Auswertung:
4. **Öffentliche Freigabe:** Logbook Shared → Public Link Opened 4. **Öffentliche Freigabe:** Logbook Shared → Public Link Opened
5. **Export:** Travel Day Saved → PDF Exported / CSV Exported 5. **Export:** Travel Day Saved → PDF Exported / CSV Exported
6. **Datensicherung:** Backup Exported → Backup Restored 6. **Datensicherung:** Backup Exported → Backup Restored
7. **Kontosicherheit:** Profile Opened → Passkey Added / Local PIN Set; Last Passkey Remove Hinted → Account Deleted (selten, aber aussagekräftig)
## Entwicklung ## Entwicklung