Files
pn-new-crm/docs/umami-api-capabilities.md
Matt bac253b360 feat(analytics): Umami website-analytics suite — world map, realtime, sessions, heatmap, pixel tracking, tracked links
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>
2026-05-20 15:53:41 +02:00

190 lines
12 KiB
Markdown

# 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.
```json
{
"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's `identify()` 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 fire `purchase` events 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:
1. **Avg session time + bounce rate KPI tiles** (~20 min) — already in the `/stats` response, just need new tiles.
2. **`compare=prev` overlay on the pageviews trend chart** (~30 min) — dual-line "vs last period" surface.
3. **Country choropleth heatmap** (~4-6h) — already queued in Bucket 3 of the UAT findings doc as "World-map heatmap of Umami visitor origins."
4. **Surface top browsers / OS / devices** (~30 min) — additional `TopList` columns; pure UI work.
5. **Fire CRM-side events back into Umami** (~2-3h marketing-site + CRM hook) — closes the funnel between marketing and outcomes.
6. **EOI funnel via `/api/reports/funnel`** (~3-4h) — drop-off analysis from berth view → inquiry → signed EOI.
7. **Identify visitors → link sessions to clients** (~4-6h spread across marketing site + CRM detail surfaces) — biggest unlock but needs marketing-site changes.
8. **Sessions-list "recent visitors" panel** (~2-3h) — see who's browsing right now, drill into individual sessions.
9. **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-in
- `getSessionActivity(portId, sessionId, range)` → event timeline
- `getSessionsWeekly(portId, range)` → heatmap source
- `getEvents(portId, range)` + `getEventsStats(portId, range)` + `getEventsSeries(portId, range, eventName, unit)` → custom events
- `getRealtime(portId, range)``/api/realtime/:id` for the live panel
- `getReport(portId, reportType, body)` → POST wrappers for funnel/retention/journey/utm/goals/revenue/attribution
- `trackEvent(portId, name, payload)` → POST to `/api/send` for 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=url` returns 400 — use `type=path` (handled in our code via back-compat alias).
- `/api/websites/:id/pageviews` returns `sessions` only when `compare` is in the query string — keep `.sessions` optional 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/login` returns a JWT with no `expires_in` field — we assume 1h and refresh proactively at 55min.
- Visiting `/api` in a browser returns nothing — base path has no GET handler. Use `/api/heartbeat` to check liveness.
- Filters are passed as query params (e.g. `&country=DE`), NOT as a JSON `filters` body, per actual API behaviour (docs occasionally show JSON which doesn't work for GET endpoints).