Add dynamic Business Metrics with sample e-commerce definitions

Replace hardcoded Keboola-specific metrics card in Data Catalog with
dynamic Jinja template that renders whatever metric YAMLs exist in
docs/metrics/. Add 10 sample e-commerce metric definitions across
4 categories (revenue, customers, marketing, support) that align
with the sample data generator tables.

Key changes:
- MetricParser: new category colors + dynamic sql_* field discovery
- _load_metrics_data(): scans docs/metrics/*/*.yml with prod fallback
- catalog.html: 240 lines hardcoded HTML -> 35 lines Jinja loop
- metric_modal.js: regex-based category class removal, new categories
- 21 tests validating YAML schema, parser, and loader
This commit is contained in:
Petr 2026-03-10 22:38:44 +01:00
parent f685dc357f
commit 5a84473213
17 changed files with 794 additions and 205 deletions

View file

@ -0,0 +1,39 @@
- name: customer_count
display_name: Customer Count
category: customers
type: count_distinct
unit: customers
grain: monthly
time_column: created_at
table: customers
expression: "COUNT(DISTINCT customer_id)"
description: "Total number of unique customers. Tracks customer base growth over time. Counts distinct customer records based on registration date."
dimensions:
- segment
- region
- acquisition_channel
notes:
- "Counts only active customers (not deleted or merged)"
- "A customer is counted in the month of their first registration"
- "Segment is assigned based on lifetime spend thresholds"
synonyms:
- total_customers
- customer_base
- active_customers
sql: |
SELECT
DATE_TRUNC('month', created_at) AS month,
COUNT(DISTINCT customer_id) AS new_customers
FROM customers
WHERE status = 'active'
GROUP BY 1
ORDER BY 1
sql_by_segment: |
SELECT
segment,
COUNT(DISTINCT customer_id) AS customer_count,
AVG(lifetime_value) AS avg_ltv
FROM customers
WHERE status = 'active'
GROUP BY 1
ORDER BY 2 DESC

View file

@ -0,0 +1,66 @@
- name: repeat_purchase_rate
display_name: Repeat Purchase Rate
category: customers
type: ratio
unit: "%"
grain: monthly
time_column: order_date
table: orders
tables:
- orders
- customers
expression: "COUNT(DISTINCT CASE WHEN order_number > 1 THEN customer_id END) / COUNT(DISTINCT customer_id)"
description: "Percentage of customers who made more than one purchase. Key loyalty and retention indicator. Higher rates signal strong product-market fit and customer satisfaction."
dimensions:
- customer_segment
- acquisition_channel
- product_category
notes:
- "Calculated over a rolling 12-month window by default"
- "Joins orders to customers via customer_id"
- "Order numbering is based on chronological order per customer"
- "Excludes cancelled and fully refunded orders"
synonyms:
- retention_rate
- repurchase_rate
- customer_loyalty_rate
sql: |
WITH customer_orders AS (
SELECT
customer_id,
COUNT(*) AS order_count
FROM orders
WHERE status = 'completed'
AND order_date >= CURRENT_DATE - INTERVAL '12 months'
GROUP BY 1
)
SELECT
ROUND(
COUNT(CASE WHEN order_count > 1 THEN 1 END) * 100.0
/ COUNT(*), 2
) AS repeat_purchase_rate_pct,
COUNT(*) AS total_customers,
COUNT(CASE WHEN order_count > 1 THEN 1 END) AS repeat_customers
FROM customer_orders
sql_by_channel: |
WITH customer_orders AS (
SELECT
o.customer_id,
c.acquisition_channel,
COUNT(*) AS order_count
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id
WHERE o.status = 'completed'
AND o.order_date >= CURRENT_DATE - INTERVAL '12 months'
GROUP BY 1, 2
)
SELECT
acquisition_channel,
ROUND(
COUNT(CASE WHEN order_count > 1 THEN 1 END) * 100.0
/ COUNT(*), 2
) AS repeat_rate_pct,
COUNT(*) AS total_customers
FROM customer_orders
GROUP BY 1
ORDER BY 2 DESC

View file

@ -0,0 +1,55 @@
- name: campaign_roi
display_name: Campaign ROI
category: marketing
type: ratio
unit: "%"
grain: monthly
time_column: start_date
table: campaigns
tables:
- campaigns
- orders
- web_leads
expression: "(SUM(attributed_revenue) - SUM(spend)) / NULLIF(SUM(spend), 0) * 100"
description: "Return on investment for marketing campaigns. Measures revenue generated relative to campaign spend. Negative ROI indicates underperforming campaigns that need optimization."
dimensions:
- campaign_type
- channel
- target_segment
notes:
- "Attribution uses last-touch model by default"
- "Joins campaigns to orders via utm_campaign tracking codes"
- "Web leads are attributed to campaigns via landing page tracking"
- "ROI above 300% is considered excellent for e-commerce"
synonyms:
- marketing_roi
- campaign_return
- roas
sql: |
SELECT
c.campaign_name,
c.campaign_type,
c.spend,
SUM(o.total_amount) AS attributed_revenue,
ROUND(
(SUM(o.total_amount) - c.spend) / NULLIF(c.spend, 0) * 100, 2
) AS roi_pct
FROM campaigns c
LEFT JOIN orders o ON o.utm_campaign = c.campaign_id
AND o.status = 'completed'
GROUP BY 1, 2, 3
ORDER BY 5 DESC
sql_by_type: |
SELECT
c.campaign_type,
SUM(c.spend) AS total_spend,
SUM(o.total_amount) AS total_revenue,
ROUND(
(SUM(o.total_amount) - SUM(c.spend))
/ NULLIF(SUM(c.spend), 0) * 100, 2
) AS roi_pct
FROM campaigns c
LEFT JOIN orders o ON o.utm_campaign = c.campaign_id
AND o.status = 'completed'
GROUP BY 1
ORDER BY 4 DESC

View file

@ -0,0 +1,53 @@
- name: cost_per_acquisition
display_name: Cost per Acquisition
category: marketing
type: ratio
unit: USD
grain: monthly
time_column: start_date
table: campaigns
tables:
- campaigns
- customers
expression: "SUM(spend) / NULLIF(COUNT(DISTINCT new_customer_id), 0)"
description: "Average cost to acquire one new customer through marketing campaigns. Compares total campaign spend to the number of new customer registrations attributed to those campaigns."
dimensions:
- campaign_type
- channel
- region
notes:
- "Only counts first-time customers (no repeat purchasers)"
- "Joins campaigns to customers via attribution tracking"
- "CPA below customer lifetime value indicates sustainable growth"
synonyms:
- cpa
- customer_acquisition_cost
- cac
sql: |
SELECT
DATE_TRUNC('month', c.start_date) AS month,
SUM(c.spend) AS total_spend,
COUNT(DISTINCT cust.customer_id) AS new_customers,
ROUND(
SUM(c.spend) / NULLIF(COUNT(DISTINCT cust.customer_id), 0), 2
) AS cost_per_acquisition
FROM campaigns c
LEFT JOIN customers cust
ON cust.attribution_campaign = c.campaign_id
AND cust.is_first_purchase = true
GROUP BY 1
ORDER BY 1
sql_by_channel: |
SELECT
c.channel,
SUM(c.spend) AS total_spend,
COUNT(DISTINCT cust.customer_id) AS new_customers,
ROUND(
SUM(c.spend) / NULLIF(COUNT(DISTINCT cust.customer_id), 0), 2
) AS cpa
FROM campaigns c
LEFT JOIN customers cust
ON cust.attribution_campaign = c.campaign_id
AND cust.is_first_purchase = true
GROUP BY 1
ORDER BY 4

View file

@ -0,0 +1,46 @@
- name: lead_conversion_rate
display_name: Lead Conversion Rate
category: marketing
type: ratio
unit: "%"
grain: monthly
time_column: created_at
table: web_leads
expression: "COUNT(CASE WHEN status = 'converted' THEN 1 END) / COUNT(*) * 100"
description: "Percentage of web leads that convert to paying customers. Measures the effectiveness of the sales funnel from initial lead capture through purchase completion."
dimensions:
- source
- landing_page
- lead_score_tier
notes:
- "A lead is 'converted' when they complete their first purchase"
- "Conversion window is 90 days from lead creation"
- "Duplicate leads (same email) are deduplicated by earliest creation"
synonyms:
- conversion_rate
- lead_to_customer_rate
- funnel_conversion
sql: |
SELECT
DATE_TRUNC('month', created_at) AS month,
COUNT(*) AS total_leads,
COUNT(CASE WHEN status = 'converted' THEN 1 END) AS converted,
ROUND(
COUNT(CASE WHEN status = 'converted' THEN 1 END) * 100.0
/ COUNT(*), 2
) AS conversion_rate_pct
FROM web_leads
GROUP BY 1
ORDER BY 1
sql_by_source: |
SELECT
source,
COUNT(*) AS total_leads,
COUNT(CASE WHEN status = 'converted' THEN 1 END) AS converted,
ROUND(
COUNT(CASE WHEN status = 'converted' THEN 1 END) * 100.0
/ COUNT(*), 2
) AS conversion_rate_pct
FROM web_leads
GROUP BY 1
ORDER BY 4 DESC

View file

@ -0,0 +1,45 @@
- name: average_order_value
display_name: Average Order Value
category: revenue
type: average
unit: USD
grain: monthly
time_column: order_date
table: orders
tables:
- orders
- customers
expression: "AVG(total_amount)"
description: "Average monetary value per order. Key indicator of customer purchasing behavior and pricing effectiveness. Joins to customers for segmentation."
dimensions:
- channel
- customer_segment
- product_category
- is_first_order
notes:
- "Calculated only on completed orders"
- "Joins to customers table via customer_id for segment analysis"
- "Useful to compare AOV by new vs returning customers"
synonyms:
- aov
- avg_basket_size
sql: |
SELECT
DATE_TRUNC('month', o.order_date) AS month,
AVG(o.total_amount) AS avg_order_value,
COUNT(*) AS order_count
FROM orders o
WHERE o.status = 'completed'
GROUP BY 1
ORDER BY 1
sql_by_segment: |
SELECT
DATE_TRUNC('month', o.order_date) AS month,
c.segment AS customer_segment,
AVG(o.total_amount) AS avg_order_value,
COUNT(*) AS order_count
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id
WHERE o.status = 'completed'
GROUP BY 1, 2
ORDER BY 1, 3 DESC

View file

@ -0,0 +1,41 @@
- name: revenue_by_channel
display_name: Revenue by Channel
category: revenue
type: sum
unit: USD
grain: monthly
time_column: order_date
table: orders
expression: "SUM(total_amount) GROUP BY channel"
description: "Revenue breakdown by sales channel (web, mobile, in-store, marketplace). Identifies highest-performing channels and guides marketing spend allocation."
dimensions:
- channel
- region
- product_category
notes:
- "Channel is assigned at order creation and does not change"
- "Marketplace channel includes all third-party platforms (Amazon, eBay, etc.)"
- "Cross-channel attribution is not applied; each order is counted once"
synonyms:
- channel_revenue
- sales_by_channel
sql: |
SELECT
DATE_TRUNC('month', order_date) AS month,
channel,
SUM(total_amount) AS revenue,
COUNT(*) AS order_count
FROM orders
WHERE status = 'completed'
GROUP BY 1, 2
ORDER BY 1, 3 DESC
sql_by_region: |
SELECT
DATE_TRUNC('month', order_date) AS month,
channel,
region,
SUM(total_amount) AS revenue
FROM orders
WHERE status = 'completed'
GROUP BY 1, 2, 3
ORDER BY 1, 4 DESC

View file

@ -0,0 +1,40 @@
- name: total_revenue
display_name: Total Revenue
category: revenue
type: sum
unit: USD
grain: monthly
time_column: order_date
table: orders
expression: "SUM(total_amount)"
description: "Total revenue from all orders. Primary top-line metric tracking overall business performance across all channels and product categories."
dimensions:
- channel
- product_category
- region
- payment_method
notes:
- "Includes all completed orders, excludes cancelled and refunded"
- "Revenue is recognized at order completion date, not payment date"
- "Multi-currency orders are converted to USD at daily exchange rate"
synonyms:
- gross_revenue
- total_sales
- top_line_revenue
sql: |
SELECT
DATE_TRUNC('month', order_date) AS month,
SUM(total_amount) AS total_revenue
FROM orders
WHERE status = 'completed'
GROUP BY 1
ORDER BY 1
sql_by_channel: |
SELECT
DATE_TRUNC('month', order_date) AS month,
channel,
SUM(total_amount) AS revenue
FROM orders
WHERE status = 'completed'
GROUP BY 1, 2
ORDER BY 1, 3 DESC

View file

@ -0,0 +1,47 @@
- name: avg_resolution_hours
display_name: Average Resolution Time
category: support
type: average
unit: hours
grain: monthly
time_column: created_at
table: support_tickets
expression: "AVG(EXTRACT(EPOCH FROM (resolved_at - created_at)) / 3600)"
description: "Average time in hours from ticket creation to resolution. Key support team performance metric. Lower values indicate more efficient support operations."
dimensions:
- priority
- category
- agent
- channel
notes:
- "Only includes resolved tickets (excludes open and escalated)"
- "Business hours calculation is not applied; uses wall-clock time"
- "Outliers above 720 hours (30 days) are excluded from average"
synonyms:
- resolution_time
- time_to_resolve
- ttr
sql: |
SELECT
DATE_TRUNC('month', created_at) AS month,
ROUND(
AVG(EXTRACT(EPOCH FROM (resolved_at - created_at)) / 3600), 1
) AS avg_resolution_hours,
COUNT(*) AS resolved_tickets
FROM support_tickets
WHERE resolved_at IS NOT NULL
AND EXTRACT(EPOCH FROM (resolved_at - created_at)) / 3600 <= 720
GROUP BY 1
ORDER BY 1
sql_by_priority: |
SELECT
priority,
ROUND(
AVG(EXTRACT(EPOCH FROM (resolved_at - created_at)) / 3600), 1
) AS avg_resolution_hours,
COUNT(*) AS ticket_count
FROM support_tickets
WHERE resolved_at IS NOT NULL
AND EXTRACT(EPOCH FROM (resolved_at - created_at)) / 3600 <= 720
GROUP BY 1
ORDER BY 2

View file

@ -0,0 +1,46 @@
- name: satisfaction_score
display_name: Customer Satisfaction Score
category: support
type: average
unit: score (1-5)
grain: monthly
time_column: created_at
table: support_tickets
expression: "AVG(satisfaction_score)"
description: "Average customer satisfaction rating on a 1-5 scale collected after ticket resolution. Measures customer perception of support quality and identifies areas for improvement."
dimensions:
- priority
- category
- agent
- resolution_type
notes:
- "Score is collected via post-resolution survey email"
- "Response rate is typically 25-35% of resolved tickets"
- "Score of 4+ is considered 'satisfied', below 3 is 'unsatisfied'"
- "Only tickets with a satisfaction response are included"
synonyms:
- csat
- customer_satisfaction
- satisfaction_rating
sql: |
SELECT
DATE_TRUNC('month', created_at) AS month,
ROUND(AVG(satisfaction_score), 2) AS avg_satisfaction,
COUNT(*) AS responses,
ROUND(
COUNT(CASE WHEN satisfaction_score >= 4 THEN 1 END) * 100.0
/ COUNT(*), 1
) AS pct_satisfied
FROM support_tickets
WHERE satisfaction_score IS NOT NULL
GROUP BY 1
ORDER BY 1
sql_by_category: |
SELECT
category,
ROUND(AVG(satisfaction_score), 2) AS avg_satisfaction,
COUNT(*) AS responses
FROM support_tickets
WHERE satisfaction_score IS NOT NULL
GROUP BY 1
ORDER BY 2 DESC

151
tests/test_metrics.py Normal file
View file

@ -0,0 +1,151 @@
"""Tests for business metric YAML definitions and parser."""
import yaml
import pytest
from pathlib import Path
from webapp.utils.metric_parser import MetricParser
METRICS_DIR = Path(__file__).parent.parent / "docs" / "metrics"
REQUIRED_FIELDS = [
"name", "display_name", "category", "type", "unit",
"grain", "time_column", "table", "description", "expression",
]
def _get_all_metric_files():
"""Return list of all metric YAML files."""
return sorted(METRICS_DIR.glob("*/*.yml"))
class TestMetricYAMLValidity:
"""Validate all metric YAML files have required fields."""
def test_metrics_directory_exists(self):
assert METRICS_DIR.exists(), f"Metrics directory not found: {METRICS_DIR}"
def test_at_least_one_metric_exists(self):
files = _get_all_metric_files()
assert len(files) > 0, "No metric YAML files found"
@pytest.mark.parametrize("metric_file", _get_all_metric_files(), ids=lambda f: f.relative_to(METRICS_DIR).as_posix())
def test_all_metric_yamls_valid(self, metric_file):
"""Every metric YAML must have all required fields."""
with open(metric_file) as f:
raw = yaml.safe_load(f)
assert isinstance(raw, list), f"{metric_file.name}: expected YAML list, got {type(raw).__name__}"
assert len(raw) >= 1, f"{metric_file.name}: YAML list is empty"
metric = raw[0]
assert isinstance(metric, dict), f"{metric_file.name}: first item is not a dict"
missing = [field for field in REQUIRED_FIELDS if field not in metric]
assert not missing, f"{metric_file.name}: missing required fields: {missing}"
# Category must match parent directory name
expected_category = metric_file.parent.name
assert metric["category"] == expected_category, (
f"{metric_file.name}: category '{metric['category']}' != directory '{expected_category}'"
)
class TestMetricCategoriesInParser:
"""Verify CATEGORY_COLORS has entries for all used categories."""
def test_all_categories_have_colors(self):
files = _get_all_metric_files()
categories_used = set()
for f in files:
with open(f) as fh:
raw = yaml.safe_load(fh)
if isinstance(raw, list) and raw:
categories_used.add(raw[0].get("category", ""))
parser = MetricParser(METRICS_DIR)
missing = categories_used - set(parser.CATEGORY_COLORS.keys())
assert not missing, f"CATEGORY_COLORS missing entries for: {missing}"
class TestMetricParserParsesSample:
"""Parse one metric and verify structured output."""
def test_parse_total_revenue(self):
parser = MetricParser(METRICS_DIR)
data = parser.parse_metric("revenue/total_revenue.yml")
assert data["name"] == "total_revenue"
assert data["display_name"] == "Total Revenue"
assert data["category"] == "revenue"
assert data["category_color"] == "#0073D1"
assert data["metadata"]["unit"] == "USD"
assert data["metadata"]["grain"] == "monthly"
assert len(data["dimensions"]) > 0
assert "sql" in data["sql_examples"]
assert data["technical"]["table"] == "orders"
assert data["technical"]["expression"] == "SUM(total_amount)"
def test_parse_metric_with_tables_field(self):
parser = MetricParser(METRICS_DIR)
data = parser.parse_metric("revenue/average_order_value.yml")
assert data["name"] == "average_order_value"
assert "sql_by_segment" in data["sql_examples"]
class TestLoadMetricsData:
"""Verify _load_metrics_data returns correct structure."""
def test_returns_four_categories(self):
from webapp.app import _load_metrics_data
result = _load_metrics_data()
assert isinstance(result, list)
assert len(result) == 4
category_keys = [c["key"] for c in result]
assert "revenue" in category_keys
assert "customers" in category_keys
assert "marketing" in category_keys
assert "support" in category_keys
def test_total_metrics_count(self):
from webapp.app import _load_metrics_data
result = _load_metrics_data()
total = sum(len(c["metrics"]) for c in result)
assert total == 10
def test_metric_has_required_fields(self):
from webapp.app import _load_metrics_data
result = _load_metrics_data()
for cat in result:
for m in cat["metrics"]:
assert "name" in m
assert "display_name" in m
assert "description" in m
assert "grain" in m
assert "path" in m
class TestDynamicSqlFields:
"""Verify sql_by_* fields are auto-discovered by parser."""
def test_dynamic_sql_fields_discovered(self):
parser = MetricParser(METRICS_DIR)
data = parser.parse_metric("revenue/total_revenue.yml")
# sql_by_channel should be found via dynamic discovery
assert "sql_by_channel" in data["sql_examples"]
assert data["sql_examples"]["sql_by_channel"]["title"] == "By Channel"
def test_dynamic_sql_title_generation(self):
parser = MetricParser(METRICS_DIR)
data = parser.parse_metric("customers/repeat_purchase_rate.yml")
# sql_by_channel should be found via dynamic discovery
assert "sql_by_channel" in data["sql_examples"]
assert data["sql_examples"]["sql_by_channel"]["title"] == "By Channel"
def test_static_sql_still_works(self):
parser = MetricParser(METRICS_DIR)
data = parser.parse_metric("revenue/total_revenue.yml")
assert "sql" in data["sql_examples"]
assert data["sql_examples"]["sql"]["title"] == "Basic Query"

View file

@ -13,6 +13,8 @@ import os
from datetime import datetime
from pathlib import Path
import yaml
from flask import Flask, flash, jsonify, redirect, render_template, request, session, url_for
from .auth import admin_required, auth_bp, login_required
@ -351,6 +353,80 @@ def _load_catalog_data() -> list:
return catalog
# Category metadata for Business Metrics card
METRIC_CATEGORY_META = {
'revenue': {'label': 'Revenue', 'css': 'sales', 'order': 1},
'customers': {'label': 'Customers', 'css': 'hr', 'order': 2},
'marketing': {'label': 'Marketing', 'css': 'telemetry', 'order': 3},
'support': {'label': 'Support', 'css': 'support', 'order': 4},
}
def _load_metrics_data():
"""Load business metric definitions for catalog display.
Returns list of category dicts ordered by METRIC_CATEGORY_META:
[{'key': 'revenue', 'label': 'Revenue', 'css': 'sales', 'metrics': [...]}, ...]
"""
# Try production path first, fall back to local dev path
metrics_dir = Path("/data/docs/metrics")
if not metrics_dir.exists():
metrics_dir = Path(__file__).parent.parent / "docs" / "metrics"
if not metrics_dir.exists():
return []
categories = {}
for yml_file in sorted(metrics_dir.glob("*/*.yml")):
try:
with open(yml_file, 'r', encoding='utf-8') as f:
raw = yaml.safe_load(f)
if isinstance(raw, list) and raw:
metric = raw[0]
elif isinstance(raw, dict):
metric = raw
else:
continue
cat_key = yml_file.parent.name
if cat_key not in categories:
categories[cat_key] = []
categories[cat_key].append({
'name': metric.get('name', yml_file.stem),
'display_name': metric.get('display_name', yml_file.stem),
'description': metric.get('description', ''),
'grain': metric.get('grain', ''),
'path': f"{cat_key}/{yml_file.name}",
})
except Exception as e:
logger.warning(f"Could not parse metric {yml_file}: {e}")
# Build ordered result using METRIC_CATEGORY_META
result = []
for cat_key, meta in sorted(METRIC_CATEGORY_META.items(), key=lambda x: x[1]['order']):
if cat_key in categories:
result.append({
'key': cat_key,
'label': meta['label'],
'css': meta['css'],
'metrics': categories[cat_key],
})
# Add any unknown categories at the end
for cat_key, metrics in sorted(categories.items()):
if cat_key not in METRIC_CATEGORY_META:
result.append({
'key': cat_key,
'label': cat_key.replace('_', ' ').title(),
'css': cat_key,
'metrics': metrics,
})
return result
def _send_welcome_message(username: str) -> None:
"""Send a welcome message to the user via bot socket after linking."""
try:
@ -492,11 +568,14 @@ def register_routes(app: Flask) -> None:
else:
table["subscribed"] = table_subs.get(table["name"], False)
metrics_data = _load_metrics_data()
return render_template(
"catalog.html",
data_stats=data_stats,
catalog_data=catalog_data,
sync_settings=sync_settings,
metrics_data=metrics_data,
)
@app.route("/api/catalog/profile/<table_name>")

View file

@ -117,6 +117,26 @@
color: #78350F;
}
.metric-chip.category.revenue {
background: #EFF6FF;
color: #1E40AF;
}
.metric-chip.category.customers {
background: rgba(139, 92, 246, 0.1);
color: #7c3aed;
}
.metric-chip.category.marketing {
background: #FEF3C7;
color: #78350F;
}
.metric-chip.category.support {
background: rgba(234, 88, 12, 0.1);
color: #EA580C;
}
.metric-chip.grain,
.metric-chip.unit {
background: #F3F4F6;
@ -207,6 +227,26 @@
border-bottom-color: #78350F;
}
.metric-tab.active.category-revenue {
color: #1E40AF;
border-bottom-color: #1E40AF;
}
.metric-tab.active.category-customers {
color: #7c3aed;
border-bottom-color: #7c3aed;
}
.metric-tab.active.category-marketing {
color: #78350F;
border-bottom-color: #78350F;
}
.metric-tab.active.category-support {
color: #EA580C;
border-bottom-color: #EA580C;
}
/* Modal Body */
.metric-modal-body {
flex: 1;

View file

@ -112,7 +112,7 @@ function renderMetricModal(data) {
// Apply category class to tabs
document.querySelectorAll('.metric-tab').forEach(tab => {
tab.classList.remove('category-finance', 'category-telemetry', 'category-sales_revenue', 'category-weekly_leadership_kpis');
tab.className = tab.className.replace(/\bcategory-\w+/g, '');
tab.classList.add(categoryClass);
});
@ -420,9 +420,13 @@ function formatCategory(category) {
'finance': 'Finance',
'product_usage': 'Product Usage',
'sales_revenue': 'Sales & Revenue',
'weekly_leadership_kpis': 'Weekly Leadership KPIs'
'weekly_leadership_kpis': 'Weekly Leadership KPIs',
'revenue': 'Revenue',
'customers': 'Customers',
'marketing': 'Marketing',
'support': 'Support'
};
return map[category] || category;
return map[category] || category.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
}
/**

View file

@ -767,6 +767,9 @@ body {
.category-tag.sales { background: rgba(0, 115, 209, 0.1); color: #0073D1; }
.category-tag.telemetry { background: rgba(245, 159, 10, 0.1); color: #b45309; }
.category-tag.support { background: rgba(234, 88, 12, 0.1); color: #EA580C; }
.category-tag.revenue { background: rgba(0, 115, 209, 0.1); color: #0073D1; }
.category-tag.customers { background: rgba(139, 92, 246, 0.1); color: #7c3aed; }
.category-tag.marketing { background: rgba(245, 159, 10, 0.1); color: #b45309; }
.data-highlights {
display: flex;

View file

@ -1374,6 +1374,9 @@
</div>
<!-- ── Card: Business Metrics ── -->
{% if metrics_data %}
{% set metrics_total = namespace(n=0) %}
{% for c in metrics_data %}{% set metrics_total.n = metrics_total.n + c.metrics|length %}{% endfor %}
<div class="source-card">
<div class="source-card-header">
<div class="source-card-left">
@ -1385,8 +1388,8 @@
</div>
<div class="source-card-info">
<div class="source-card-name">Business Metrics</div>
<div class="source-card-desc">Standardized metric definitions for revenue, usage, KPIs, and financial reporting</div>
<div class="source-card-meta">18 metrics &middot; 4 categories</div>
<div class="source-card-desc">Standardized metric definitions with SQL examples and documentation</div>
<div class="source-card-meta">{{ metrics_total.n }} metrics &middot; {{ metrics_data|length }} categories</div>
</div>
</div>
<div class="source-card-right">
@ -1398,220 +1401,32 @@
</div>
</div>
<!-- Finance Category -->
{% for category in metrics_data %}
<div class="accordion-category">
<button class="accordion-trigger" onclick="toggleAccordion(this)">
<svg class="accordion-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="9 18 15 12 9 6"/>
</svg>
<span class="category-tag finance">Finance</span>
<span class="accordion-count">1 metric</span>
<span class="category-tag {{ category.css }}">{{ category.label }}</span>
<span class="accordion-count">{{ category.metrics|length }} metric{{ 's' if category.metrics|length != 1 }}</span>
</button>
<div class="accordion-content">
<div class="table-row" onclick="openMetricModal('finance/infra_cost.yml')">
{% for metric in category.metrics %}
<div class="table-row" onclick="openMetricModal('{{ metric.path }}')">
<div class="table-row-left">
<div class="table-row-name">infra_cost</div>
<div class="table-row-desc">Monthly infrastructure cost from cloud providers (GCP, AWS, Azure) and data warehouses (Snowflake, BigQuery)</div>
<div class="table-row-name">{{ metric.name }}</div>
<div class="table-row-desc">{{ metric.description }}</div>
</div>
<div class="table-row-right">
<span class="rows-badge">monthly</span>
</div>
</div>
</div>
</div>
<!-- Product Usage Category -->
<div class="accordion-category">
<button class="accordion-trigger" onclick="toggleAccordion(this)">
<svg class="accordion-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="9 18 15 12 9 6"/>
</svg>
<span class="category-tag telemetry">Product Usage</span>
<span class="accordion-count">3 metrics</span>
</button>
<div class="accordion-content">
<div class="table-row" onclick="openMetricModal('product_usage/usage_value.yml')">
<div class="table-row-left">
<div class="table-row-name">usage_value</div>
<div class="table-row-desc">Platform usage consumption aggregated per company and metric type with conditional aggregation</div>
</div>
<div class="table-row-right">
<span class="rows-badge">monthly</span>
</div>
</div>
<div class="table-row" onclick="openMetricModal('product_usage/contract_limit_value.yml')">
<div class="table-row-left">
<div class="table-row-name">contract_limit_value</div>
<div class="table-row-desc">Monthly contracted limits for usage metrics per company</div>
</div>
<div class="table-row-right">
<span class="rows-badge">monthly</span>
</div>
</div>
<div class="table-row" onclick="openMetricModal('product_usage/usage_vs_limit.yml')">
<div class="table-row-left">
<div class="table-row-name">usage_vs_limit</div>
<div class="table-row-desc">Comparison of actual usage against contracted limits, identifies overage risk and unpaid usage</div>
</div>
<div class="table-row-right">
<span class="rows-badge">monthly</span>
</div>
</div>
</div>
</div>
<!-- Sales & Revenue Category -->
<div class="accordion-category">
<button class="accordion-trigger" onclick="toggleAccordion(this)">
<svg class="accordion-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="9 18 15 12 9 6"/>
</svg>
<span class="category-tag sales">Sales &amp; Revenue</span>
<span class="accordion-count">4 metrics</span>
</button>
<div class="accordion-content">
<div class="table-row" onclick="openMetricModal('sales_revenue/product_revenue.yml')">
<div class="table-row-left">
<div class="table-row-name">product_revenue</div>
<div class="table-row-desc">Monthly recurring revenue broken down by individual contract lines (product level)</div>
</div>
<div class="table-row-right">
<span class="rows-badge">monthly</span>
</div>
</div>
<div class="table-row" onclick="openMetricModal('sales_revenue/mrr.yml')">
<div class="table-row-left">
<div class="table-row-name">mrr</div>
<div class="table-row-desc">Total monthly recurring revenue aggregated at company level - PRIMARY MRR metric</div>
</div>
<div class="table-row-right">
<span class="rows-badge">monthly</span>
</div>
</div>
<div class="table-row" onclick="openMetricModal('sales_revenue/new_arr.yml')">
<div class="table-row-left">
<div class="table-row-name">new_arr</div>
<div class="table-row-desc">Annual Recurring Revenue from truly new customers (first month of revenue)</div>
</div>
<div class="table-row-right">
<span class="rows-badge">monthly</span>
</div>
</div>
<div class="table-row" onclick="openMetricModal('sales_revenue/upsell_expansion.yml')">
<div class="table-row-left">
<div class="table-row-name">upsell_expansion</div>
<div class="table-row-desc">Revenue expansion from existing customers upgrading their contracts</div>
</div>
<div class="table-row-right">
<span class="rows-badge">monthly</span>
</div>
</div>
</div>
</div>
<!-- Weekly Leadership KPIs Category -->
<div class="accordion-category">
<button class="accordion-trigger" onclick="toggleAccordion(this)">
<svg class="accordion-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="9 18 15 12 9 6"/>
</svg>
<span class="category-tag sales">Weekly Leadership KPIs</span>
<span class="accordion-count">10 metrics</span>
</button>
<div class="accordion-content">
<div class="table-row" onclick="openMetricModal('weekly_leadership_kpis/top_risk_churn_count.yml')">
<div class="table-row-left">
<div class="table-row-name">top_risk_churn_count</div>
<div class="table-row-desc">Weekly count of active customers flagged as high churn risk (CSM opinion = RED) • Customer Success</div>
</div>
<div class="table-row-right">
<span class="rows-badge">weekly</span>
</div>
</div>
<div class="table-row" onclick="openMetricModal('weekly_leadership_kpis/top_risk_churn_usd.yml')">
<div class="table-row-left">
<div class="table-row-name">top_risk_churn_usd</div>
<div class="table-row-desc">Weekly ARR of active customers flagged as high churn risk • Customer Success</div>
</div>
<div class="table-row-right">
<span class="rows-badge">weekly</span>
</div>
</div>
<div class="table-row" onclick="openMetricModal('weekly_leadership_kpis/demo.yml')">
<div class="table-row-left">
<div class="table-row-name">demo</div>
<div class="table-row-desc">Weekly count and ARR of New Business opportunities moved to Demo stage • Sales</div>
</div>
<div class="table-row-right">
<span class="rows-badge">weekly</span>
</div>
</div>
<div class="table-row" onclick="openMetricModal('weekly_leadership_kpis/negotiation.yml')">
<div class="table-row-left">
<div class="table-row-name">negotiation</div>
<div class="table-row-desc">Weekly count and ARR of New Business opportunities moved to Negotiation stage • Sales</div>
</div>
<div class="table-row-right">
<span class="rows-badge">weekly</span>
</div>
</div>
<div class="table-row" onclick="openMetricModal('weekly_leadership_kpis/closed_won.yml')">
<div class="table-row-left">
<div class="table-row-name">closed_won</div>
<div class="table-row-desc">Weekly count and ARR of New Business opportunities closed successfully • Sales</div>
</div>
<div class="table-row-right">
<span class="rows-badge">weekly</span>
</div>
</div>
<div class="table-row" onclick="openMetricModal('weekly_leadership_kpis/revenue_upsells_ytd.yml')">
<div class="table-row-left">
<div class="table-row-name">revenue_upsells_ytd</div>
<div class="table-row-desc">Year-to-Date cumulative revenue upsells based on contract signing dates • Sales</div>
</div>
<div class="table-row-right">
<span class="rows-badge">weekly YTD</span>
</div>
</div>
<div class="table-row" onclick="openMetricModal('weekly_leadership_kpis/revenue_downsells_ytd.yml')">
<div class="table-row-left">
<div class="table-row-name">revenue_downsells_ytd</div>
<div class="table-row-desc">Year-to-Date cumulative revenue downsells based on contract signing dates • Sales</div>
</div>
<div class="table-row-right">
<span class="rows-badge">weekly YTD</span>
</div>
</div>
<div class="table-row" onclick="openMetricModal('weekly_leadership_kpis/net_retention_rate.yml')">
<div class="table-row-left">
<div class="table-row-name">net_retention_rate</div>
<div class="table-row-desc">Net Dollar Retention Rate (LTM) - measures revenue retention and expansion from existing customers • Finance</div>
</div>
<div class="table-row-right">
<span class="rows-badge">weekly</span>
</div>
</div>
<div class="table-row" onclick="openMetricModal('weekly_leadership_kpis/credits_consumption_trend.yml')">
<div class="table-row-left">
<div class="table-row-name">credits_consumption_trend</div>
<div class="table-row-desc">Week-over-week percentage change in total credits consumption for customers aged 90+ days • Product</div>
</div>
<div class="table-row-right">
<span class="rows-badge">weekly</span>
</div>
</div>
<div class="table-row" onclick="openMetricModal('weekly_leadership_kpis/accounts_at_risk.yml')">
<div class="table-row-left">
<div class="table-row-name">accounts_at_risk</div>
<div class="table-row-desc">Number of customer accounts showing concerning usage decline patterns (2 consecutive weeks of 25%+ job decline) • Product</div>
</div>
<div class="table-row-right">
<span class="rows-badge">weekly</span>
<span class="rows-badge">{{ metric.grain }}</span>
</div>
</div>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
{% endif %}
<!-- ── Card 2: Support Data (Jira) ── -->
<div class="source-card" id="jiraCard">

View file

@ -17,7 +17,11 @@ class MetricParser:
'finance': '#0d9668',
'product_usage': '#b45309',
'sales_revenue': '#0073D1',
'weekly_leadership_kpis': '#0073D1'
'weekly_leadership_kpis': '#0073D1',
'revenue': '#0073D1',
'customers': '#7c3aed',
'marketing': '#b45309',
'support': '#EA580C',
}
# Complexity keywords for SQL query classification
@ -181,6 +185,21 @@ class MetricParser:
'complexity': complexity
}
# Dynamic discovery: auto-detect sql_* keys not in the static map
for key in metric:
if key.startswith('sql_') and key not in sql_fields and metric[key]:
# Generate title from key: "sql_by_channel" -> "By Channel"
title_parts = key.replace('sql_', '').replace('_', ' ').title()
# Clean up "By X" pattern
title = title_parts if title_parts.startswith('By') else title_parts
query = metric[key]
complexity = self._classify_sql_complexity(query)
sql_examples[key] = {
'title': title,
'query': query.strip(),
'complexity': complexity
}
return sql_examples
def _classify_sql_complexity(self, query: str) -> str: