Initial commit: Portfolio Website
Vollständige Next.js 15 Portfolio-Website mit: - Blog-System mit 100+ Artikeln - Supabase-Integration - Responsive Design mit Tailwind CSS - TypeScript-Konfiguration - Testing-Setup mit Vitest und Playwright Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,661 @@
|
||||
# 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
|
||||
|
||||
```typescript
|
||||
// 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
|
||||
|
||||
```typescript
|
||||
// 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
|
||||
|
||||
```tsx
|
||||
// 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
|
||||
|
||||
- [SaaS Metrics 2.0](https://www.forentrepreneurs.com/saas-metrics-2/)
|
||||
- [ChartMogul SaaS Metrics](https://chartmogul.com/blog/saas-metrics/)
|
||||
- [Stripe Atlas Guides](https://stripe.com/atlas/guides)
|
||||
- [OpenView SaaS Benchmarks](https://openviewpartners.com/saas-benchmarks/)
|
||||
Reference in New Issue
Block a user