Kvika Team·· 7 min read

Product Analytics Without New Database Tables

Focus hours, meeting load, task sources, and integration health — computed from CalendarEvent, Task, and CalendarSync rows you already have, with rule-based insights instead of a metrics warehouse.

analyticsengineeringproductivitysql

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

typescript
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.

Users trust "12 open tasks older than 14 days" more than opaque productivity scores. Explainable beats clever.

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.

Kvika Team

Kvika unifies your calendar, email, and tasks across Google and Microsoft. Join the beta at kvika.work/waitlist.