feat: add 10 starter pack metrics (revenue, usage, sales, operations)

This commit is contained in:
ZdenekSrotyr 2026-04-10 19:35:28 +02:00
parent 5cf0df77fc
commit 344d744089
12 changed files with 399 additions and 0 deletions

15
docs/metrics/metrics.yml Normal file
View file

@ -0,0 +1,15 @@
version: "2.0"
description: "Business metrics starter pack. Import with: da metrics import docs/metrics/"
categories:
- name: revenue
folder: revenue/
metrics: [total_revenue, mrr, arr, churn_rate]
- name: product_usage
folder: product_usage/
metrics: [active_users, feature_adoption]
- name: sales
folder: sales/
metrics: [new_customers, upsell_expansion, pipeline_value]
- name: operations
folder: operations/
metrics: [support_resolution_time, infrastructure_cost]

View file

@ -0,0 +1,40 @@
name: infrastructure_cost
display_name: Infrastructure Cost
category: operations
type: sum
unit: USD
grain: monthly
table: infra_costs
expression: "SUM(cost_usd)"
time_column: usage_date
description: "Infrastructure Cost — total cloud infrastructure spend per month, broken down by provider, service, and environment."
dimensions:
- provider
- service
- environment
synonyms:
- cloud_cost
- infra_spend
- cloud_spend
notes:
- "Includes compute, storage, networking, and managed services"
- "Production and staging costs should be tracked separately via environment dimension"
- "Compare month-over-month to detect unexpected cost spikes"
- "Calculate cost per MAU to normalize against growth"
sql: |
SELECT
DATE_TRUNC('month', usage_date) AS month,
SUM(cost_usd) AS infrastructure_cost
FROM infra_costs
GROUP BY 1
ORDER BY 1
sql_by_provider: |
SELECT
DATE_TRUNC('month', usage_date) AS month,
provider,
service,
environment,
SUM(cost_usd) AS cost
FROM infra_costs
GROUP BY 1, 2, 3, 4
ORDER BY 1, cost DESC

View file

@ -0,0 +1,36 @@
name: support_resolution_time
display_name: Support Resolution Time
category: operations
type: avg
unit: hours
grain: monthly
table: tickets
expression: "AVG(resolution_hours)"
time_column: created_at
description: "Average Support Resolution Time — the mean time in hours from ticket creation to resolution. Key support quality and efficiency metric."
dimensions:
- priority
- category
synonyms:
- mean_resolution_time
- avg_resolution_time
- time_to_resolve
- ttr
notes:
- "Resolution time = resolved_at - created_at, in hours"
- "Exclude tickets still open (no resolved_at)"
- "SLA targets vary by priority: Critical <4h, High <24h, Normal <72h"
- "Consider p50/p95 in addition to mean to detect outliers"
sql: |
SELECT
DATE_TRUNC('month', created_at) AS month,
priority,
category,
COUNT(*) AS tickets_resolved,
ROUND(AVG(resolution_hours), 2) AS avg_resolution_hours,
ROUND(PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY resolution_hours), 2) AS p50_hours,
ROUND(PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY resolution_hours), 2) AS p95_hours
FROM tickets
WHERE resolved_at IS NOT NULL
GROUP BY 1, 2, 3
ORDER BY 1, priority, avg_resolution_hours DESC

View file

@ -0,0 +1,29 @@
name: active_users
display_name: Monthly Active Users
category: product_usage
type: count
unit: users
grain: monthly
table: user_events
expression: "COUNT(DISTINCT user_id)"
time_column: event_date
description: "Monthly Active Users (MAU) — the count of unique users who performed at least one event in a given month."
dimensions:
- feature
- plan_type
synonyms:
- mau
- monthly_actives
- active_users_monthly
notes:
- "A user counts as active if they trigger any tracked event within the month"
- "Distinguish from logins-only — prefer meaningful engagement events"
- "Filter internal/test accounts before aggregating"
sql: |
SELECT
DATE_TRUNC('month', event_date) AS month,
COUNT(DISTINCT user_id) AS active_users
FROM user_events
WHERE event_type != 'internal'
GROUP BY 1
ORDER BY 1

View file

@ -0,0 +1,48 @@
name: feature_adoption
display_name: Feature Adoption Rate
category: product_usage
type: ratio
unit: percentage
grain: monthly
table: user_events
expression: "COUNT(DISTINCT feature_users) / COUNT(DISTINCT total_users) * 100"
time_column: event_date
description: "Feature Adoption Rate — the percentage of active users who used a specific feature in a given month."
dimensions:
- feature
- plan_type
synonyms:
- feature_usage_rate
- adoption_rate
notes:
- "Denominator is all MAU for the period, numerator is MAU who triggered the feature event"
- "Track each feature separately by filtering on feature_name or event_type"
- "Adoption >20% within 90 days of launch is a common success threshold"
sql: |
WITH monthly_active AS (
SELECT
DATE_TRUNC('month', event_date) AS month,
COUNT(DISTINCT user_id) AS total_users
FROM user_events
WHERE event_type != 'internal'
GROUP BY 1
),
feature_users AS (
SELECT
DATE_TRUNC('month', event_date) AS month,
feature_name,
COUNT(DISTINCT user_id) AS users_adopted
FROM user_events
WHERE event_type = 'feature_used'
AND event_type != 'internal'
GROUP BY 1, 2
)
SELECT
f.month,
f.feature_name,
f.users_adopted,
m.total_users,
ROUND(f.users_adopted * 100.0 / NULLIF(m.total_users, 0), 2) AS adoption_rate_pct
FROM feature_users f
JOIN monthly_active m ON f.month = m.month
ORDER BY f.month, adoption_rate_pct DESC

View file

@ -0,0 +1,29 @@
name: arr
display_name: Annual Recurring Revenue
category: revenue
type: sum
unit: USD
grain: monthly
table: subscriptions
expression: "SUM(mrr_amount) * 12"
time_column: billing_date
description: "Annual Recurring Revenue — MRR annualized. Standard SaaS valuation metric used for board reporting and investor communications."
dimensions:
- plan_type
- region
synonyms:
- annual_revenue
- annualized_revenue
- annualized_mrr
notes:
- "ARR = MRR * 12 — snapshot at end of period"
- "Not a trailing 12-month sum; it is the current MRR extrapolated to a year"
- "Excludes one-time and non-recurring revenue"
sql: |
SELECT
DATE_TRUNC('month', billing_date) AS month,
SUM(mrr_amount) * 12 AS arr
FROM subscriptions
WHERE status = 'active'
GROUP BY 1
ORDER BY 1

View file

@ -0,0 +1,48 @@
name: churn_rate
display_name: Monthly Churn Rate
category: revenue
type: ratio
unit: percentage
grain: monthly
table: subscriptions
expression: "churned_mrr / beginning_mrr * 100"
time_column: billing_date
description: "Monthly revenue churn rate — the percentage of MRR lost due to cancellations and downgrades in a given month."
dimensions:
- plan_type
- region
synonyms:
- mrr_churn
- revenue_churn
- monthly_churn
notes:
- "Revenue churn differs from customer churn — one large customer churning can dominate"
- "Negative churn occurs when expansion revenue exceeds lost revenue"
- "Target: <2% monthly for healthy SaaS businesses"
- "Churned MRR includes full cancellations and partial downgrades"
sql: |
WITH monthly_mrr AS (
SELECT
DATE_TRUNC('month', billing_date) AS month,
SUM(CASE WHEN status = 'active' THEN mrr_amount ELSE 0 END) AS active_mrr,
SUM(CASE WHEN status = 'churned'
AND DATE_TRUNC('month', churned_at) = DATE_TRUNC('month', billing_date)
THEN mrr_amount ELSE 0 END) AS churned_mrr
FROM subscriptions
GROUP BY 1
),
lagged AS (
SELECT
month,
churned_mrr,
LAG(active_mrr) OVER (ORDER BY month) AS beginning_mrr
FROM monthly_mrr
)
SELECT
month,
churned_mrr,
beginning_mrr,
ROUND(churned_mrr / NULLIF(beginning_mrr, 0) * 100, 2) AS churn_rate_pct
FROM lagged
WHERE beginning_mrr IS NOT NULL
ORDER BY month

View file

@ -0,0 +1,39 @@
name: mrr
display_name: Monthly Recurring Revenue
category: revenue
type: sum
unit: USD
grain: monthly
table: subscriptions
expression: "SUM(mrr_amount)"
time_column: billing_date
description: "Monthly Recurring Revenue — the predictable monthly revenue from active subscriptions. Core SaaS health metric."
dimensions:
- plan_type
- region
synonyms:
- monthly_revenue
- recurring_revenue
- subscription_revenue
notes:
- "Excludes one-time fees, setup fees, and professional services"
- "Churned subscriptions are removed in the month they cancel"
- "Upgrades and downgrades are reflected in the month the change takes effect"
- "Use billing_date (not payment date) for recognition"
sql: |
SELECT
DATE_TRUNC('month', billing_date) AS month,
SUM(mrr_amount) AS mrr
FROM subscriptions
WHERE status = 'active'
GROUP BY 1
ORDER BY 1
sql_by_plan: |
SELECT
DATE_TRUNC('month', billing_date) AS month,
plan_type,
SUM(mrr_amount) AS mrr
FROM subscriptions
WHERE status = 'active'
GROUP BY 1, 2
ORDER BY 1, 3 DESC

View file

@ -0,0 +1,36 @@
name: new_customers
display_name: New Customers
category: sales
type: count
unit: customers
grain: monthly
table: orders
expression: "COUNT(DISTINCT customer_id)"
time_column: order_date
description: "New Customers — the count of customers placing their first-ever order in a given month."
dimensions:
- channel
- region
synonyms:
- new_logos
- first_time_buyers
- customer_acquisition
notes:
- "A customer is 'new' only if they have no prior orders in any historical period"
- "Exclude internal test accounts and employees from the count"
- "Use acquisition channel (UTM/referral) from first order for attribution"
sql: |
WITH first_orders AS (
SELECT
customer_id,
MIN(order_date) AS first_order_date
FROM orders
WHERE status != 'cancelled'
GROUP BY customer_id
)
SELECT
DATE_TRUNC('month', first_order_date) AS month,
COUNT(DISTINCT customer_id) AS new_customers
FROM first_orders
GROUP BY 1
ORDER BY 1

View file

@ -0,0 +1,34 @@
name: pipeline_value
display_name: Pipeline Value
category: sales
type: sum
unit: USD
grain: monthly
table: opportunities
expression: "SUM(deal_value * probability / 100)"
time_column: created_at
description: "Pipeline Value — the probability-weighted total value of open sales opportunities, used for revenue forecasting."
dimensions:
- stage
- owner
- region
synonyms:
- weighted_pipeline
- pipeline_forecast
- opportunity_value
notes:
- "Weighted by close probability for each stage (e.g., 20% at Qualify, 80% at Proposal)"
- "Snapshot the pipeline at month-end for trend analysis"
- "Exclude closed-won and closed-lost opportunities"
- "Compare to quota to assess pipeline coverage ratio (target: 3-4x)"
sql: |
SELECT
DATE_TRUNC('month', created_at) AS month,
stage,
COUNT(*) AS opportunity_count,
SUM(deal_value) AS total_deal_value,
SUM(deal_value * probability / 100.0) AS weighted_pipeline_value
FROM opportunities
WHERE status = 'open'
GROUP BY 1, 2
ORDER BY 1, weighted_pipeline_value DESC

View file

@ -0,0 +1,30 @@
name: upsell_expansion
display_name: Upsell & Expansion Revenue
category: sales
type: sum
unit: USD
grain: monthly
table: subscriptions
expression: "SUM(delta_mrr)"
time_column: billing_date
description: "Upsell & Expansion MRR — incremental recurring revenue gained from existing customers upgrading or expanding their subscriptions."
dimensions:
- plan_type
- region
synonyms:
- expansion_mrr
- upsell_revenue
- expansion_revenue
notes:
- "Only includes positive MRR changes (upgrades, seat additions, add-ons)"
- "Excludes new business — filter on customers with prior active subscriptions"
- "Negative expansion (downgrades) is tracked separately as contraction MRR"
sql: |
SELECT
DATE_TRUNC('month', billing_date) AS month,
SUM(delta_mrr) AS expansion_mrr
FROM subscriptions
WHERE change_type IN ('upgrade', 'expansion', 'seat_add')
AND delta_mrr > 0
GROUP BY 1
ORDER BY 1

View file

@ -396,6 +396,21 @@ class TestMetricRepositoryImport:
assert len(all_metrics) == 2
class TestStarterPack:
def test_import_starter_pack(self, db_conn):
from src.repositories.metrics import MetricRepository
from pathlib import Path
repo = MetricRepository(db_conn)
starter_dir = Path(__file__).parent.parent / "docs" / "metrics"
if not starter_dir.exists():
pytest.skip("Starter pack not found")
count = repo.import_from_yaml(starter_dir)
assert count >= 11 # total_revenue + 10 new
assert repo.get("revenue/total_revenue") is not None
assert repo.get("revenue/mrr") is not None
assert repo.get("operations/infrastructure_cost") is not None
class TestMetricRepositoryExport:
def test_export_to_yaml(self, db_conn, metrics_dir, tmp_path):
from src.repositories.metrics import MetricRepository