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 <cursoragent@cursor.com>
This commit is contained in:
@@ -474,7 +474,10 @@ export default function LiveLogView({
|
||||
try {
|
||||
let data: Record<string, unknown>
|
||||
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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<string, string | number | boolean>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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<typeof import('./analytics.js')>()
|
||||
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()
|
||||
})
|
||||
})
|
||||
@@ -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<Record<string, unknown>> {
|
||||
export async function fetchOpenWeatherCurrent(
|
||||
params: {
|
||||
lat?: string
|
||||
lon?: string
|
||||
q?: string
|
||||
},
|
||||
options?: { analyticsSource: OwmAnalyticsSource }
|
||||
): Promise<Record<string, unknown>> {
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user