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 }) ```