First-party, consent-based funnel tracking: a cookie notice asks once, and only visitors who allow it get an anonymous visit id. Event page views and registration starts are tracked from the browser; submit and paid come from the registration. Adds the Registration funnel report (conversion per stage, daily trend, by source/device/ticket option), a conversion tile on the supervisor/admin dashboards, short-link source tagging, and a daily rollup of raw rows older than 90 days. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CP62L9JxHomA36aYP1ekCe
45 lines
2.1 KiB
JavaScript
45 lines
2.1 KiB
JavaScript
const prisma = require('../config/db');
|
|
const { safeErrorMessage } = require('../utils/errorUtils');
|
|
const { track, funnelFor } = require('../utils/analytics');
|
|
|
|
// @desc Record a funnel step (event page view, registration started)
|
|
// @route POST /api/analytics/track { sessionId, eventId, stage: 'view'|'start', source? }
|
|
// @access Public — only sent by browsers whose visitor accepted cookies
|
|
const trackStage = async (req, res) => {
|
|
try {
|
|
const { sessionId, eventId, stage, source } = req.body || {};
|
|
await track({ sessionId, eventId, stage, source, userAgent: req.headers['user-agent'] });
|
|
} catch (e) {
|
|
console.warn('[analytics] track failed:', e?.message || e);
|
|
}
|
|
// Always a quiet 204 — tracking must never get in a visitor's way.
|
|
res.status(204).end();
|
|
};
|
|
|
|
// @desc Registration funnel for one or more events
|
|
// @route GET /api/analytics/funnel?eventIds=a,b&from=YYYY-MM-DD&to=YYYY-MM-DD
|
|
// @access Private/Supervisor
|
|
const getFunnel = async (req, res) => {
|
|
try {
|
|
const eventIds = String(req.query.eventIds || '').split(',').map(s => s.trim()).filter(Boolean).slice(0, 50);
|
|
if (eventIds.length === 0) { res.status(400); throw new Error('Choose at least one event'); }
|
|
const events = await prisma.event.findMany({ where: { id: { in: eventIds } }, select: { id: true, title: true } });
|
|
const total = await funnelFor(eventIds, { from: req.query.from, to: req.query.to });
|
|
// Per event too, so events can be compared.
|
|
const perEvent = [];
|
|
if (events.length > 1) {
|
|
for (const ev of events) {
|
|
const f = await funnelFor([ev.id], { from: req.query.from, to: req.query.to });
|
|
perEvent.push({ eventId: ev.id, title: ev.title, totals: f.totals, conversion: f.conversion });
|
|
}
|
|
} else if (events[0]) {
|
|
perEvent.push({ eventId: events[0].id, title: events[0].title, totals: total.totals, conversion: total.conversion });
|
|
}
|
|
res.json({ ...total, perEvent });
|
|
} catch (error) {
|
|
res.status(res.statusCode === 200 ? 400 : res.statusCode).json({ message: safeErrorMessage(error) });
|
|
}
|
|
};
|
|
|
|
module.exports = { trackStage, getFunnel };
|