Skip to content

Instantly share code, notes, and snippets.

@dbose
Last active July 8, 2025 09:10
Show Gist options
  • Save dbose/01711218f66d73864f9012d68e5b68e1 to your computer and use it in GitHub Desktop.
Save dbose/01711218f66d73864f9012d68e5b68e1 to your computer and use it in GitHub Desktop.
metrics-dbt-docs-integration

// ============================================================================= // COMPLETE DBT-DOCS METRICS INTEGRATION // ============================================================================= // This file contains all the code changes needed to integrate YAML-based // metrics from model configurations into dbt-docs natively. // // Implementation Order: // 1. Service Layer (project_service.js) // 2. Routing (app.js) // 3. Metric Controller & Template // 4. Model Enhancement // 5. Navigation Updates // =============================================================================

// ============================================================================= // 1. SERVICE LAYER INTEGRATION // File: src/app/services/project_service.js // Add after existing model processing logic (around line 200) // =============================================================================

// Extract and process metrics from model meta function processModelMetrics(nodes) { var extractedMetrics = {};

_.each(nodes, function(node) {
    if (node.resource_type === 'model') {
        var modelMeta = node.meta || {};
        var modelMetrics = modelMeta.metrics || {};
        
        if (Object.keys(modelMetrics).length > 0) {
            _.each(modelMetrics, function(metricConfig, metricName) {
                var metricNode = createMetricNode(node, metricName, metricConfig);
                extractedMetrics[metricNode.unique_id] = metricNode;
                
                // Also add to main nodes collection
                nodes[metricNode.unique_id] = metricNode;
            });
            
            // Add metrics reference to the model
            node.associated_metrics = Object.keys(modelMetrics).map(function(metricName) {
                return 'metric.' + node.package_name + '.' + node.name + '.' + metricName;
            });

        }
    }
});

return extractedMetrics;

}

function createMetricNode(modelNode, metricName, metricConfig) { // Include model name in unique_id to prevent collisions var uniqueId = 'metric.' + modelNode.package_name + '.' + modelNode.name + '.' + metricName;

return {
    unique_id: uniqueId,
    name: metricName,
    label: metricConfig.label || metricName,
    resource_type: 'metric',
    package_name: modelNode.package_name,
    description: metricConfig.description || '',
    
    // Keep model context
    model: modelNode.name,
    model_unique_id: modelNode.unique_id,
    
    // Add a display name that shows context
    display_name: modelNode.name + '.' + metricName,
    
    // All other properties remain the same...
    type: metricConfig.type || 'calculated',
    method: metricConfig.method || 'sql',
    column: metricConfig.column || '',
    sql: metricConfig.sql || '',
    macro: metricConfig.macro || '',
    filters: metricConfig.filters || [],
    groups: metricConfig.groups || [],
    format: metricConfig.format || '',
    parameters: metricConfig.parameters || {},
    
    meta: metricConfig,
    tags: metricConfig.tags || [],
    depends_on: {
        nodes: [modelNode.unique_id],
        macros: metricConfig.macro ? ['macro.' + modelNode.package_name + '.' + metricConfig.macro] : []
    },
    original_file_path: modelNode.original_file_path,
    docs: {
        show: true
    }
};

}

// Add to buildMetricTree function in project_service.js (around line 800) // Enhanced buildMetricTree function - Type-First with Model grouping function buildMetricTree(nodes, select) { var metrics = {};

_.each(nodes, function(node) {
    if (node.resource_type !== 'metric') return;
    
    var metricName = node.label || node.name;
    var metricType = node.type || 'calculated';
    var modelName = node.model || 'unknown';
    var is_active = node.unique_id == select;

    // Create type group if it doesn't exist
    if (!metrics[metricType]) {
        metrics[metricType] = {
            type: "folder",
            name: metricType.charAt(0).toUpperCase() + metricType.slice(1) + ' Metrics',
            active: false,
            items: {},
            metricType: metricType
        };
    }

    // Create model subgroup within type if it doesn't exist
    if (!metrics[metricType].items[modelName]) {
        metrics[metricType].items[modelName] = {
            type: "folder",
            name: modelName,
            active: false,
            items: [],
            modelName: modelName,
            metricType: metricType
        };
    }

    // Update active state bubbling up
    if (is_active) {
        metrics[metricType].active = true;
        metrics[metricType].items[modelName].active = true;
    }

    // Add the metric to the model subgroup
    metrics[metricType].items[modelName].items.push({
        type: 'file',
        name: metricName,
        node: node,
        active: is_active,
        unique_id: node.unique_id,
        node_type: 'metric',
        model: modelName,
        metricType: metricType
    });
});

// Convert to array structure and sort
var metrics_array = _.map(metrics, function(typeGroup, typeName) {
    // Convert model items to array and sort
    typeGroup.items = _.sortBy(_.values(typeGroup.items), 'name');
    
    // Sort metrics within each model
    _.each(typeGroup.items, function(modelGroup) {
        modelGroup.items = _.sortBy(modelGroup.items, 'name');
    });
    
    return typeGroup;
});

// Sort by metric type
return _.sortBy(metrics_array, 'name');

}

// Add these calls after the existing node processing (around line 250) // Process metrics from model meta var extractedMetrics = processModelMetrics(service.files.manifest.nodes); console.log('πŸ“Š Extracted', Object.keys(extractedMetrics).length, 'metrics from model meta');

// Update getModelTree function to include metrics // Add this call in getModelTree function (around line 850) var metrics_from_meta = .filter(.values(service.project.nodes), function(node) { return node.resource_type === 'metric'; }); service.tree.metrics = buildMetricTree(metrics_from_meta, select);

// Update searchable collection to include metrics // Modify search_nodes filter (around line 600) var search_nodes = _.filter(service.project.nodes, function(node) { return _.includes(['model', 'source', 'seed', 'snapshot', 'analysis', 'exposure', 'metric'], node.resource_type); });

// ============================================================================= // 2. ROUTING CONFIGURATION // File: src/app/app.js // Add to state definitions // =============================================================================

.state('dbt.metric', { url: "/metric/:unique_id?g_v&g_i&g_e", templateUrl: require('./docs/metric.html'), controller: 'MetricCtrl' })

// ============================================================================= // 3. METRIC CONTROLLER // File: src/app/docs/metric.js (NEW FILE) // =============================================================================

'use strict';

var angular = require('angular'); var dag_utils = require('./dag_utils');

angular .module('dbt') .controller('MetricCtrl', ['$scope', '$state', 'project', 'code', function($scope, $state, projectService, codeService) {

$scope.model_uid = $state.params.unique_id;
$scope.project = projectService;
$scope.codeService = codeService;
$scope.extra_table_fields = [];
$scope.usage_examples = [];
$scope.metric = {};

projectService.ready(function(project) {
    var metric = project.nodes[$scope.model_uid];
    $scope.metric = metric || {};
    
    if (!metric) {
        console.error('Metric not found:', $scope.model_uid);
        return;
    }

    $scope.parents = dag_utils.getParents(project, metric);
    $scope.parentsLength = Object.keys($scope.parents).length;

    // Build extra fields for the details table
    $scope.extra_table_fields = [
        { name: "Type", value: $scope.metric.type },
        { name: "Method", value: $scope.metric.method },
        { name: "Source Model", value: $scope.metric.model }
    ];

    // Add optional fields
    if ($scope.metric.column) {
        $scope.extra_table_fields.push({
            name: "Column",
            value: $scope.metric.column
        });
    }

    if ($scope.metric.groups && $scope.metric.groups.length > 0) {
        $scope.extra_table_fields.push({
            name: "Groups",
            value: $scope.metric.groups.join(', ')
        });
    }

    if ($scope.metric.format) {
        $scope.extra_table_fields.push({
            name: "Format",
            value: $scope.metric.format
        });
    }

    // Generate usage examples - NO TEMPLATE LITERALS!
    $scope.usage_examples = generateUsageExamples($scope.metric);
});

// FIXED: No template literals anywhere
function generateUsageExamples(metric) {
    var examples = [];
    
    // Basic reference example - using string concatenation
    examples.push({
        title: 'Basic Reference',
        language: 'sql',
        code: '-- Reference this metric in a model\nSELECT \n    *,\n    {{ calculate_metric(\'' + metric.name + '\') }} as ' + metric.name + '\nFROM {{ ref(\'' + metric.model + '\') }}'
    });

    // Macro usage if applicable
    if (metric.method === 'macro' && metric.macro) {
        examples.push({
            title: 'Macro Implementation',
            language: 'sql', 
            code: '-- Using the underlying macro\n{{ ' + metric.macro + '(\'' + metric.model + '\', \'' + metric.name + '\') }}'
        });
    }

    // SQL implementation if provided
    if (metric.sql) {
        examples.push({
            title: 'SQL Definition',
            language: 'sql',
            code: metric.sql
        });
    }

    // Advanced usage with filters
    if (metric.filters && metric.filters.length > 0) {
        var filterComments = '-- ' + metric.filters.join('\n-- ');
        examples.push({
            title: 'With Filters Applied',
            language: 'sql',
            code: '-- This metric includes the following filters:\n' + filterComments + '\nSELECT {{ calculate_metric(\'' + metric.name + '\') }} as ' + metric.name + '\nFROM {{ ref(\'' + metric.model + '\') }}'
        });
    }

    // YAML configuration example
    examples.push({
        title: 'YAML Configuration',
        language: 'yaml',
        code: generateYamlExample(metric)
    });
    
    return examples;
}

function generateYamlExample(metric) {
    var yaml = '# models/' + metric.model + '.yml or in model meta\nversion: 2\nmodels:\n  - name: ' + metric.model + '\n    meta:\n      metrics:\n        ' + metric.name + ':\n';
    
    if (metric.label && metric.label !== metric.name) {
        yaml += '          label: "' + metric.label + '"\n';
    }
    
    if (metric.description) {
        yaml += '          description: "' + metric.description + '"\n';
    }
    
    yaml += '          type: ' + metric.type + '\n';
    yaml += '          method: ' + metric.method + '\n';
    
    if (metric.column) {
        yaml += '          column: ' + metric.column + '\n';
    }
    
    if (metric.sql) {
        var indentedSql = metric.sql.replace(/\n/g, '\n            ');
        yaml += '          sql: |\n            ' + indentedSql + '\n';
    }
    
    if (metric.macro) {
        yaml += '          macro: ' + metric.macro + '\n';
    }
    
    if (metric.groups && metric.groups.length > 0) {
        yaml += '          groups:\n';
        for (var i = 0; i < metric.groups.length; i++) {
            yaml += '            - ' + metric.groups[i] + '\n';
        }
    }
    
    if (metric.filters && metric.filters.length > 0) {
        yaml += '          filters:\n';
        for (var j = 0; j < metric.filters.length; j++) {
            yaml += '            - ' + metric.filters[j] + '\n';
        }
    }
    
    return yaml;
}

$scope.copyMetricUsage = function() {
    var usage = '{{ calculate_metric(\'' + $scope.metric.name + '\') }}';
    codeService.copy_to_clipboard(usage);
    console.log('Copied metric usage to clipboard:', usage);
};

$scope.goToModel = function() {
    if ($scope.metric.model_unique_id) {
        $state.go('dbt.model', {unique_id: $scope.metric.model_unique_id});
    }
};

}]);

// ============================================================================= // 4. METRIC TEMPLATE // File: src/app/docs/metric.html (NEW FILE) // =============================================================================

/*

<style> .section-target { top: -8em; } .metric-badge { display: inline-block; padding: 4px 8px; border-radius: 4px; font-size: 12px; font-weight: bold; margin-right: 8px; } .metric-type-calculated { background-color: #007bff; color: white; } .metric-type-aggregate { background-color: #28a745; color: white; } .metric-type-ratio { background-color: #ffc107; color: black; } .metric-method-macro { background-color: #6f42c1; color: white; } .metric-method-sql { background-color: #17a2b8; color: white; } .usage-examples .panel { margin-bottom: 20px; } .usage-examples pre { max-height: 300px; overflow-y: auto; } </style>

{{ metric.label || metric.name }} metric
                <div class="pull-right">
                    <span class="metric-badge metric-type-{{ metric.type }}">{{ metric.type }}</span>
                    <span class="metric-badge metric-method-{{ metric.method }}">{{ metric.method }}</span>
                </div>
                
                <div class='clearfix'></div>
            </h1>
            
            <div class="metric-summary" ng-if="metric.model">
                <p class="text-muted">
                    Defined in model 
                    <a ng-click="goToModel()" style="cursor: pointer; color: #0094b3;">
                        <strong>{{ metric.model }}</strong>
                    </a>
                    <span ng-if="metric.column"> on column <code>{{ metric.column }}</code></span>
                </p>
            </div>
        </div>
    </div>
    
    <div class="app-frame app-pad-h">
        <ul class="nav nav-tabs">
            <li ui-sref-active='active'><a ui-sref="dbt.metric({'#': 'details'})">Details</a></li>
            <li ui-sref-active='active'><a ui-sref="dbt.metric({'#': 'description'})">Description</a></li>
            <li ui-sref-active='active'><a ui-sref="dbt.metric({'#': 'usage'})">Usage</a></li>
            <li ui-sref-active='active' ng-show="parentsLength != 0"><a ui-sref="dbt.metric({'#': 'depends_on'})">Depends On</a></li>
        </ul>
    </div>
</div>

<div class="app-details">
    <div class="app-frame app-pad">
        
        <!-- Details Section -->
        <section class="section">
            <div class="section-target" id="details"></div>
            <table-details model="metric" extras="extra_table_fields" />
        </section>

        <!-- Description Section -->
        <section class="section">
            <div class="section-target" id="description"></div>
            <div class="section-content">
                <h6>Description</h6>
                <div class="panel">
                    <div class="panel-body">
                        <div ng-if="metric.description" class="model-markdown" marked="metric.description"></div>
                        <div ng-if="!metric.description" class="text-muted">
                            This metric is not currently documented. Consider adding a description to help others understand its purpose and calculation.
                        </div>
                    </div>
                </div>
            </div>
        </section>

        <!-- Usage Examples Section -->
        <section class="section">
            <div class="section-target" id="usage"></div>
            <div class="section-content">
                <h6>Usage Examples</h6>
                <div class="usage-examples">
                    <div class="panel" ng-repeat="example in usage_examples">
                        <div class="panel-header">
                            <h5 style="margin: 15px 0 10px 15px;">{{ example.title }}</h5>
                        </div>
                        <div class="panel-body">
                            <div class="pull-right">
                                <button class="btn btn-xs btn-default" ng-click="codeService.copy_to_clipboard(example.code)">
                                    πŸ“‹ Copy
                                </button>
                            </div>
                            <pre style="background-color: #f8f9fa; border: 1px solid #e9ecef; border-radius: 4px; padding: 15px;"><code class="language-{{ example.language || 'sql' }}">{{ example.code }}</code></pre>
                        </div>
                    </div>
                </div>
                
                <!-- Quick Actions -->
                <div class="panel" style="background-color: #f8f9fa;">
                    <div class="panel-body">
                        <h6>Quick Actions</h6>
                        <div class="btn-group" role="group">
                            <button class="btn btn-sm btn-primary" ng-click="copyMetricUsage()">
                                πŸ“‹ Copy Basic Usage
                            </button>
                            <button class="btn btn-sm btn-default" ng-click="goToModel()">
                                πŸ”— View Source Model
                            </button>
                        </div>
                    </div>
                </div>
            </div>
        </section>

        <!-- SQL Implementation (if available) -->
        <section class="section" ng-show="metric.sql">
            <div class="section-target" id="sql"></div>
            <div class="section-content">
                <h6>SQL Implementation</h6>
                <div class="panel">
                    <div class="panel-body">
                        <pre style="background-color: #f8f9fa; border: 1px solid #e9ecef; border-radius: 4px; padding: 15px;"><code class="language-sql">{{ metric.sql }}</code></pre>
                    </div>
                </div>
            </div>
        </section>

        <!-- Filters (if any) -->
        <section class="section" ng-show="metric.filters.length > 0">
            <div class="section-target" id="filters"></div>
            <div class="section-content">
                <h6>Applied Filters</h6>
                <div class="panel">
                    <div class="panel-body">
                        <p>This metric applies the following filters:</p>
                        <ul>
                            <li ng-repeat="filter in metric.filters">
                                <code>{{ filter }}</code>
                            </li>
                        </ul>
                    </div>
                </div>
            </div>
        </section>

        <!-- Groups (if any) -->
        <section class="section" ng-show="metric.groups.length > 0">
            <div class="section-target" id="groups"></div>
            <div class="section-content">
                <h6>Groups</h6>
                <div class="panel">
                    <div class="panel-body">
                        <p>This metric belongs to the following groups:</p>
                        <div>
                            <span ng-repeat="group in metric.groups" class="label label-info" style="margin-right: 8px;">
                                {{ group }}
                            </span>
                        </div>
                    </div>
                </div>
            </div>
        </section>

        <!-- Dependencies -->
        <section class="section" ng-show="parentsLength != 0">
            <div class="section-target" id="depends_on"></div>
            <div class="section-content">
                <h6>Depends On</h6>
                <reference-list references="parents" node="metric" />
            </div>
        </section>
    </div>
</div>

*/

// ============================================================================= // 5. MODEL CONTROLLER ENHANCEMENT // File: src/app/docs/model.js // Add after the existing model loading logic (around line 30) // =============================================================================

// Load associated metrics for this model $scope.modelMetrics = []; $scope.metricsCount = 0;

projectService.ready(function(project) { // ... existing code ...

// Find metrics associated with this model
$scope.modelMetrics = _.filter(project.nodes, function(node) {
    return node.resource_type === 'metric' && node.model_unique_id === $scope.model.unique_id;
});

$scope.metricsCount = $scope.modelMetrics.length;

// Sort metrics by name for consistent display
$scope.modelMetrics = _.sortBy($scope.modelMetrics, 'name');

console.log('πŸ“Š Found', $scope.metricsCount, 'metrics for model', $scope.model.name);

});

// Add utility functions for metrics $scope.copyMetricUsage = function(metric) { var usage = '{{ calculate_metric('' + metric.name + '') }}'; codeService.copy_to_clipboard(usage);

// Simple feedback
console.log('Copied metric usage:', usage);

// Could enhance with toast notification here
showToast('βœ… Metric usage copied to clipboard!');

};

$scope.getMetricBadgeClass = function(metric) { var classes = ['label'];

switch(metric.type) {
    case 'aggregate':
        classes.push('label-success');
        break;
    case 'calculated':
        classes.push('label-primary');
        break;
    case 'ratio':
        classes.push('label-warning');
        break;
    default:
        classes.push('label-default');
}

return classes.join(' ');

};

$scope.getMethodBadgeClass = function(metric) { return metric.method === 'macro' ? 'label label-info' : 'label label-default'; };

// Simple toast notification function function showToast(message) { var toast = document.createElement('div'); toast.className = 'alert alert-success'; toast.style.cssText = position: fixed; top: 20px; right: 20px; z-index: 1000; min-width: 250px; animation: slideIn 0.3s ease;; toast.textContent = message; document.body.appendChild(toast);

setTimeout(function() {
    if (toast.parentNode) {
        toast.style.animation = 'slideOut 0.3s ease';
        setTimeout(function() {
            if (toast.parentNode) {
                toast.parentNode.removeChild(toast);
            }
        }, 300);
    }
}, 3000);

}

// Add CSS for animations var style = document.createElement('style'); style.textContent = @keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } } @keyframes slideOut { from { transform: translateX(0); opacity: 1; } to { transform: translateX(100%); opacity: 0; } }; document.head.appendChild(style);

// ============================================================================= // 6. MODEL TEMPLATE ENHANCEMENT // File: src/app/docs/model.html // Replace the nav tabs section and add metrics section // =============================================================================

/*

  • Details
  • Description
  • Columns
  • Metrics {{ metricsCount }}
  • Referenced By
  • Depends On
  • Code
Associated Metrics ({{ metricsCount }})

This model has {{ metricsCount }} metric{{ metricsCount === 1 ? '' : 's' }} defined in its configuration.

            <div class="table-responsive">
                <table class="table table-striped table-hover">
                    <thead>
                        <tr>
                            <th>Metric</th>
                            <th>Type</th>
                            <th>Method</th>
                            <th>Description</th>
                            <th>Actions</th>
                        </tr>
                    </thead>
                    <tbody>
                        <tr ng-repeat="metric in modelMetrics">
                            <td style="vertical-align: middle;">
                                <a ui-sref="dbt.metric({unique_id: metric.unique_id})" class="text-decoration-none">
                                    <strong>{{ metric.label || metric.name }}</strong>
                                </a>
                                <div class="text-muted small" style="margin-top: 4px;">
                                    <code style="font-size: 11px;">{{ metric.display_name || (metric.model + '.' + metric.name) }}</code>
                                </div>
                                <div ng-if="metric.column" class="text-muted small">
                                    Column: <code>{{ metric.column }}</code>
                                </div>
                            </td>
                            <td style="vertical-align: middle;">
                                <span ng-class="getMetricBadgeClass(metric)">{{ metric.type }}</span>
                            </td>
                            <td style="vertical-align: middle;">
                                <span ng-class="getMethodBadgeClass(metric)">{{ metric.method }}</span>
                            </td>
                            <td style="vertical-align: middle; max-width: 200px;">
                                <div ng-if="metric.description" class="text-truncate" title="{{ metric.description }}">
                                    {{ metric.description }}
                                </div>
                                <div ng-if="!metric.description" class="text-muted">
                                    <em>No description</em>
                                </div>
                            </td>
                            <td style="vertical-align: middle;">
                                <div class="btn-group btn-group-sm" role="group">
                                    <button class="btn btn-default" 
                                            ng-click="copyMetricUsage(metric)"
                                            title="Copy usage code">
                                        πŸ“‹
                                    </button>
                                    <a class="btn btn-primary" 
                                       ui-sref="dbt.metric({unique_id: metric.unique_id})"
                                       title="View metric details">
                                        πŸ‘οΈ
                                    </a>
                                </div>
                            </td>
                        </tr>
                    </tbody>
                </table>
            </div>
            
            <!-- Quick usage examples -->
            <div class="alert alert-info" style="margin-top: 15px;">
                <strong>πŸ’‘ Quick Usage:</strong> 
                Use <code>{{ '{{ calculate_metric(\'METRIC_NAME\') }}' }}</code> to reference these metrics in your models.
            </div>
        </div>
    </div>
</div>
<style> /* Add some custom styling for metrics */ .text-truncate { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } .badge { font-size: 11px; padding: 2px 6px; border-radius: 10px; margin-left: 5px; } .btn-group-sm .btn { padding: 2px 6px; font-size: 12px; } .table-hover tbody tr:hover { background-color: #f5f5f5; } .text-decoration-none { text-decoration: none !important; } .text-decoration-none:hover { text-decoration: none !important; } </style>

*/

// ============================================================================= // 7. NAVIGATION UPDATES // File: src/app/components/model_tree/model_tree.html // Add after the existing metrics section // =============================================================================

/*

πŸ“Š Metrics from Models

*/

// ============================================================================= // 8. DOCUMENTATION INDEX UPDATE // File: src/app/docs/index.js // Add this line to include the metric controller // =============================================================================

require('./metric');

// ============================================================================= // 9. GRAPH SERVICE UPDATES // File: src/app/services/graph.service.js // Add metric styling to the style array // =============================================================================

{ selector: 'node[resource_type="metric"]', style: { 'background-color': '#ff9500', 'border-color': '#ff9500', 'shape': 'diamond' } },

// ============================================================================= // 10. SELECTOR SERVICE UPDATES // File: src/app/services/node_selection_service.js // Update initial_selector and options to include metrics // =============================================================================

var initial_selector = { include: '', exclude: '', packages: [], tags: [null], resource_types: [ 'model', 'seed', 'snapshot', 'source', 'test', 'unit_test', 'analysis', 'exposure', 'metric', // Add this line 'semantic_model', 'saved_query' ], depth: 1, };

// Update options options: { packages: [], tags: [null], resource_types: ['model', 'seed', 'snapshot', 'source', 'test', 'analysis', 'exposure', 'metric', 'semantic_model', 'unit_test', 'saved_query'], }

// ============================================================================= // 11. REFERENCES COMPONENT UPDATE // File: src/app/components/references/index.js // Add to the mapResourceType function // =============================================================================

} else if (type == 'metric') { return 'Metrics';

// ============================================================================= // EXAMPLE YAML CONFIGURATION // This is how your model YAML should be structured // =============================================================================

/* version: 2 models:

  • name: customers description: "Customer dimension table" meta: metrics: total_customers: label: "Total Customers" description: "Count of all customers" type: aggregate method: sql sql: "COUNT(*)" groups: ["core", "customer"]

    avg_customer_ltv:
      label: "Average Customer LTV" 
      description: "Average lifetime value per customer"
      type: calculated
      method: macro
      macro: calculate_avg_ltv
      column: lifetime_value
      format: "$,.2f"
      
    active_customer_rate:
      label: "Active Customer Rate"
      description: "Percentage of customers active in last 30 days"
      type: ratio
      method: sql
      sql: |
        COUNT(CASE WHEN last_order_date >= CURRENT_DATE - 30 THEN 1 END) 
        / COUNT(*)::FLOAT
      format: "0.2%"
      filters:
        - "status = 'active'"
      groups: ["engagement"]
    

*/

// ============================================================================= // IMPLEMENTATION CHECKLIST // =============================================================================

/*

  1. βœ… Update project_service.js with metrics extraction logic
  2. βœ… Add metric route to app.js
  3. βœ… Create metric controller (src/app/docs/metric.js)
  4. βœ… Create metric template (src/app/docs/metric.html)
  5. βœ… Update model controller to show associated metrics
  6. βœ… Update model template with metrics section
  7. βœ… Update navigation tree to include metrics
  8. βœ… Update docs index to include metric controller
  9. βœ… Update graph service for metric visualization
  10. βœ… Update selector services to include metrics
  11. βœ… Update references component for metrics

Testing:

  • Verify metrics are extracted from model YAML
  • Test navigation to metric pages
  • Test model pages show associated metrics
  • Test search includes metrics
  • Test graph visualization includes metrics */
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment