DEV Community

Indra Gunanda
Indra Gunanda

Posted on Originally published at zettacrm.com

Building Real-Time Analytics for a WhatsApp Native CRM

Building Real-Time Analytics for a WhatsApp Native CRM

Every CRM vendor ships a table of numbers. Very few ship analytics that a team actually acts on. When we were building the analytics layer inside Zetta CRM, the numbers were never the hard part — the hard part was deciding which numbers matter and getting them to the screen fast enough that a support lead can change their routing mid-shift, not report on last week's problems.

This is the story of that layer: the event pipeline, the metric definitions that survived contact with real teams, and the operational decisions we'd make differently next time.

Why WhatsApp Analytics Is Different

If you've ever built analytics for an email-based product, you're used to a comfortable rhythm: open a message, leave it in the inbox, process it hours later. Timestamps are forgiving. Peaks and valleys are predictable.

WhatsApp is real-time in a way email never is. A customer waits 40 seconds, not 40 hours, before deciding you're unresponsive. That changes the analytical contract:

  1. Latency is a first-class metric. Email tools can report on "median time to first response" as a weekly average. WhatsApp teams optimize p95 now, in the middle of the day.
  2. Conversations are bursty and multi-modal. A single interaction is a photo, a voice note, three short texts, and a document — not a clean thread of one message per event.
  3. Groups warp every metric. One active reseller group can produce hundreds of messages that are not "leads" and should not pollute first-response stats.

So the first decision was: analytics is not a reporting tab bolted onto the CRUD layer. It's a separate pipeline with its own shape. Here is what we built.

The Event Pipeline: One Shape for Everything

Early on we made a choice that paid off repeatedly: every meaningful thing that happens in the product emits a strongly-typed event. A message arrived, a label was applied, a handoff triggered, an agent picked up a conversation, an AI auto-replied. No ad-hoc database polling, no "just query the messages table for this dashboard."

WhatsApp Gateway ──► Message Processor ──► normalized Event ──► event store
                                                                    │
                        Team Inbox actions ──► Event                 │
                                                                    ▼
                                                          stream consumers
                                          (rollups, anomaly checks, caches)
Enter fullscreen mode Exit fullscreen mode

Each event carries a conversation_id, contact_id, number_id, agent_id (nullable — AI events carry their own actor type), a channel_context flag for is_group, and a precise timestamp.

{
  "event": "message.arrived",
  "ts": "2026-08-19T09:14:03Z",
  "conversation_id": "conv_9021",
  "contact_id": "c_441",
  "number_id": "n_2",
  "is_group": false,
  "is_ai": false,
  "media_types": ["image"],
  "direction": "inbound"
}
Enter fullscreen mode Exit fullscreen mode

Why typed events instead of tables? Because the same event feeds three very different consumers — live dashboards, weekly rollups, and the AI agent's context — and giving each one a cleaned, normalized event beats making them all parse raw chat rows. It also means we can add a new metric later without touching the ingestion code that teams already depend on.

Defining the Metrics That Actually Matter

We shipped an initial dashboard crammed with vanity metrics and watched nobody open it. The reset came when we asked support leads one question: what would you change at noon if you saw this number?

That produced a short, opinionated list — and it's still what the dashboard shows today:

  • First response time (median, p95, p99). The single metric that decides whether customers feel ignored. Segmented by number and time-of-day.
  • AI deflection. What fraction of conversations reached a resolution without a human touching them. Too high for you? Your AI may be too eager. Too low? Your knowledge base is too thin.
  • Agent load. Live count of active conversations per agent, feeding routing so a free teammate picks up the next assignment.
  • Resolution rate. Tracking a conversation to an actual outcome, not just "someone replied."
  • Peak hours. When the queue overflows so a lead can staff the afternoon accordingly.

Every metric lives behind a group-aware flag. Messages inside group chats are excluded from first-response and deflection stats by default, because they follow a totally different rhythm. That one flag stopped us from publishing numbers that scare teams for the wrong reasons.

Going Real-Time Without Rebuilding the World

The naive approach to live dashboards is in-memory state on an app server — which dies on redeploy, and drifts on multi-instance. The corporate approach is a full streaming platform like Kafka, which is overkill when you need thousands of concurrent connections, not millions.

We landed on a deliberately boring stack:

  • A lightweight event store (append-only, partitioned by number) as the source of truth for rollups.
  • An in-memory rolling window per number for the live dashboard, recomputed from recent events, seeded from the store on server start.
  • A WebSocket push layer to the browser for sub-second updates.

The trade-off we accepted: the live window is eventually consistent — a small window of data can be ephemeral if a server dies. That's fine for "what's happening right now," where being 30 seconds stale is acceptable. The durable rollups, which power weekly reports and historical charts, are recomputed from the event store in a background job, so they never lose a message.

If we ever outgrow the in-memory window, the upgrade path is explicit: replay the event store into a real stream. Because ingestion was event-first from day one, that migration doesn't invalidate the dashboards — it just changes their backend.

What We Got Wrong

Three mistakes are worth writing down, because each cost us a redesign that a bit more thought could have avoided.

Mistake 1: vanilla analytics for everyone. We rolled out one dashboard for all roles. Admins wanted strategic views; agents wanted "is my queue drowning." Same screen served nobody. The fix was role-scoped dashboards — a small change that dramatically lifted usage.

Mistake 2: no timezone awareness. A business that serves customers across Indonesia on one number was bucketing "peak hours" by server timezone, so the busiest period looked like 03:00 AM. All analytics now respect the workspace's configured timezone, and peak-hour analysis is done per number, not globally.

Mistake 3: single-agent routing assumed for agent-load. Load tracking assumed one conversation = one person's queue. Teams with shared pools and hot-handoff workflows broke that assumption. The metric now reports active conversations per team, with an agent-level breakdown as the secondary view.

What the Analytics Feed Back Into

The best part of building analytics into a WhatsApp-native CRM is that the output doesn't just sit on a dashboard — it drives product behavior:

  • Routing. Live agent load feeds the assignment router, so new conversations go to the least-loaded teammate instead of pinging everyone.
  • AI tuning. Deflection and handoff rates tell us whether the Hallo Zetta agent's knowledge base is underpowered — without waiting for complaints.
  • Team accountability. p95 response times, split by agent and number, make slow periods visible instead of personal.

For developer teams, all of these metrics are reachable programmatically. Zetta CRM exposes contacts, labels, conversations, and analytics through a first-party API, with webhooks for real-time events like message.arrived and label.applied. Teams building custom dashboards, internal SLAs, or AI workflows consume the same event stream that powers our own UI — a lesson we learned the hard way (delay the API once, and your most technical users start screen-scraping).

Principles Worth Stealing

If you're building analytics for a chat-native product, these are the ideas we'd defend:

  1. Emit typed events for everything, before you need them. The cost is small; the flexibility is enormous.
  2. Define metrics by the decision they enable, not by what's easy to count. Ask "what would I change at noon?"
  3. Segregate group traffic from DM metrics. One active group will otherwise corrupt your entire report.
  4. Boring infrastructure wins. An append-only store plus a rolling window beats a platform team you don't have yet.
  5. Feed metrics back into behavior — routing, tuning, staffing — or nobody will look twice.

This is the analytics layer we run behind Zetta CRM today, built the same way we build everything: event-first, observability as a product feature, and small enough to change when reality disagrees with the plan.


Built by Cipta Dusa — software development for teams that move fast.

Top comments (0)