Files
Portfolio/blog-posts/88-saas-analytics-metrics.md
T
damjan_savicandClaude Opus 4.5 43484c5023 Add blog posts, cleanup unused files, update components
- Add 100 blog posts covering AI, development, and tech topics
- Add .env.example for environment configuration
- Add accessibility and lighthouse audit scripts
- Remove obsolete SEO reports and temporary files
- Remove dev-dist build artifacts and backup files
- Remove unused portrait images (moved/consolidated elsewhere)
- Update contact form and component improvements

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 11:42:11 +01:00

20 KiB
Raw Blame History

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

Metric Seed/Early Series A Scale
MRR Growth 15-20%/mo 10-15%/mo 5-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