Adds the read-side Umami integration queued in last week's website-analytics plan (Phases 1–6 of `docs/website-analytics-flesh-out-plan.md`): - Realtime panel polls Umami at 5s intervals; world map renders visitor origins via echarts + `public/world-map/echarts-world.json` topo. - Sessions list + session-detail-sheet drill-down (per-session event timeline pulled from `/api/v1/website-analytics`). - Weekly heatmap (day-of-week × hour-of-day) for engagement timing. - Metric-detail pages under `/[portSlug]/website-analytics/[metric]` for pageviews / referrers / events deep-dives. - Email-pixel write path: `/api/public/email-pixel/[sendId]` 1×1 GIF beacon backed by `email_open_tracking` (migration 0076); resolves inline on render in inbox. - Tracked-link redirect: `/q/[slug]` routes through `tracked_links` (migration 0077) and forwards to the canonical destination after logging the click. - Dashboard `website-glance-tile` now reads from the live Umami service instead of placeholder data. Deps: `@umami/node`, `echarts`, `echarts-for-react`, `@types/geojson`, `@types/topojson-client`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
12 KiB
Umami v2 / v3 API capabilities — reference for flesh-out planning
Verified against: analytics.portnimara.com (Umami v3.1.0), 2026-05-19.
Auth: username/password → JWT via POST /api/auth/login, Bearer on every request, 1h TTL (we cache 55min).
Companion code: src/lib/services/umami.service.ts (currently wraps stats/pageviews/metrics/active).
Endpoints below are listed by topic area, with what we currently use, what's available but unused, and where it could plug into the CRM.
1. Stats & traffic snapshots — /api/websites/:id/stats
Currently used. Returns the flat aggregate over the requested window plus a comparison block for the prior window of equal length.
{
"pageviews": 2081, "visitors": 726, "visits": 872,
"bounces": 457, "totaltime": 109519,
"comparison": { "pageviews": 1935, "visitors": 642, ... }
}
Unused fields we could surface:
totaltime— total seconds on site → derive avg session time (totaltime / visits).bounces / visits→ bounce-rate KPI.- Period-over-period deltas (already wired for trend arrows, but the full comparison object has more we could use for a "what changed since last period" panel).
Filters supported (per Umami docs, mostly untested by us): url, referrer, title, query, event, host, os, browser, device, country, region, city — meaning every stats call can be sliced. Big unlock: show stats for a specific landing-page URL on the berth detail (e.g. /berths/A12 stats), or filter by referrer to see which channels drove signed EOIs.
2. Time-series — /api/websites/:id/pageviews
Currently used for the trend chart. Returns {pageviews: [{x, y}], sessions?: [{x, y}]} (sessions only when compare is requested).
Parameters: startAt, endAt, unit (year|month|day|hour), timezone, compare (untapped), filters (untapped).
Unused: compare=prev gives the same series for the previous period — could power a dual-line "vs last period" overlay on the chart.
3. Top-N metrics — /api/websites/:id/metrics
Currently used for Top Pages / Referrers / Countries (limit 10). Returns [{x, y}].
Available type values (we surface 4, Umami offers 17):
| Type | What it returns | CRM use case |
|---|---|---|
path |
Top URLs | ✅ Already shown (we mis-typed as url, now fixed) |
referrer |
Top referring sites | ✅ Already shown |
country |
Visitors by country | ✅ Already shown |
browser / os / device |
Tech breakdown | Not surfaced — useful for "is mobile traffic converting?" |
region / city |
Geographic drill-down | Strong fit for marina marketing |
language |
Visitor browser language | Could feed i18n decisions |
screen |
Resolution | Low value |
event |
Top custom events | Big unlock — see §6 below |
tag |
Event tags | Same |
query |
Top URL query strings | UTM-debug surface |
entry / exit |
First/last page in session | Funnel analysis |
title |
Top page titles (vs paths) | Better labels for non-slug URLs |
hostname |
Multi-domain sites | Probably N/A |
distinctId |
Custom user identifiers | If we ever pipe CRM user IDs into Umami |
4. Live visitors — /api/websites/:id/active
Currently used for the green-dot "N active right now" indicator. Returns {visitors: number} (last-5-min count).
Alternative for richer realtime: /api/realtime/:websiteId (live realtime feed) returns far more — current top URLs being viewed, current top countries, recent event stream, a 30-minute time-series, totals, plus a timestamp you can poll against. We could surface a "live" panel on the dashboard showing the most-viewed pages right now.
5. Sessions API — /api/websites/:id/sessions/*
Not currently used. Multiple endpoints worth integrating:
GET /sessions— list every session in a range with full device/geo/visits/views columns. Pageable. Could power a "recent visitors" surface — see who's browsing the berth detail pages right now.GET /sessions/stats— summary aggregate (pageviews, visitors, visits, countries, events) keyed by session.GET /sessions/:sessionId— drill into a single session: device, OS, browser, country, subdivision, city, screen, language, firstAt, lastAt, visits, views, events, totaltime.GET /sessions/:sessionId/activity— full event timeline for one session (urlPath, eventName, referrerDomain, timestamps).GET /sessions/:sessionId/properties— custom session properties (email, name, etc. — if Umami'sidentify()is called from the marketing site).GET /session-data/properties+/session-data/values— aggregate custom session properties.GET /sessions/weekly— heatmap of session count by hour-of-week. Direct fit for an "engagement heatmap" widget.
Big unlock: if marketing site calls umami.identify({email}) after EOI form submit, sessions can be linked back to a specific client. We could then show "this client's website journey" on their CRM detail page.
6. Events API — /api/websites/:id/events/*
Not currently used. Umami auto-tracks pageviews; custom events are fired explicitly (e.g. button clicks, form submits, video plays). Endpoints:
GET /events— list custom events in a range.GET /events/stats— totals.GET /events/series— time-series per event.GET /event-data/*— aggregate over event payload properties.
High-leverage CRM use cases:
- Fire an event on the marketing site when someone clicks "Inquire about berth A12" → CRM Activity feed shows it in real-time on the inquiry record.
- Fire an event when someone downloads a brochure → see which brochures convert.
- Fire an event on EOI form-step completions → drop-off funnel analysis.
We'd need to add umami.track('event-name', {payload}) calls on the marketing site (~1-2h work there) and a new admin surface to define/view these events.
7. Reports API — /api/reports/*
Not currently used. Umami's "saved reports" system. Endpoints:
GET /reports+GET /reports/:id— list / retrieve saved reports.POST /reports/insights— slice-and-dice with arbitrary filters/dimensions.POST /reports/funnel— multi-step conversion analysis.POST /reports/retention— cohort retention over time.POST /reports/utm— UTM-tagged campaign performance.POST /reports/journey— most common navigation paths.POST /reports/goals— pageview/event-goal completion tracking.POST /reports/revenue— revenue attribution (if we firepurchaseevents with amount).POST /reports/attribution— first/last-click attribution modelling.
Best fits for the CRM:
- Funnel report for the EOI flow:
/berths → /berths/A12 → /inquire?berth=A12 → form submit → CRM EOI signed. Surface drop-off percentages on the Pulse-style dashboard. - Journey report to see "what paths do visitors take before signing an EOI?" — informs marketing-site IA.
- UTM report to plumb campaign attribution into the lead-source breakdown (currently CRM-side; could be cross-validated against marketing's UTM-tagged traffic).
- Attribution report to give Pipeline-by-Source a "first-click vs last-click" toggle.
8. Send events from CRM → Umami — /api/send
Not currently used. The collect endpoint accepts page hits + custom events from any client. CRM doesn't currently push events, but we could:
- Fire
umami.track('signed-eoi', {berth: 'A12', deal_value: 50000})from the CRM after EOI completion — closes the loop between marketing-site funnel and CRM outcome. - Fire
umami.track('contract-signed'),umami.track('deposit-received')— full funnel visible in Umami without leaving it.
9. Multi-website + team admin — /api/websites, /api/teams, /api/users
Not currently used. We hard-code a single umami_website_id per port. Useful if a port runs multiple sites (e.g. main marina + residential subdomain): admin UI could list-and-pick from the configured Umami instance's websites instead of requiring manual ID copy-paste. Same for team membership.
Prioritized opportunity list
Ranked by leverage-vs-effort, assuming the v3.1.0 fix in this commit is the baseline:
- Avg session time + bounce rate KPI tiles (~20 min) — already in the
/statsresponse, just need new tiles. compare=prevoverlay on the pageviews trend chart (~30 min) — dual-line "vs last period" surface.- Country choropleth heatmap (~4-6h) — already queued in Bucket 3 of the UAT findings doc as "World-map heatmap of Umami visitor origins."
- Surface top browsers / OS / devices (~30 min) — additional
TopListcolumns; pure UI work. - Fire CRM-side events back into Umami (~2-3h marketing-site + CRM hook) — closes the funnel between marketing and outcomes.
- EOI funnel via
/api/reports/funnel(~3-4h) — drop-off analysis from berth view → inquiry → signed EOI. - Identify visitors → link sessions to clients (~4-6h spread across marketing site + CRM detail surfaces) — biggest unlock but needs marketing-site changes.
- Sessions-list "recent visitors" panel (~2-3h) — see who's browsing right now, drill into individual sessions.
- Saved-reports admin surface (~6-10h) — let admins create + share Umami reports without leaving the CRM. Bigger product surface; defer until #1-#5 land.
Service-layer additions needed to support the above
src/lib/services/umami.service.ts currently exports: getStats, getPageviewsSeries, getMetric, getActiveVisitors, testConnection. To unlock the opportunities above, add:
getSessions(portId, range, opts)→/sessions(paged)getSession(portId, sessionId)→ single-session drill-ingetSessionActivity(portId, sessionId, range)→ event timelinegetSessionsWeekly(portId, range)→ heatmap sourcegetEvents(portId, range)+getEventsStats(portId, range)+getEventsSeries(portId, range, eventName, unit)→ custom eventsgetRealtime(portId, range)→/api/realtime/:idfor the live panelgetReport(portId, reportType, body)→ POST wrappers for funnel/retention/journey/utm/goals/revenue/attributiontrackEvent(portId, name, payload)→ POST to/api/sendfor CRM → Umami event emission
Each is a thin wrapper around the existing umamiFetch (or a new umamiPost variant for the reports endpoints). The auth + JWT cache + retry logic already in place handles them all.
Known gotchas (verified against v3.1.0)
- Metric
type=urlreturns 400 — usetype=path(handled in our code via back-compat alias). /api/websites/:id/pageviewsreturnssessionsonly whencompareis in the query string — keep.sessionsoptional in TS types.- Stats response is flat (
pageviews: number), not nested (pageviews: {value, prev}). The v1 nested shape isn't in v2/v3. /api/auth/loginreturns a JWT with noexpires_infield — we assume 1h and refresh proactively at 55min.- Visiting
/apiin a browser returns nothing — base path has no GET handler. Use/api/heartbeatto check liveness. - Filters are passed as query params (e.g.
&country=DE), NOT as a JSONfiltersbody, per actual API behaviour (docs occasionally show JSON which doesn't work for GET endpoints).