Menu
Zurück zum Blog
2 min read
SaaS

SaaS Analytics & Metrics

SaaS Metriken verstehen und tracken. MRR, Churn, LTV, CAC und Product Analytics für datengetriebenes Wachstum.

SaaS MetricsMRRARRChurn RateLTVCAC
SaaS Analytics & Metrics

SaaS Analytics & Metrics

Meta-Description: SaaS Metriken verstehen und tracken. MRR, Churn, LTV, CAC und Product Analytics für datengetriebenes Wachstum.

Keywords: SaaS Metrics, MRR, ARR, Churn Rate, LTV, CAC, Product Analytics, Cohort Analysis, Retention


Einführung

SaaS Metriken sind das Cockpit für Wachstum. Von MRR bis Churn Rate – die richtigen KPIs messen und verstehen ist entscheidend für fundierte Entscheidungen. Dieser Guide zeigt die wichtigsten Metriken und deren Implementation.


SaaS Metrics Overview

┌─────────────────────────────────────────────────────────────┐
│              SAAS METRICS FRAMEWORK                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  Revenue Metrics:                                           │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  MRR (Monthly Recurring Revenue):                   │   │
│  │  ├── New MRR (New customers)                       │   │
│  │  ├── Expansion MRR (Upgrades, add-ons)             │   │
│  │  ├── Contraction MRR (Downgrades)                  │   │
│  │  └── Churned MRR (Cancellations)                   │   │
│  │                                                     │   │
│  │  Net MRR = New + Expansion - Contraction - Churn   │   │
│  │                                                     │   │
│  │  ARR = MRR × 12                                     │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  Growth Metrics:                                            │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  CAC (Customer Acquisition Cost):                   │   │
│  │  = (Sales + Marketing Costs) / New Customers        │   │
│  │                                                     │   │
│  │  LTV (Lifetime Value):                              │   │
│  │  = ARPU / Churn Rate                                │   │
│  │                                                     │   │
│  │  LTV:CAC Ratio:                                     │   │
│  │  ├── < 1:1 = Losing money                          │   │
│  │  ├── 3:1 = Healthy                                 │   │
│  │  └── > 5:1 = Under-investing in growth             │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  Retention Metrics:                                         │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  Churn Rate:                                        │   │
│  │  = Customers Lost / Total Customers × 100           │   │
│  │                                                     │   │
│  │  Net Revenue Retention (NRR):                       │   │
│  │  = (MRR + Expansion - Contraction - Churn) / MRR    │   │
│  │  ├── < 100% = Net negative (shrinking)             │   │
│  │  ├── 100% = Flat                                   │   │
│  │  └── > 100% = Net positive (growing)               │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  Product Metrics:                                           │
│  ├── DAU/MAU (Daily/Monthly Active Users)                 │
│  ├── Feature Adoption Rate                                 │
│  ├── Time to Value                                         │
│  └── NPS (Net Promoter Score)                              │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Metrics Implementation

// lib/analytics/metrics.ts
import { db } from '@/lib/db';
import { startOfMonth, endOfMonth, subMonths, differenceInDays } from 'date-fns';

interface MRRBreakdown {
  newMRR: number;
  expansionMRR: number;
  contractionMRR: number;
  churnedMRR: number;
  netMRR: number;
  totalMRR: number;
}

export async function calculateMRR(date: Date = new Date()): Promise<MRRBreakdown> {
  const monthStart = startOfMonth(date);
  const monthEnd = endOfMonth(date);
  const prevMonthStart = startOfMonth(subMonths(date, 1));
  const prevMonthEnd = endOfMonth(subMonths(date, 1));

  // Get active subscriptions at end of month
  const activeSubscriptions = await db.subscription.findMany({
    where: {
      status: 'active',
      startDate: { lte: monthEnd }
    },
    include: { plan: true }
  });

  const totalMRR = activeSubscriptions.reduce(
    (sum, sub) => sum + sub.plan.monthlyPrice,
    0
  );

  // New subscriptions this month
  const newSubscriptions = await db.subscription.findMany({
    where: {
      startDate: { gte: monthStart, lte: monthEnd },
      previousSubscriptionId: null
    },
    include: { plan: true }
  });

  const newMRR = newSubscriptions.reduce(
    (sum, sub) => sum + sub.plan.monthlyPrice,
    0
  );

  // Upgrades this month
  const upgrades = await db.subscriptionChange.findMany({
    where: {
      type: 'upgrade',
      changedAt: { gte: monthStart, lte: monthEnd }
    }
  });

  const expansionMRR = upgrades.reduce(
    (sum, change) => sum + change.mrrDelta,
    0
  );

  // Downgrades this month
  const downgrades = await db.subscriptionChange.findMany({
    where: {
      type: 'downgrade',
      changedAt: { gte: monthStart, lte: monthEnd }
    }
  });

  const contractionMRR = Math.abs(downgrades.reduce(
    (sum, change) => sum + change.mrrDelta,
    0
  ));

  // Churned this month
  const churned = await db.subscription.findMany({
    where: {
      status: 'canceled',
      canceledAt: { gte: monthStart, lte: monthEnd }
    },
    include: { plan: true }
  });

  const churnedMRR = churned.reduce(
    (sum, sub) => sum + sub.plan.monthlyPrice,
    0
  );

  const netMRR = newMRR + expansionMRR - contractionMRR - churnedMRR;

  return {
    newMRR,
    expansionMRR,
    contractionMRR,
    churnedMRR,
    netMRR,
    totalMRR
  };
}

export async function calculateChurnRate(date: Date = new Date()): Promise<{
  customerChurn: number;
  revenueChurn: number;
  netRevenueRetention: number;
}> {
  const monthStart = startOfMonth(date);
  const prevMonthStart = startOfMonth(subMonths(date, 1));
  const prevMonthEnd = endOfMonth(subMonths(date, 1));

  // Customers at start of month
  const customersAtStart = await db.subscription.count({
    where: {
      status: 'active',
      startDate: { lt: monthStart }
    }
  });

  // Customers churned this month
  const customersChurned = await db.subscription.count({
    where: {
      status: 'canceled',
      canceledAt: { gte: monthStart }
    }
  });

  const customerChurn = customersAtStart > 0
    ? (customersChurned / customersAtStart) * 100
    : 0;

  // MRR calculations for revenue churn
  const mrrAtStart = await db.subscription.aggregate({
    where: {
      status: 'active',
      startDate: { lt: monthStart }
    },
    _sum: { monthlyAmount: true }
  });

  const startMRR = mrrAtStart._sum.monthlyAmount || 0;

  const { churnedMRR, expansionMRR, contractionMRR } = await calculateMRR(date);

  const revenueChurn = startMRR > 0
    ? ((churnedMRR + contractionMRR) / startMRR) * 100
    : 0;

  const netRevenueRetention = startMRR > 0
    ? ((startMRR + expansionMRR - contractionMRR - churnedMRR) / startMRR) * 100
    : 100;

  return {
    customerChurn,
    revenueChurn,
    netRevenueRetention
  };
}

export async function calculateLTV(): Promise<{
  averageLTV: number;
  ltv_by_plan: Record<string, number>;
}> {
  // Average customer lifespan
  const canceledSubscriptions = await db.subscription.findMany({
    where: { status: 'canceled' },
    select: {
      startDate: true,
      canceledAt: true,
      monthlyAmount: true,
      plan: { select: { name: true } }
    }
  });

  if (canceledSubscriptions.length === 0) {
    return { averageLTV: 0, ltv_by_plan: {} };
  }

  // Calculate average lifespan and ARPU
  let totalLifespanMonths = 0;
  let totalRevenue = 0;
  const planData: Record<string, { revenue: number; months: number; count: number }> = {};

  canceledSubscriptions.forEach(sub => {
    const months = differenceInDays(sub.canceledAt!, sub.startDate) / 30;
    const revenue = months * sub.monthlyAmount;

    totalLifespanMonths += months;
    totalRevenue += revenue;

    const planName = sub.plan.name;
    if (!planData[planName]) {
      planData[planName] = { revenue: 0, months: 0, count: 0 };
    }
    planData[planName].revenue += revenue;
    planData[planName].months += months;
    planData[planName].count += 1;
  });

  const averageLTV = totalRevenue / canceledSubscriptions.length;

  const ltv_by_plan: Record<string, number> = {};
  Object.entries(planData).forEach(([plan, data]) => {
    ltv_by_plan[plan] = data.revenue / data.count;
  });

  return { averageLTV, ltv_by_plan };
}

export async function calculateCAC(
  startDate: Date,
  endDate: Date
): Promise<{
  totalCAC: number;
  cacByChannel: Record<string, number>;
}> {
  // Get marketing and sales costs
  const costs = await db.marketingCost.aggregate({
    where: {
      date: { gte: startDate, lte: endDate }
    },
    _sum: { amount: true }
  });

  // Get new customers in period
  const newCustomers = await db.subscription.count({
    where: {
      startDate: { gte: startDate, lte: endDate },
      previousSubscriptionId: null
    }
  });

  const totalCosts = costs._sum.amount || 0;
  const totalCAC = newCustomers > 0 ? totalCosts / newCustomers : 0;

  // CAC by channel
  const costsByChannel = await db.marketingCost.groupBy({
    by: ['channel'],
    where: { date: { gte: startDate, lte: endDate } },
    _sum: { amount: true }
  });

  const customersByChannel = await db.subscription.groupBy({
    by: ['acquisitionChannel'],
    where: {
      startDate: { gte: startDate, lte: endDate },
      previousSubscriptionId: null
    },
    _count: true
  });

  const cacByChannel: Record<string, number> = {};
  costsByChannel.forEach(cost => {
    const customers = customersByChannel.find(
      c => c.acquisitionChannel === cost.channel
    )?._count || 0;
    cacByChannel[cost.channel] = customers > 0
      ? (cost._sum.amount || 0) / customers
      : 0;
  });

  return { totalCAC, cacByChannel };
}

Cohort Analysis

// lib/analytics/cohorts.ts
import { db } from '@/lib/db';
import { startOfMonth, format, differenceInMonths } from 'date-fns';

interface CohortData {
  cohort: string; // "2024-01"
  size: number;
  retention: number[]; // Retention % for each month
  revenue: number[]; // Revenue for each month
}

export async function generateRetentionCohorts(
  months: number = 12
): Promise<CohortData[]> {
  const cohorts: CohortData[] = [];
  const today = new Date();

  for (let i = months - 1; i >= 0; i--) {
    const cohortDate = startOfMonth(subMonths(today, i));
    const cohortKey = format(cohortDate, 'yyyy-MM');

    // Get users who signed up in this cohort
    const cohortUsers = await db.user.findMany({
      where: {
        createdAt: {
          gte: cohortDate,
          lt: startOfMonth(subMonths(today, i - 1))
        }
      },
      select: { id: true }
    });

    const cohortSize = cohortUsers.length;
    if (cohortSize === 0) continue;

    const userIds = cohortUsers.map(u => u.id);
    const retention: number[] = [];
    const revenue: number[] = [];

    // Calculate retention for each subsequent month
    const monthsSinceCohor = differenceInMonths(today, cohortDate);

    for (let m = 0; m <= monthsSinceCohor; m++) {
      const checkMonth = startOfMonth(subMonths(today, monthsSinceCohor - m));

      // Count active users in this month
      const activeUsers = await db.userActivity.groupBy({
        by: ['userId'],
        where: {
          userId: { in: userIds },
          activityDate: {
            gte: checkMonth,
            lt: startOfMonth(subMonths(today, monthsSinceCohor - m - 1))
          }
        }
      });

      const retentionRate = (activeUsers.length / cohortSize) * 100;
      retention.push(Math.round(retentionRate * 10) / 10);

      // Calculate revenue from this cohort in this month
      const monthRevenue = await db.payment.aggregate({
        where: {
          userId: { in: userIds },
          paidAt: {
            gte: checkMonth,
            lt: startOfMonth(subMonths(today, monthsSinceCohor - m - 1))
          }
        },
        _sum: { amount: true }
      });

      revenue.push(monthRevenue._sum.amount || 0);
    }

    cohorts.push({
      cohort: cohortKey,
      size: cohortSize,
      retention,
      revenue
    });
  }

  return cohorts;
}

Analytics Dashboard

// app/admin/analytics/page.tsx
'use client';

import { useState, useEffect } from 'react';
import { Line, Bar } from 'react-chartjs-2';

interface DashboardMetrics {
  mrr: {
    total: number;
    growth: number;
    breakdown: {
      new: number;
      expansion: number;
      contraction: number;
      churn: number;
    };
  };
  customers: {
    total: number;
    new: number;
    churned: number;
    churnRate: number;
  };
  revenue: {
    ltv: number;
    cac: number;
    ltvCacRatio: number;
    nrr: number;
  };
  mrrHistory: Array<{ month: string; mrr: number }>;
}

export default function AnalyticsDashboard() {
  const [metrics, setMetrics] = useState<DashboardMetrics | null>(null);
  const [dateRange, setDateRange] = useState('12m');

  useEffect(() => {
    fetch(`/api/analytics/dashboard?range=${dateRange}`)
      .then(res => res.json())
      .then(setMetrics);
  }, [dateRange]);

  if (!metrics) return <div>Loading...</div>;

  return (
    <div className="p-6 space-y-6">
      <div className="flex justify-between items-center">
        <h1 className="text-2xl font-bold">Analytics Dashboard</h1>
        <select
          value={dateRange}
          onChange={(e) => setDateRange(e.target.value)}
          className="border rounded px-3 py-2"
        >
          <option value="3m">Last 3 months</option>
          <option value="6m">Last 6 months</option>
          <option value="12m">Last 12 months</option>
        </select>
      </div>

      {/* Key Metrics */}
      <div className="grid grid-cols-4 gap-4">
        <MetricCard
          title="MRR"
          value={`$${metrics.mrr.total.toLocaleString()}`}
          change={metrics.mrr.growth}
          changeLabel="vs last month"
        />
        <MetricCard
          title="Customers"
          value={metrics.customers.total.toLocaleString()}
          subtitle={`+${metrics.customers.new} new this month`}
        />
        <MetricCard
          title="Churn Rate"
          value={`${metrics.customers.churnRate.toFixed(1)}%`}
          status={metrics.customers.churnRate < 5 ? 'good' : 'warning'}
        />
        <MetricCard
          title="LTV:CAC"
          value={`${metrics.revenue.ltvCacRatio.toFixed(1)}:1`}
          status={metrics.revenue.ltvCacRatio >= 3 ? 'good' : 'warning'}
        />
      </div>

      {/* MRR Chart */}
      <div className="bg-white p-6 rounded-lg shadow">
        <h2 className="text-lg font-semibold mb-4">MRR Growth</h2>
        <Line
          data={{
            labels: metrics.mrrHistory.map(h => h.month),
            datasets: [{
              label: 'MRR',
              data: metrics.mrrHistory.map(h => h.mrr),
              borderColor: '#3b82f6',
              tension: 0.3
            }]
          }}
          options={{
            scales: {
              y: {
                beginAtZero: true,
                ticks: {
                  callback: (value) => `$${value.toLocaleString()}`
                }
              }
            }
          }}
        />
      </div>

      {/* MRR Breakdown */}
      <div className="grid grid-cols-2 gap-6">
        <div className="bg-white p-6 rounded-lg shadow">
          <h2 className="text-lg font-semibold mb-4">MRR Breakdown</h2>
          <MRRWaterfall data={metrics.mrr.breakdown} />
        </div>

        <div className="bg-white p-6 rounded-lg shadow">
          <h2 className="text-lg font-semibold mb-4">Unit Economics</h2>
          <div className="space-y-4">
            <div className="flex justify-between">
              <span>Customer Lifetime Value (LTV)</span>
              <span className="font-semibold">
                ${metrics.revenue.ltv.toLocaleString()}
              </span>
            </div>
            <div className="flex justify-between">
              <span>Customer Acquisition Cost (CAC)</span>
              <span className="font-semibold">
                ${metrics.revenue.cac.toLocaleString()}
              </span>
            </div>
            <div className="flex justify-between">
              <span>Net Revenue Retention (NRR)</span>
              <span className={`font-semibold ${
                metrics.revenue.nrr >= 100 ? 'text-green-600' : 'text-red-600'
              }`}>
                {metrics.revenue.nrr.toFixed(0)}%
              </span>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

function MetricCard({
  title,
  value,
  change,
  changeLabel,
  subtitle,
  status
}: {
  title: string;
  value: string;
  change?: number;
  changeLabel?: string;
  subtitle?: string;
  status?: 'good' | 'warning' | 'bad';
}) {
  return (
    <div className="bg-white p-6 rounded-lg shadow">
      <h3 className="text-sm text-gray-500 mb-1">{title}</h3>
      <div className="flex items-baseline gap-2">
        <span className="text-3xl font-bold">{value}</span>
        {status && (
          <span className={`w-3 h-3 rounded-full ${
            status === 'good' ? 'bg-green-500' :
            status === 'warning' ? 'bg-yellow-500' : 'bg-red-500'
          }`} />
        )}
      </div>
      {change !== undefined && (
        <p className={`text-sm mt-1 ${change >= 0 ? 'text-green-600' : 'text-red-600'}`}>
          {change >= 0 ? '+' : ''}{change.toFixed(1)}% {changeLabel}
        </p>
      )}
      {subtitle && (
        <p className="text-sm text-gray-500 mt-1">{subtitle}</p>
      )}
    </div>
  );
}

Metrics Benchmarks

MetricSeed/EarlySeries AScale
**MRR Growth**15-20%/mo10-15%/mo5-10%/mo
**Churn Rate**< 8%< 5%< 3%
**NRR**> 90%> 100%> 120%
**LTV:CAC**> 1:1> 3:1> 5:1
**CAC Payback**< 18mo< 12mo< 8mo

Fazit

SaaS Analytics erfordert:

  1. Revenue Metrics: MRR, ARR, NRR verstehen
  2. Cohort Analysis: Retention über Zeit tracken
  3. Unit Economics: LTV:CAC optimieren
  4. Automation: Dashboards automatisch aktualisieren

Datengetriebene Entscheidungen führen zu nachhaltigem Wachstum.


Bildprompts

  1. "SaaS metrics dashboard, MRR chart and key performance indicators"
  2. "Cohort retention heatmap, color-coded percentages"
  3. "LTV:CAC ratio visualization, balance scale metaphor"

Quellen