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>
This commit is contained in:
@@ -7,7 +7,13 @@ import {
|
||||
getActiveVisitors,
|
||||
getMetric,
|
||||
getPageviewsSeries,
|
||||
getRealtime,
|
||||
getSession,
|
||||
getSessionActivity,
|
||||
getSessions,
|
||||
getSessionsWeekly,
|
||||
getStats,
|
||||
getWebsiteInfo,
|
||||
type UmamiMetricType,
|
||||
} from '@/lib/services/umami.service';
|
||||
|
||||
@@ -31,7 +37,11 @@ import {
|
||||
*/
|
||||
|
||||
const ISO_DATE_RX = /^\d{4}-\d{2}-\d{2}$/;
|
||||
const TOP_METRIC_RX = /^top-(url|referrer|country|browser|os|device|event)$/;
|
||||
// Umami v2/v3 metric `type` values surfaced by the CRM. `path` is the
|
||||
// current name for what older versions called `url` — accept both as
|
||||
// inbound metric names (old clients won't break) but `path` is what the
|
||||
// service forwards to Umami.
|
||||
const TOP_METRIC_RX = /^top-(path|url|referrer|country|browser|os|device|event)$/;
|
||||
|
||||
function parseRange(req: NextRequest): DateRange | { error: string } {
|
||||
const url = new URL(req.url);
|
||||
@@ -88,8 +98,30 @@ export const GET = withAuth(
|
||||
data = await getPageviewsSeries(ctx.portId, range);
|
||||
} else if (metric === 'active') {
|
||||
data = await getActiveVisitors(ctx.portId);
|
||||
} else if (metric === 'realtime') {
|
||||
data = await getRealtime(ctx.portId);
|
||||
} else if (metric === 'website') {
|
||||
data = await getWebsiteInfo(ctx.portId);
|
||||
} else if (metric === 'sessions') {
|
||||
const page = Number(url.searchParams.get('page') ?? 1);
|
||||
const pageSize = Number(url.searchParams.get('pageSize') ?? 25);
|
||||
const query = url.searchParams.get('query') ?? undefined;
|
||||
data = await getSessions(ctx.portId, range, { page, pageSize, query });
|
||||
} else if (metric === 'session') {
|
||||
const sessionId = url.searchParams.get('sessionId');
|
||||
if (!sessionId) throw new ValidationError('Missing sessionId');
|
||||
data = await getSession(ctx.portId, sessionId);
|
||||
} else if (metric === 'session-activity') {
|
||||
const sessionId = url.searchParams.get('sessionId');
|
||||
if (!sessionId) throw new ValidationError('Missing sessionId');
|
||||
data = await getSessionActivity(ctx.portId, sessionId, range);
|
||||
} else if (metric === 'sessions-weekly') {
|
||||
data = await getSessionsWeekly(ctx.portId, range);
|
||||
} else if (TOP_METRIC_RX.test(metric)) {
|
||||
const type = metric.replace(/^top-/, '') as UmamiMetricType;
|
||||
const raw = metric.replace(/^top-/, '');
|
||||
// Legacy alias — older callers still send `top-url`; map to the
|
||||
// Umami v3 enum name to keep them working post-rewrite.
|
||||
const type = (raw === 'url' ? 'path' : raw) as UmamiMetricType;
|
||||
const limit = Number(url.searchParams.get('limit') ?? 10);
|
||||
data = await getMetric(ctx.portId, range, type, limit);
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user