Users asked for a productivity dashboard — focus time vs meetings, tasks completed, where work comes from. We did not want a six-week analytics schema project blocking the feature. The data already existed in calendar events synced from Google and Microsoft, tasks from email AI and manual entry, and integration sync metadata.
getUserAnalytics runs parallel Prisma queries for the selected week or month, aggregates in TypeScript, and returns KPIs plus plain-English insights. No new tables. No nightly ETL. This post is what that compromise looks like.
Ship without a migration
Analytics payloads are computed at request time. Acceptable for beta scale; revisit caching when dashboard load becomes hot. The tradeoff: ship now, optimise when metrics prove useful.
Week vs month toggles use date-fns bounds with Monday week start. Each KPI includes a delta string comparing current period to the previous period — "+2.3h vs last week" — not just absolute numbers.
KPIs from existing rows
Focus hours: sum duration of events where eventType === FOCUS_TIME, clipped to the query window.
Meeting hours: same for MEETING.
Tasks completed: rows with completedAt in range, or legacy status === Done with updatedAt in range for older data.
Email-sourced tasks: percentage of completed tasks where source === EMAIL_AI.
Clipping event hours to windows
function sumEventHours(events, windowStart, windowEnd) {
return events.reduce((sum, event) => {
const start = event.startTime < windowStart ? windowStart : event.startTime
const end = event.endTime > windowEnd ? windowEnd : event.endTime
if (end <= start) return sum
return sum + (end - start) / (1000 * 60 * 60)
}, 0)
}Events spanning week boundaries contribute partial hours — not zero, not double-counted across periods. All-day events need separate handling; duration math on them is misleading.
Rule-based insights
We generate at most four insights from heuristics — no ML:
Best focus day in the period. Busiest meeting day. Stale open tasks (not updated in 7+ days). Integration sync errors from CalendarSync.lastSyncError.
Garbage in, garbage out
Analytics reads eventType assigned at calendar sync time via keyword heuristics and attendee checks. Misclassified meetings inflate focus hours or vice versa. Fixing classification improves dashboards and auto-scheduling simultaneously.
Meeting pattern charts bucket by startTime.getHours() in server timezone today — a known gap for global users. Integration health rows surface last successful sync and error strings as reconnect prompts, not empty charts.
Related: calendar sync and classification. Join the Kvika beta.