From 60a8533a4426d4c980e03ab2e4598b6875bf1581 Mon Sep 17 00:00:00 2001 From: elpatron Date: Mon, 1 Jun 2026 11:34:21 +0200 Subject: [PATCH] feat: add Plausible events for live log photos and OWM usage Track Live Log Photo Uploaded and centralize OWM Weather Fetched with source props for live log and entry editor call sites. Co-authored-by: Cursor --- client/src/components/LiveLogView.tsx | 7 +-- client/src/components/LogEntryEditor.tsx | 8 +++- client/src/services/analytics.ts | 7 ++- client/src/services/photoAttachments.ts | 3 ++ client/src/services/weather.test.ts | 61 ++++++++++++++++++++++++ client/src/services/weather.ts | 24 ++++++++-- docs/plausible-events.md | 21 +++++++- 7 files changed, 118 insertions(+), 13 deletions(-) create mode 100644 client/src/services/weather.test.ts diff --git a/client/src/components/LiveLogView.tsx b/client/src/components/LiveLogView.tsx index 974da2e..af1a976 100644 --- a/client/src/components/LiveLogView.tsx +++ b/client/src/components/LiveLogView.tsx @@ -474,7 +474,10 @@ export default function LiveLogView({ try { let data: Record try { - data = await fetchOpenWeatherCurrent({ lat, lon: lng }) + data = await fetchOpenWeatherCurrent( + { lat, lon: lng }, + { analyticsSource: 'live_log' } + ) } catch (err) { if (err instanceof WeatherApiError && err.code === 'NO_KEY') { void showAlert(t('settings.no_key'), t('logs.live_weather_owm_btn')) @@ -515,7 +518,6 @@ export default function LiveLogView({ await appendQuickEvents(logbookId, id, partials) await refreshEntry(id) showUndo() - trackPlausibleEvent(PlausibleEvents.LIVE_LOG_EVENT_LOGGED, { action: 'weather_owm' }) } catch (err: unknown) { console.error('Live log OWM weather save failed:', err) setError(err instanceof Error ? err.message : t('logs.live_action_error')) @@ -575,7 +577,6 @@ export default function LiveLogView({ setModal('none') setPhotoCaption('') showUndo('photo') - trackPlausibleEvent(PlausibleEvents.LIVE_LOG_EVENT_LOGGED, { action: 'photo' }) } catch (err: unknown) { console.error('Live log photo save failed:', err) void showAlert( diff --git a/client/src/components/LogEntryEditor.tsx b/client/src/components/LogEntryEditor.tsx index 23f9905..639bf24 100644 --- a/client/src/components/LogEntryEditor.tsx +++ b/client/src/components/LogEntryEditor.tsx @@ -900,7 +900,10 @@ export default function LogEntryEditor({ } try { - const data = await fetchOpenWeatherCurrent({ q: locationQuery }) + const data = await fetchOpenWeatherCurrent( + { q: locationQuery }, + { analyticsSource: 'entry_editor_gps_lookup' } + ) const coord = data.coord as { lat?: number; lon?: number } | undefined if (coord?.lat !== undefined && coord?.lon !== undefined) { setEvGpsLat(Number(coord.lat).toFixed(6)) @@ -955,7 +958,8 @@ export default function LogEntryEditor({ const data = await fetchOpenWeatherCurrent( hasGps ? { lat: evGpsLat, lon: evGpsLng } - : { q: fallbackLocation } + : { q: fallbackLocation }, + { analyticsSource: 'entry_editor' } ) const coord = data.coord as { lat?: number; lon?: number } | undefined diff --git a/client/src/services/analytics.ts b/client/src/services/analytics.ts index ccc07a8..4464b60 100644 --- a/client/src/services/analytics.ts +++ b/client/src/services/analytics.ts @@ -39,9 +39,14 @@ export const PlausibleEvents = { NMEA_IMPORTED: 'NMEA Imported', NMEA_UPLOADED: 'NMEA Uploaded', LIVE_LOG_OPENED: 'Live Log Opened', - LIVE_LOG_EVENT_LOGGED: 'Live Log Event Logged' + LIVE_LOG_EVENT_LOGGED: 'Live Log Event Logged', + LIVE_LOG_PHOTO_UPLOADED: 'Live Log Photo Uploaded', + OWM_WEATHER_FETCHED: 'OWM Weather Fetched' } as const +/** Where a successful OpenWeatherMap API call originated (no coordinates or place names). */ +export type OwmAnalyticsSource = 'live_log' | 'entry_editor' | 'entry_editor_gps_lookup' + export type PlausibleEventName = (typeof PlausibleEvents)[keyof typeof PlausibleEvents] export type PlausibleEventProps = Record diff --git a/client/src/services/photoAttachments.ts b/client/src/services/photoAttachments.ts index fb04a9e..62c8115 100644 --- a/client/src/services/photoAttachments.ts +++ b/client/src/services/photoAttachments.ts @@ -55,6 +55,9 @@ export async function saveEntryPhoto(options: { }) trackPlausibleEvent(PlausibleEvents.PHOTO_UPLOADED, { context: analyticsContext }) + if (analyticsContext === 'live_log') { + trackPlausibleEvent(PlausibleEvents.LIVE_LOG_PHOTO_UPLOADED) + } syncLogbook(logbookId).catch((err) => console.warn('Background sync failed:', err)) return photoId } diff --git a/client/src/services/weather.test.ts b/client/src/services/weather.test.ts new file mode 100644 index 0000000..b765291 --- /dev/null +++ b/client/src/services/weather.test.ts @@ -0,0 +1,61 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { PlausibleEvents } from './analytics.js' + +const apiFetch = vi.fn() +const trackPlausibleEvent = vi.fn() + +vi.mock('./api.js', () => ({ apiFetch })) +vi.mock('./analytics.js', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + trackPlausibleEvent: (...args: unknown[]) => trackPlausibleEvent(...args) + } +}) +vi.mock('./userPreferences.js', () => ({ + getOwmApiKeyForActiveUser: () => '' +})) + +describe('fetchOpenWeatherCurrent', () => { + beforeEach(() => { + apiFetch.mockReset() + trackPlausibleEvent.mockReset() + }) + + afterEach(() => { + vi.unstubAllGlobals() + }) + + it('tracks OWM Weather Fetched on success when analyticsSource is set', async () => { + apiFetch.mockResolvedValue({ + ok: true, + status: 200, + json: async () => ({ coord: { lat: 54, lon: 10 }, main: { temp: 20 } }) + }) + + const { fetchOpenWeatherCurrent } = await import('./weather.js') + await fetchOpenWeatherCurrent( + { lat: '54.0', lon: '10.0' }, + { analyticsSource: 'live_log' } + ) + + expect(trackPlausibleEvent).toHaveBeenCalledWith(PlausibleEvents.OWM_WEATHER_FETCHED, { + source: 'live_log' + }) + }) + + it('does not track when the API request fails', async () => { + apiFetch.mockResolvedValue({ + ok: false, + status: 500, + json: async () => ({ error: 'fail' }) + }) + + const { fetchOpenWeatherCurrent, WeatherApiError } = await import('./weather.js') + await expect( + fetchOpenWeatherCurrent({ lat: '54', lon: '10' }, { analyticsSource: 'entry_editor' }) + ).rejects.toBeInstanceOf(WeatherApiError) + + expect(trackPlausibleEvent).not.toHaveBeenCalled() + }) +}) diff --git a/client/src/services/weather.ts b/client/src/services/weather.ts index 3854f60..5b0f5ad 100644 --- a/client/src/services/weather.ts +++ b/client/src/services/weather.ts @@ -1,5 +1,10 @@ import { apiFetch } from './api.js' import { getOwmApiKeyForActiveUser } from './userPreferences.js' +import { + type OwmAnalyticsSource, + PlausibleEvents, + trackPlausibleEvent +} from './analytics.js' export class WeatherApiError extends Error { code: 'NO_KEY' | 'REQUEST_FAILED' @@ -13,11 +18,14 @@ export class WeatherApiError extends Error { const OWM_FETCH_TIMEOUT_MS = 20_000 -export async function fetchOpenWeatherCurrent(params: { - lat?: string - lon?: string - q?: string -}): Promise> { +export async function fetchOpenWeatherCurrent( + params: { + lat?: string + lon?: string + q?: string + }, + options?: { analyticsSource: OwmAnalyticsSource } +): Promise> { const searchParams = new URLSearchParams() if (params.lat && params.lon) { @@ -59,5 +67,11 @@ export async function fetchOpenWeatherCurrent(params: { throw new WeatherApiError('Weather API rejected the request') } + if (options?.analyticsSource) { + trackPlausibleEvent(PlausibleEvents.OWM_WEATHER_FETCHED, { + source: options.analyticsSource + }) + } + return data } diff --git a/docs/plausible-events.md b/docs/plausible-events.md index be4d3df..de4ce82 100644 --- a/docs/plausible-events.md +++ b/docs/plausible-events.md @@ -36,7 +36,9 @@ Kapteins Daagbok nutzt [Plausible Analytics](https://plausible.io/) mit dem Scri | PDF Exported | PDF-Export eines Reisetags (`LogEntryEditor.tsx`, `LogEntriesList.tsx`) | `scope`: `entry` | | CSV Exported | CSV-Download aus der Eintragsliste (`LogEntriesList.tsx`) | — | | CSV Shared | CSV über Web Share API geteilt (`LogEntriesList.tsx`) | — | -| Photo Uploaded | Foto hochgeladen (`PhotoCapture.tsx`, `CrewForm.tsx`) | `context`: `logbook` \| `crew`, bei Crew zusätzlich `role`: `skipper` \| `crew` | +| Photo Uploaded | Foto hochgeladen (`photoAttachments.ts`, `PhotoCapture.tsx`, `CrewForm.tsx`) | `context`: `logbook` \| `live_log` \| `crew`, bei Crew zusätzlich `role`: `skipper` \| `crew` | +| Live Log Photo Uploaded | Foto im Live-Journal per Kamera gespeichert (`photoAttachments.ts`, `analyticsContext`: `live_log`) | — | +| OWM Weather Fetched | Erfolgreicher OpenWeatherMap-API-Abruf (`weather.ts`, zentral nach HTTP 200) | `source`: siehe [OWM-Quellen](#owm-quellen) | | Backup Exported | Backup-Datei heruntergeladen (`LogbookBackupPanel.tsx`) | `entries`, `photos` (Anzahlen, keine Inhalte) | | Backup Restored | Backup wiederhergestellt (`LogbookBackupPanel.tsx`) | `entries`, `photos`, `mode`: `same_id` \| `overwrite` \| `new_id` | | Push Enabled | Crew-Änderungs-Push aktiviert (`PushNotificationSettings.tsx`) | — | @@ -80,6 +82,18 @@ Property `action` bei **Live Log Event Logged** — stabile englische Schlüssel | `comment` | Kommentar | | `undo` | Letztes Ereignis rückgängig | +### OWM-Quellen + +Property `source` bei **OWM Weather Fetched** — ein Event pro erfolgreichem API-Call (keine Koordinaten, kein Ortsname): + +| `source` | Auslöser | +|----------|----------| +| `live_log` | OpenWeatherMap-Wetter im Live-Journal (`LiveLogView.tsx`) | +| `entry_editor` | Wetter-Button im Reisetag-Editor (`LogEntryEditor.tsx`, `handleFetchWeather`) | +| `entry_editor_gps_lookup` | GPS-Fallback per Ortsname im Reisetag-Editor (`LogEntryEditor.tsx`, `handleGetGps`) | + +Fehlgeschlagene Abrufe (kein API-Key, Timeout, leere Antwort) lösen **kein** Event aus. + ## Bewusst nicht getrackt - **Demo-Logbuch:** Beim automatischen Seed (`demoLogbook.ts`) werden keine Events ausgelöst — nur echte Nutzeraktionen zählen. @@ -106,7 +120,8 @@ Empfohlene Goal-Ketten für Auswertung (nur Business!): 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 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`) +10. **Live-Journal:** Live Log Opened → Live Log Event Logged (Verteilung `action`; z. B. `fix`, `course`, `motor_start`) → Live Log Photo Uploaded +11. **OpenWeatherMap:** OWM Weather Fetched (Verteilung `source`; Live-Journal vs. Reisetag-Editor) ## Entwicklung @@ -117,6 +132,8 @@ 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.LIVE_LOG_PHOTO_UPLOADED) +trackPlausibleEvent(PlausibleEvents.OWM_WEATHER_FETCHED, { source: 'live_log' }) trackPlausibleEvent(PlausibleEvents.NMEA_UPLOADED, { lines: 1200, candidates: 8, duplicate: false, has_position: true }) trackPlausibleEvent(PlausibleEvents.NMEA_IMPORTED, { mode: 'both', events: 6, track: true }) ```