A full analytics panel mounted as a tab on the Sales Pipeline (Pipeline / Analytics / Leaderboard). It reads the durable lifecycle event log — one row per lead created, stage move, won/lost, project complete — and derives every number by subtracting stored timestamps. KPI cards, a pipeline-by-stage bar, stage dwell times, an SOP scorecard, a daily throughput area chart, an activity heatmap, an owner leaderboard and a group-compare panel all run off one request, so a filter applied at the top changes every chart below it.
Also called: sales reports · lead analytics · pipeline analytics · conversion reporting · where are my deals getting stuck
- 1Every stage move, lead creation, won/lost flip and project completion writes an append-only row to stage_transition_log with the real event time, the owner snapshot, the stage label and the seconds spent in the stage just left.
- 2The Analytics tab POSTs a date range, pipeline ('sales' or 'project') and an optional set of rep ids to /api/analytics.
- 3The route pages the log (1000 rows a page, up to 50 pages), groups rows by opportunity into per-lead timelines, and computes averages, medians and 90th percentiles in TypeScript — there is no database report to maintain.
- 4The response is one fixed payload (overview, funnel, stageDurations, leaderboard, timeOfDay, throughput, sop) that the panel renders section by section.
- 5Range presets (7/30/90 days, all time, custom) and the rep/role filter re-issue the same request, so nothing on the page can be looking at a different window than anything else.
Because durations kept being stored as numbers and then going stale or wrong. Migration 096's header sets the rule: 'store only TIMESTAMPS (one row per key event…). Every duration/metric is DERIVED later by subtraction.' It also explains why the log denormalises the owner, stage label and contact onto each row — 'so a row stays meaningful even if the opportunity is later edited, re-linked, or deleted.' The route header adds the second rule: 'ALL math is done in TypeScript by subtraction over the stored timestamps — there is no in-DB RPC', and 'Never throws to the client — any error returns a graceful 500', because a broken report should not take the pipeline down with it.
- Owner cannot answer 'how many leads became jobs last quarter' without manual counting.
- Stage durations were never stored, so 'how long does an estimate sit' had no answer.
- Reports drifted from reality when a deal was reassigned or a contact renamed.
See it on your own jobs
Twenty minutes, your numbers, no slide deck. We’ll build one of your real buildings in front of you and send you the estimate link at the end — yours to keep either way.
or keep browsing features →