Monitoring Your MVP: Building a Simple but Powerful Analytics Dashboard with Python

Johnny NgareJohnny Ngare
9 min read

In the fast-paced world of startups and product development, launching a Minimum Viable Product (MVP) is just the beginning. What separates successful products from those that fizzle out is often how well teams monitor, understand, and react to user behavior. In this post, we'll walk through building a simple yet powerful analytics dashboard using Python that will help you track the metrics that matter most for your MVP.

Why Build Your Own Dashboard?

While there are plenty of off-the-shelf analytics solutions available, creating a custom dashboard offers several advantages:

  1. Complete control over your data - You own it all, no third-party dependencies

  2. Customized to your specific KPIs - Track exactly what matters to your product

  3. Cost-effective - Save money during your early startup stages

  4. A learning opportunity - Gain valuable data analysis skills

What We'll Build

Our analytics dashboard will:

  • Track key user engagement metrics

  • Visualize conversion funnels

  • Monitor system performance

  • Alert you to unusual patterns

  • Present everything in an easy-to-understand web interface

Prerequisites

  • Basic Python knowledge

  • Familiarity with SQL for data queries

  • Your MVP should be collecting basic usage data

  • A development environment with Python 3.8+

Step 1: Setting Up Your Data Pipeline

Before we can visualize anything, we need to collect and structure our data. For an MVP, I recommend a simple setup:

import pandas as pd
import sqlite3
from datetime import datetime, timedelta

# Connect to your database
# This could be SQLite for simplicity or PostgreSQL/MySQL for production
conn = sqlite3.connect('mvp_analytics.db')

def collect_daily_metrics():
    """Gather key metrics from your database into a DataFrame"""

    query = """
    SELECT 
        date(created_at) as day,
        COUNT(DISTINCT user_id) as daily_active_users,
        COUNT(*) as total_actions,
        SUM(CASE WHEN action_type = 'signup' THEN 1 ELSE 0 END) as signups,
        SUM(CASE WHEN action_type = 'purchase' THEN 1 ELSE 0 END) as purchases
    FROM user_actions
    WHERE created_at >= date('now', '-30 days')
    GROUP BY date(created_at)
    ORDER BY day
    """

    return pd.read_sql_query(query, conn)

# Example usage
daily_metrics = collect_daily_metrics()
print(daily_metrics.head())

This function pulls the last 30 days of user activity from our database, aggregating by day. Customize the SQL query to match your database schema and the specific metrics you want to track.

Step 2: Building Core Metric Functions

Next, let's create functions for calculating the key metrics most MVPs should track:

def calculate_retention(days_back=30, cohort_days=7):
    """Calculate user retention by cohort"""

    query = """
    WITH first_seen AS (
        SELECT 
            user_id, 
            date(MIN(created_at)) as first_day
        FROM user_actions
        GROUP BY user_id
    ),
    daily_activity AS (
        SELECT 
            user_id, 
            date(created_at) as activity_day
        FROM user_actions
        GROUP BY user_id, date(created_at)
    )
    SELECT 
        first_day as cohort,
        COUNT(DISTINCT fs.user_id) as cohort_size,
        SUM(CASE WHEN julianday(da.activity_day) - julianday(fs.first_day) BETWEEN 0 AND 6 THEN 1 ELSE 0 END) as week_1,
        SUM(CASE WHEN julianday(da.activity_day) - julianday(fs.first_day) BETWEEN 7 AND 13 THEN 1 ELSE 0 END) as week_2,
        SUM(CASE WHEN julianday(da.activity_day) - julianday(fs.first_day) BETWEEN 14 AND 20 THEN 1 ELSE 0 END) as week_3,
        SUM(CASE WHEN julianday(da.activity_day) - julianday(fs.first_day) BETWEEN 21 AND 27 THEN 1 ELSE 0 END) as week_4
    FROM first_seen fs
    LEFT JOIN daily_activity da ON fs.user_id = da.user_id
    WHERE fs.first_day >= date('now', ? || ' days')
    GROUP BY fs.first_day
    ORDER BY fs.first_day
    """

    retention_data = pd.read_sql_query(query, conn, params=[f'-{days_back}'])

    # Convert to percentages
    for week in ['week_1', 'week_2', 'week_3', 'week_4']:
        retention_data[week] = (retention_data[week] / retention_data['cohort_size'] * 100).round(1)

    return retention_data

def calculate_conversion_funnel():
    """Calculate conversion between key user journey steps"""

    query = """
    WITH user_journey AS (
        SELECT 
            user_id,
            MAX(CASE WHEN action_type = 'visit' THEN 1 ELSE 0 END) as reached_visit,
            MAX(CASE WHEN action_type = 'signup' THEN 1 ELSE 0 END) as reached_signup,
            MAX(CASE WHEN action_type = 'activation' THEN 1 ELSE 0 END) as reached_activation,
            MAX(CASE WHEN action_type = 'purchase' THEN 1 ELSE 0 END) as reached_purchase
        FROM user_actions
        WHERE created_at >= date('now', '-30 days')
        GROUP BY user_id
    )
    SELECT 
        SUM(reached_visit) as visits,
        SUM(reached_signup) as signups,
        SUM(reached_activation) as activations,
        SUM(reached_purchase) as purchases
    FROM user_journey
    """

    funnel_data = pd.read_sql_query(query, conn)

    # Reshape for easier visualization
    funnel_steps = pd.DataFrame({
        'step': ['Visit', 'Signup', 'Activation', 'Purchase'],
        'count': [funnel_data['visits'][0], funnel_data['signups'][0], 
                  funnel_data['activations'][0], funnel_data['purchases'][0]]
    })

    # Calculate conversion rates
    funnel_steps['conversion_rate'] = (funnel_steps['count'] / funnel_steps['count'].shift(1) * 100).round(1)
    funnel_steps.loc[0, 'conversion_rate'] = 100.0  # First step is always 100%

    return funnel_steps

These functions handle two critical metrics:

  1. User retention - How many users come back over time

  2. Conversion funnel - How users progress through your key workflows

Step 3: Creating Data Visualizations

Now let's visualize our metrics using matplotlib and seaborn:

import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np

def plot_daily_metrics(metrics_df):
    """Create a time series plot of daily metrics"""

    plt.figure(figsize=(12, 6))

    # Plot daily active users
    ax1 = plt.subplot(111)
    ax1.plot(metrics_df['day'], metrics_df['daily_active_users'], 'b-', label='Daily Active Users')
    ax1.set_xlabel('Date')
    ax1.set_ylabel('Users', color='b')
    ax1.tick_params('y', colors='b')

    # Plot actions on a secondary y-axis
    ax2 = ax1.twinx()
    ax2.plot(metrics_df['day'], metrics_df['total_actions'], 'r-', label='Total Actions')
    ax2.set_ylabel('Actions', color='r')
    ax2.tick_params('y', colors='r')

    # Add a legend
    lines1, labels1 = ax1.get_legend_handles_labels()
    lines2, labels2 = ax2.get_legend_handles_labels()
    ax1.legend(lines1 + lines2, labels1 + labels2, loc='upper left')

    plt.title('Daily User Activity')
    plt.tight_layout()
    plt.savefig('static/daily_metrics.png')
    plt.close()

def plot_retention_heatmap(retention_df):
    """Create a heatmap of cohort retention"""

    # Prepare data for heatmap
    retention_matrix = retention_df.set_index('cohort')
    retention_matrix = retention_matrix.drop('cohort_size', axis=1)

    plt.figure(figsize=(10, 8))
    sns.heatmap(retention_matrix, annot=True, cmap='YlGnBu', fmt='.1f', 
                cbar_kws={'label': 'Retention %'})
    plt.title('User Retention by Cohort')
    plt.tight_layout()
    plt.savefig('static/retention_heatmap.png')
    plt.close()

def plot_conversion_funnel(funnel_df):
    """Create a conversion funnel visualization"""

    plt.figure(figsize=(10, 6))

    # Plot the funnel
    sns.barplot(x='step', y='count', data=funnel_df, color='skyblue')

    # Add conversion percentages
    for i, row in funnel_df.iterrows():
        plt.text(i, row['count'] / 2, f"{row['conversion_rate']}%", 
                 ha='center', va='center', color='white', fontweight='bold')

        if i > 0:
            plt.text(i - 0.5, (row['count'] + funnel_df.loc[i-1, 'count']) / 2, 
                     f"→ {row['count'] / funnel_df.loc[i-1, 'count'] * 100:.1f}%", 
                     ha='center', va='center', rotation=90)

    plt.title('User Conversion Funnel')
    plt.ylabel('Number of Users')
    plt.tight_layout()
    plt.savefig('static/conversion_funnel.png')
    plt.close()

These functions create three key visualizations:

  1. A time series of daily active users and actions

  2. A cohort retention heatmap

  3. A conversion funnel with conversion rates between steps

Step 4: Building a Web Dashboard with Flask

Let's put everything together in a simple web application using Flask:

from flask import Flask, render_template
import os

app = Flask(__name__)

@app.route('/')
def dashboard():
    # Make sure our static directory exists
    os.makedirs('static', exist_ok=True)

    # Generate our analytics visualizations
    daily_data = collect_daily_metrics()
    plot_daily_metrics(daily_data)

    retention_data = calculate_retention()
    plot_retention_heatmap(retention_data)

    funnel_data = calculate_conversion_funnel()
    plot_conversion_funnel(funnel_data)

    # Calculate summary metrics for the dashboard
    current_dau = daily_data.iloc[-1]['daily_active_users']
    previous_dau = daily_data.iloc[-2]['daily_active_users']
    dau_growth = ((current_dau - previous_dau) / previous_dau * 100).round(1)

    last_7_days = daily_data.iloc[-7:].sum()
    weekly_active_users = last_7_days['daily_active_users']
    conversion_rate = (funnel_data.iloc[-1]['count'] / funnel_data.iloc[0]['count'] * 100).round(1)

    summary_stats = {
        'daily_active_users': current_dau,
        'dau_growth': dau_growth,
        'weekly_active_users': weekly_active_users,
        'conversion_rate': conversion_rate
    }

    return render_template('dashboard.html', stats=summary_stats)

if __name__ == '__main__':
    app.run(debug=True)

Step 5: Creating the Dashboard Template

Create a templates folder with a dashboard.html file:

<!DOCTYPE html>
<html>
<head>
    <title>MVP Analytics Dashboard</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css">
    <style>
        .metric-card {
            padding: 20px;
            border-radius: 5px;
            box-shadow: 0 4px 6px rgba(0,0,0,0.1);
            margin-bottom: 20px;
        }
        .metric-value {
            font-size: 2em;
            font-weight: bold;
        }
        .growth-positive {
            color: #28a745;
        }
        .growth-negative {
            color: #dc3545;
        }
        .chart-card {
            padding: 20px;
            border-radius: 5px;
            box-shadow: 0 4px 6px rgba(0,0,0,0.1);
            margin-bottom: 20px;
        }
    </style>
</head>
<body>
    <div class="container mt-4">
        <h1 class="mb-4">MVP Analytics Dashboard</h1>

        <div class="row">
            <div class="col-md-3">
                <div class="metric-card bg-light">
                    <h5>Daily Active Users</h5>
                    <div class="metric-value">{{ stats.daily_active_users }}</div>
                    <div class="growth 
                        {% if stats.dau_growth > 0 %}growth-positive{% else %}growth-negative{% endif %}">
                        {% if stats.dau_growth > 0 %}+{% endif %}{{ stats.dau_growth }}% from yesterday
                    </div>
                </div>
            </div>

            <div class="col-md-3">
                <div class="metric-card bg-light">
                    <h5>Weekly Active Users</h5>
                    <div class="metric-value">{{ stats.weekly_active_users }}</div>
                </div>
            </div>

            <div class="col-md-3">
                <div class="metric-card bg-light">
                    <h5>Visit-to-Purchase</h5>
                    <div class="metric-value">{{ stats.conversion_rate }}%</div>
                </div>
            </div>
        </div>

        <div class="row">
            <div class="col-md-12">
                <div class="chart-card">
                    <h4>Daily User Activity</h4>
                    <img src="/static/daily_metrics.png" class="img-fluid">
                </div>
            </div>
        </div>

        <div class="row">
            <div class="col-md-6">
                <div class="chart-card">
                    <h4>User Retention</h4>
                    <img src="/static/retention_heatmap.png" class="img-fluid">
                </div>
            </div>

            <div class="col-md-6">
                <div class="chart-card">
                    <h4>Conversion Funnel</h4>
                    <img src="/static/conversion_funnel.png" class="img-fluid">
                </div>
            </div>
        </div>
    </div>

    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

Step 6: Setting Up Automated Reports

Let's add a scheduler to run daily reports and send them via email:

import schedule
import time
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.image import MIMEImage

def send_daily_report():
    """Generate and email a daily analytics report"""

    # Generate fresh data and charts
    daily_data = collect_daily_metrics()
    plot_daily_metrics(daily_data)

    funnel_data = calculate_conversion_funnel()
    plot_conversion_funnel(funnel_data)

    # Calculate key metrics
    current_dau = daily_data.iloc[-1]['daily_active_users']
    previous_dau = daily_data.iloc[-2]['daily_active_users']
    dau_growth = ((current_dau - previous_dau) / previous_dau * 100).round(1)

    # Prepare email
    msg = MIMEMultipart()
    msg['Subject'] = f'Daily MVP Analytics Report - {datetime.now().strftime("%Y-%m-%d")}'
    msg['From'] = 'your-email@example.com'
    msg['To'] = 'team@yourstartup.com'

    # Email body
    email_body = f"""
    <html>
    <body>
        <h2>Daily MVP Analytics Report</h2>
        <p>Here are your key metrics for today:</p>
        <ul>
            <li>Daily Active Users: {current_dau} ({dau_growth:+.1f}% from yesterday)</li>
            <li>New Signups: {daily_data.iloc[-1]['signups']}</li>
            <li>Purchases: {daily_data.iloc[-1]['purchases']}</li>
        </ul>
        <h3>Daily Activity Chart</h3>
        <img src="cid:daily_metrics">
        <h3>Conversion Funnel</h3>
        <img src="cid:conversion_funnel">
    </body>
    </html>
    """

    msg.attach(MIMEText(email_body, 'html'))

    # Attach images
    with open('static/daily_metrics.png', 'rb') as f:
        img = MIMEImage(f.read())
        img.add_header('Content-ID', '<daily_metrics>')
        msg.attach(img)

    with open('static/conversion_funnel.png', 'rb') as f:
        img = MIMEImage(f.read())
        img.add_header('Content-ID', '<conversion_funnel>')
        msg.attach(img)

    # Send email (configure with your SMTP settings)
    with smtplib.SMTP('smtp.example.com', 587) as server:
        server.starttls()
        server.login('your-email@example.com', 'password')
        server.send_message(msg)

# Schedule the report to run every day at 6am
schedule.every().day.at("06:00").do(send_daily_report)

def run_scheduler():
    """Run scheduled tasks in a separate thread"""
    while True:
        schedule.run_pending()
        time.sleep(60)

# You can start this in a separate thread
# import threading
# scheduler_thread = threading.Thread(target=run_scheduler)
# scheduler_thread.daemon = True
# scheduler_thread.start()

Step 7: Setting Up Simple Anomaly Detection

Let's add a basic anomaly detection system to alert you of unusual patterns:

def detect_anomalies():
    """Detect unusual patterns in your metrics"""

    daily_data = collect_daily_metrics()

    # Calculate 7-day moving average and standard deviation
    daily_data['rolling_avg'] = daily_data['daily_active_users'].rolling(window=7).mean()
    daily_data['rolling_std'] = daily_data['daily_active_users'].rolling(window=7).std()

    # Flag days that are more than 2 standard deviations from the moving average
    daily_data['is_anomaly'] = abs(daily_data['daily_active_users'] - daily_data['rolling_avg']) > (2 * daily_data['rolling_std'])

    # Get today's data
    today = daily_data.iloc[-1]

    if today['is_anomaly']:
        deviation = (today['daily_active_users'] - today['rolling_avg']) / today['rolling_avg'] * 100
        direction = "higher" if deviation > 0 else "lower"

        # Send an alert (this could be an email, SMS, Slack message, etc.)
        message = f"ALERT: Today's user activity is {abs(deviation):.1f}% {direction} than expected!"
        print(message)  # Replace with your preferred notification method

        return True, message

    return False, None

Conclusion

Building your own analytics dashboard gives you deeper insights into your MVP's performance without breaking the bank. As your product evolves, you can extend this framework to track more sophisticated metrics, implement A/B testing, or integrate with additional data sources.

The most important aspects to monitor for most MVPs are:

  1. User Engagement - Are people using your product? How often?

  2. Retention - Do users come back after their first visit?

  3. Conversion - Are users completing key workflows?

  4. Feature Usage - Which features get used most/least?

  5. Performance - Is your product stable and responsive?

Remember, the goal isn't just to collect data, but to act on it. Schedule regular meetings with your team to review the dashboard, identify trends, and make data-driven decisions to improve your product.

Next Steps

Once you've mastered the basics, consider:

  • Adding user segmentation to understand different user behaviors

  • Implementing event tracking to monitor specific feature usage

  • Setting up automated A/B testing

  • Integrating with external data sources like marketing campaigns

Happy monitoring!

10
Subscribe to my newsletter

Read articles from Johnny Ngare directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Johnny Ngare
Johnny Ngare