Terug naar blog
analyticssaas-metricsdashboardsmrrchurnproduct-analytics

Analytics en dashboards voor je SaaS: van ruwe data naar groei-inzichten

Door SaaS Masters28 maart 20269 min leestijd

Waarom SaaS-metrics er écht toe doen

Je hebt je SaaS gelanceerd, de eerste klanten stromen binnen, en je MRR groeit. Maar weet je ook waarom het groeit? En nog belangrijker: zie je de waarschuwingssignalen voordat het te laat is?

De meeste SaaS-founders bouwen hun product op gevoel. Ze checken Stripe af en toe, kijken naar het aantal gebruikers in de database, en hopen dat het goed gaat. Maar data-gedreven beslissingen maken het verschil tussen een SaaS die schaalt en een die stilletjes leegloopt.

In dit artikel bouwen we stap voor stap een analytics-systeem voor je SaaS: van de juiste metrics tot een werkend dashboard.

De 7 metrics die elke SaaS moet tracken

1. Monthly Recurring Revenue (MRR)

MRR is de hartslag van je SaaS. Het is de voorspelbare, maandelijks terugkerende omzet van al je actieve abonnementen.

-- MRR berekenen vanuit je subscriptions tabel
SELECT
  DATE_TRUNC('month', current_period_start) AS month,
  SUM(
    CASE
      WHEN interval = 'year' THEN price / 12
      ELSE price
    END
  ) AS mrr
FROM subscriptions
WHERE status = 'active'
GROUP BY month
ORDER BY month DESC;

Belangrijk: splits je MRR op in componenten:

  • New MRR — nieuwe klanten
  • Expansion MRR — upgrades en uitbreidingen
  • Contraction MRR — downgrades
  • Churned MRR — opgezegde abonnementen

2. Churn Rate

Churn is de stille killer van SaaS-bedrijven. Er zijn twee soorten:

// Customer churn rate
const customerChurnRate = (lostCustomers / startCustomers) * 100;

// Revenue churn rate (belangrijker!)
const revenueChurnRate = (lostMRR / startMRR) * 100;

// Net revenue churn (inclusief expansie)
const netRevenueChurn = ((lostMRR - expansionMRR) / startMRR) * 100;

Een negatieve net revenue churn is de heilige graal: je bestaande klanten groeien sneller dan je verliest.

3. Customer Lifetime Value (LTV)

// Simpele LTV-berekening
const avgRevenuePerAccount = totalMRR / totalCustomers;
const avgLifetimeMonths = 1 / (customerChurnRate / 100);
const ltv = avgRevenuePerAccount * avgLifetimeMonths;

// LTV:CAC ratio moet minimaal 3:1 zijn
const ltvCacRatio = ltv / customerAcquisitionCost;

4. Customer Acquisition Cost (CAC)

Tel al je sales- en marketingkosten op en deel door het aantal nieuwe klanten. Vergeet niet: salarissen, tooling, advertenties, en content-productie tellen allemaal mee.

5. Activation Rate

Hoeveel procent van nieuwe aanmeldingen bereikt het "aha-moment"? Dit is vaak de meest onderschatte metric.

-- Activation rate: gebruikers die binnen 7 dagen een kernactie voltooien
SELECT
  DATE_TRUNC('week', u.created_at) AS cohort_week,
  COUNT(DISTINCT u.id) AS signups,
  COUNT(DISTINCT e.user_id) AS activated,
  ROUND(
    COUNT(DISTINCT e.user_id)::numeric / COUNT(DISTINCT u.id) * 100, 1
  ) AS activation_rate
FROM users u
LEFT JOIN events e ON e.user_id = u.id
  AND e.name = 'core_action_completed'
  AND e.created_at <= u.created_at + INTERVAL '7 days'
WHERE u.created_at >= NOW() - INTERVAL '3 months'
GROUP BY cohort_week
ORDER BY cohort_week DESC;

6. Daily/Weekly/Monthly Active Users (DAU/WAU/MAU)

-- DAU/MAU ratio (stickiness)
WITH daily AS (
  SELECT COUNT(DISTINCT user_id) AS dau
  FROM events
  WHERE created_at >= CURRENT_DATE
),
monthly AS (
  SELECT COUNT(DISTINCT user_id) AS mau
  FROM events
  WHERE created_at >= CURRENT_DATE - INTERVAL '30 days'
)
SELECT
  dau,
  mau,
  ROUND(dau::numeric / NULLIF(mau, 0) * 100, 1) AS stickiness
FROM daily, monthly;

Een DAU/MAU ratio boven 20% is goed; boven 50% is uitzonderlijk.

7. Time to Value (TTV)

Hoe lang duurt het voordat een nieuwe gebruiker waarde ervaart? Meet dit per cohort en optimaliseer je onboarding om TTV te verkorten.

Architectuur: een event-driven analytics pipeline

De beste manier om SaaS-analytics te bouwen is met een event-based systeem. Elke gebruikersactie wordt een event.

Event tracking implementeren

// lib/analytics.ts
interface AnalyticsEvent {
  name: string;
  userId: string;
  tenantId: string;
  properties?: Record<string, unknown>;
  timestamp?: Date;
}

class Analytics {
  private queue: AnalyticsEvent[] = [];
  private flushInterval: NodeJS.Timeout;

  constructor(private batchSize = 50, private intervalMs = 5000) {
    this.flushInterval = setInterval(() => this.flush(), intervalMs);
  }

  track(event: AnalyticsEvent) {
    this.queue.push({
      ...event,
      timestamp: event.timestamp ?? new Date(),
    });

    if (this.queue.length >= this.batchSize) {
      this.flush();
    }
  }

  private async flush() {
    if (this.queue.length === 0) return;

    const batch = this.queue.splice(0, this.batchSize);

    try {
      await db.analyticsEvent.createMany({
        data: batch.map(e => ({
          name: e.name,
          userId: e.userId,
          tenantId: e.tenantId,
          properties: e.properties ?? {},
          timestamp: e.timestamp,
        })),
      });
    } catch (error) {
      // Bij falen: events terug in de queue
      this.queue.unshift(...batch);
      console.error('Analytics flush failed:', error);
    }
  }
}

export const analytics = new Analytics();

Events tracken in je applicatie

// In je API routes of server actions
import { analytics } from '@/lib/analytics';

// Gebruiker maakt een project aan
analytics.track({
  name: 'project_created',
  userId: user.id,
  tenantId: user.tenantId,
  properties: {
    projectType: 'kanban',
    teamSize: team.members.length,
  },
});

// Gebruiker upgradet
analytics.track({
  name: 'subscription_upgraded',
  userId: user.id,
  tenantId: user.tenantId,
  properties: {
    fromPlan: 'starter',
    toPlan: 'professional',
    mrrDelta: 50,
  },
});

Het dashboard bouwen

Optie 1: Zelf bouwen met SQL + charting library

Voor een interne dashboard is een combinatie van SQL-views en een chart library als Recharts of Tremor ideaal.

// app/api/analytics/mrr/route.ts
import { db } from '@/lib/db';

export async function GET() {
  const mrrData = await db.$queryRaw`
    WITH monthly_mrr AS (
      SELECT
        DATE_TRUNC('month', s."currentPeriodStart") AS month,
        SUM(CASE
          WHEN s.interval = 'year' THEN s.price / 12
          ELSE s.price
        END) AS mrr
      FROM subscriptions s
      WHERE s.status = 'active'
      GROUP BY month
    )
    SELECT
      month,
      mrr,
      mrr - LAG(mrr) OVER (ORDER BY month) AS mrr_change,
      ROUND(
        (mrr - LAG(mrr) OVER (ORDER BY month))::numeric
        / NULLIF(LAG(mrr) OVER (ORDER BY month), 0) * 100,
        1
      ) AS growth_pct
    FROM monthly_mrr
    ORDER BY month DESC
    LIMIT 12
  `;

  return Response.json(mrrData);
}
// components/MRRChart.tsx
'use client';

import { AreaChart, Area, XAxis, YAxis, Tooltip, ResponsiveContainer } from 'recharts';

export function MRRChart({ data }: { data: MRRDataPoint[] }) {
  return (
    <div className="rounded-lg border bg-card p-6">
      <h3 className="text-lg font-semibold mb-4">Monthly Recurring Revenue</h3>
      <ResponsiveContainer width="100%" height={300}>
        <AreaChart data={data}>
          <XAxis
            dataKey="month"
            tickFormatter={(v) => new Date(v).toLocaleDateString('nl-NL', {
              month: 'short', year: '2-digit'
            })}
          />
          <YAxis tickFormatter={(v) => `€${(v / 1000).toFixed(0)}k`} />
          <Tooltip
            formatter={(v: number) => [`€${v.toLocaleString()}`, 'MRR']}
          />
          <Area
            type="monotone"
            dataKey="mrr"
            stroke="#6366f1"
            fill="#6366f1"
            fillOpacity={0.1}
          />
        </AreaChart>
      </ResponsiveContainer>
    </div>
  );
}

Optie 2: Externe tools integreren

Voor snellere implementatie kun je kiezen uit:

ToolBeste voorKosten
PostHogProduct analytics + feature flagsGratis tot 1M events/maand
MixpanelUser behavior trackingGratis tot 20M events/maand
MetabaseSQL-based dashboardsOpen source
GrafanaTechnische metricsOpen source

PostHog is een populaire keuze omdat het self-hosted kan draaien en combineert met feature flags en session replay:

// lib/posthog.ts
import PostHog from 'posthog-node';

const posthog = new PostHog(process.env.POSTHOG_API_KEY!, {
  host: process.env.POSTHOG_HOST ?? 'https://eu.posthog.com',
});

export function trackServerEvent(
  userId: string,
  event: string,
  properties?: Record<string, unknown>
) {
  posthog.capture({
    distinctId: userId,
    event,
    properties: {
      ...properties,
      $set: { lastSeen: new Date().toISOString() },
    },
  });
}

Cohort-analyse: de sleutel tot groei

Cohort-analyse laat je zien hoe groepen gebruikers zich over tijd gedragen. Dit is cruciaal om te begrijpen of je product daadwerkelijk verbetert.

-- Retentie cohort-analyse
WITH cohorts AS (
  SELECT
    id AS user_id,
    DATE_TRUNC('month', created_at) AS cohort_month
  FROM users
),
activity AS (
  SELECT DISTINCT
    user_id,
    DATE_TRUNC('month', created_at) AS activity_month
  FROM events
)
SELECT
  c.cohort_month,
  COUNT(DISTINCT c.user_id) AS cohort_size,
  COUNT(DISTINCT CASE
    WHEN a.activity_month = c.cohort_month + INTERVAL '1 month'
    THEN c.user_id
  END) AS month_1,
  COUNT(DISTINCT CASE
    WHEN a.activity_month = c.cohort_month + INTERVAL '2 months'
    THEN c.user_id
  END) AS month_2,
  COUNT(DISTINCT CASE
    WHEN a.activity_month = c.cohort_month + INTERVAL '3 months'
    THEN c.user_id
  END) AS month_3
FROM cohorts c
LEFT JOIN activity a ON a.user_id = c.user_id
GROUP BY c.cohort_month
ORDER BY c.cohort_month DESC;

Als je retentiecurve na een paar maanden afvlakt (in plaats van naar nul te dalen), heb je product-market fit gevonden.

Alerting: reageer voordat het te laat is

Een dashboard is nutteloos als niemand ernaar kijkt. Stel automatische alerts in voor kritieke drempels:

// lib/alerts.ts
interface AlertConfig {
  metric: string;
  threshold: number;
  direction: 'above' | 'below';
  channel: 'slack' | 'email';
}

const alerts: AlertConfig[] = [
  { metric: 'daily_churn_rate', threshold: 2, direction: 'above', channel: 'slack' },
  { metric: 'activation_rate', threshold: 30, direction: 'below', channel: 'slack' },
  { metric: 'error_rate_5xx', threshold: 1, direction: 'above', channel: 'slack' },
  { metric: 'mrr_growth', threshold: 0, direction: 'below', channel: 'email' },
];

async function checkAlerts() {
  for (const alert of alerts) {
    const currentValue = await getMetricValue(alert.metric);

    const triggered =
      alert.direction === 'above'
        ? currentValue > alert.threshold
        : currentValue < alert.threshold;

    if (triggered) {
      await notify(alert.channel, {
        text: `⚠️ Alert: ${alert.metric} is ${currentValue} (${alert.direction} threshold of ${alert.threshold})`,
      });
    }
  }
}

// Draai elke 6 uur via een cron job

Privacy en GDPR

Analytics en privacy gaan niet altijd makkelijk samen. Belangrijke vuistregels:

  1. Anonimiseer waar mogelijk — gebruik tenant-level metrics in plaats van user-level tracking voor externe rapportages
  2. Respecteer Do Not Track — bied een opt-out voor niet-essentiële tracking
  3. Bewaar data niet te lang — stel een retentiebeleid in (bijv. 24 maanden voor ruwe events)
  4. Documenteer je verwerkingsgrondslag — analytics valt vaak onder "gerechtvaardigd belang", maar onderbouw dit
// Middleware: respecteer tracking-voorkeuren
function shouldTrack(userId: string, eventType: string): boolean {
  // Essentiële metrics (billing, security) altijd tracken
  if (['subscription_changed', 'login_failed'].includes(eventType)) {
    return true;
  }

  // Check opt-out preference
  return !userPreferences.get(userId)?.analyticsOptOut;
}

Praktische tips voor implementatie

  1. Begin klein — Track eerst alleen MRR, churn en activation rate. Voeg meer metrics toe als je groeit.
  2. Automatiseer rapportages — Stuur elke maandag een samenvatting naar Slack met de belangrijkste metrics.
  3. Maak metrics zichtbaar — Hang een TV-dashboard op kantoor of pin het in je team-channel.
  4. Vergelijk met benchmarks — Een churn rate van 5% per maand is normaal voor SMB SaaS, maar slecht voor enterprise.
  5. Scheid operationele en analytische queries — Gebruik een read replica of een apart analytics-schema om je productiedatabase niet te belasten.

Conclusie

Een goed analytics-systeem is geen luxe — het is een noodzaak voor elke serieuze SaaS. Begin met de basis (MRR, churn, activatie), bouw een event-driven pipeline, en breid uit naar cohort-analyse en automatische alerts.

De investering betaalt zich dubbel en dwars terug: je maakt betere beslissingen, ziet problemen eerder, en kunt aan investeerders en stakeholders precies laten zien hoe je SaaS ervoor staat.

Wil je hulp bij het bouwen van een analytics-dashboard voor je SaaS? Neem contact met ons op — we helpen je graag.