feat: add 10 starter pack metrics (revenue, usage, sales, operations)
This commit is contained in:
parent
5cf0df77fc
commit
344d744089
12 changed files with 399 additions and 0 deletions
15
docs/metrics/metrics.yml
Normal file
15
docs/metrics/metrics.yml
Normal 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]
|
||||
40
docs/metrics/operations/infrastructure_cost.yml
Normal file
40
docs/metrics/operations/infrastructure_cost.yml
Normal 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
|
||||
36
docs/metrics/operations/support_resolution_time.yml
Normal file
36
docs/metrics/operations/support_resolution_time.yml
Normal 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
|
||||
29
docs/metrics/product_usage/active_users.yml
Normal file
29
docs/metrics/product_usage/active_users.yml
Normal 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
|
||||
48
docs/metrics/product_usage/feature_adoption.yml
Normal file
48
docs/metrics/product_usage/feature_adoption.yml
Normal 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
|
||||
29
docs/metrics/revenue/arr.yml
Normal file
29
docs/metrics/revenue/arr.yml
Normal 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
|
||||
48
docs/metrics/revenue/churn_rate.yml
Normal file
48
docs/metrics/revenue/churn_rate.yml
Normal 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
|
||||
39
docs/metrics/revenue/mrr.yml
Normal file
39
docs/metrics/revenue/mrr.yml
Normal 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
|
||||
36
docs/metrics/sales/new_customers.yml
Normal file
36
docs/metrics/sales/new_customers.yml
Normal 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
|
||||
34
docs/metrics/sales/pipeline_value.yml
Normal file
34
docs/metrics/sales/pipeline_value.yml
Normal 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
|
||||
30
docs/metrics/sales/upsell_expansion.yml
Normal file
30
docs/metrics/sales/upsell_expansion.yml
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue