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:
@@ -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 (
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user