Building a GitHub-Style Activity Dashboard: Lessons from My First Internship

Note: Due to NDA restrictions, all code examples and UI descriptions in this case study are reconstructed from memory for educational purposes. They represent the patterns and approaches I used, not the actual proprietary implementation.


The Project

During my internship, I was tasked with building an internal analytics dashboard to track product usage patterns. Think GitHub's contribution graph, but for tracking user interactions with various product features across the organization.

The goal was simple: give stakeholders a visual way to understand how users engaged with the product over time.

Timeline: 1-2 months Role: Solo developer (with mentorship) Tech Stack: Node.js, Express, EJS, jQuery, Chart.js, Bootstrap, MongoDB with Mongoose


The Challenge

As someone new to development, this project threw me into the deep end. I had to:

  1. Design a data model for tracking user activities
  2. Build aggregation pipelines to process thousands of records
  3. Create a heatmap visualization similar to GitHub's contribution graph
  4. Make it all render in a reasonable time

The reality? My initial queries took 3-5 seconds to render a single page. That's an eternity in web development.


Technical Architecture

Stack Overview:

LayerTechnologyPurpose
ServerExpress.jsRoute handling, API endpoints
DatabaseMongoDB + MongooseData storage, aggregation queries
TemplatingEJSServer-side HTML rendering
FrontendjQuery + Chart.jsDOM manipulation, heatmap visualization
StylingBootstrapResponsive UI components

The Activity Tracking Schema

The core data model tracked every user interaction:

// Activity Schema (Reconstructed pattern)
const activitySchema = new mongoose.Schema({
  userId: {
    type: mongoose.Schema.Types.ObjectId,
    ref: 'User',
    required: true,
    index: true
  },
  productId: {
    type: mongoose.Schema.Types.ObjectId,
    ref: 'Product',
    required: true,
    index: true
  },
  action: {
    type: String,
    enum: ['view', 'click', 'purchase', 'share', 'download'],
    required: true
  },
  metadata: {
    duration: Number,      // Time spent in seconds
    source: String,        // Where the action originated
    device: String         // Desktop, mobile, tablet
  },
  timestamp: {
    type: Date,
    default: Date.now,
    index: true
  }
});

// Compound index for common queries
activitySchema.index({ userId: 1, timestamp: -1 });
activitySchema.index({ productId: 1, timestamp: -1 });

The Performance Problem

My first aggregation query looked something like this:

// Initial approach - SLOW (3-5 seconds)
async function getActivityHeatmap(userId, year) {
  const startDate = new Date(year, 0, 1);
  const endDate = new Date(year, 11, 31);

  const activities = await Activity.find({
    userId: userId,
    timestamp: { $gte: startDate, $lte: endDate }
  });

  // Process in JavaScript - BAD IDEA
  const heatmapData = {};

  activities.forEach(activity => {
    const dateKey = activity.timestamp.toISOString().split('T')[0];
    if (!heatmapData[dateKey]) {
      heatmapData[dateKey] = 0;
    }
    heatmapData[dateKey]++;
  });

  return heatmapData;
}

Why was it slow?

  1. Fetching ALL records to Node.js memory
  2. Processing aggregation in JavaScript instead of MongoDB
  3. No pagination or limits
  4. Missing proper indexes

The Optimization Journey

Step 1: Move Aggregation to MongoDB

// Optimized approach - Let MongoDB do the heavy lifting
async function getActivityHeatmap(userId, year) {
  const startDate = new Date(year, 0, 1);
  const endDate = new Date(year + 1, 0, 1);

  const pipeline = [
    // Match only relevant documents
    {
      $match: {
        userId: mongoose.Types.ObjectId(userId),
        timestamp: { $gte: startDate, $lt: endDate }
      }
    },
    // Group by date
    {
      $group: {
        _id: {
          $dateToString: { format: '%Y-%m-%d', date: '$timestamp' }
        },
        count: { $sum: 1 }
      }
    },
    // Sort by date
    {
      $sort: { _id: 1 }
    }
  ];

  return Activity.aggregate(pipeline);
}

Result: Query time dropped from 3-5 seconds to ~500ms.

Step 2: Add Proper Indexes

// Compound index for this specific query pattern
activitySchema.index({ userId: 1, timestamp: 1 });

Result: Query time dropped to ~100-200ms.

Step 3: Implement Caching for Static Data

// Simple in-memory cache for dashboard summaries
const cache = new Map();
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes

async function getCachedHeatmap(userId, year) {
  const cacheKey = `heatmap:${userId}:${year}`;
  const cached = cache.get(cacheKey);

  if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
    return cached.data;
  }

  const data = await getActivityHeatmap(userId, year);
  cache.set(cacheKey, { data, timestamp: Date.now() });

  return data;
}

Building the Heatmap Visualization

For the GitHub-style activity grid, I used Chart.js with a custom configuration:

// Client-side rendering with jQuery and Chart.js
$(document).ready(function() {
  const ctx = document.getElementById('activityHeatmap').getContext('2d');

  // Transform API data into heatmap format
  const heatmapData = transformToHeatmapFormat(apiResponse);

  new Chart(ctx, {
    type: 'matrix',  // Using chartjs-chart-matrix plugin
    data: {
      datasets: [{
        label: 'Activity',
        data: heatmapData,
        backgroundColor: function(context) {
          const value = context.dataset.data[context.dataIndex].v;
          return getColorForValue(value);
        },
        width: function(context) {
          return (context.chart.chartArea || {}).width / 53;
        },
        height: function(context) {
          return (context.chart.chartArea || {}).height / 7;
        }
      }]
    },
    options: {
      responsive: true,
      plugins: {
        tooltip: {
          callbacks: {
            title: function(context) {
              return context[0].raw.d;
            },
            label: function(context) {
              return context.raw.v + ' activities';
            }
          }
        }
      },
      scales: {
        x: {
          type: 'category',
          labels: getWeekLabels(),
          grid: { display: false }
        },
        y: {
          type: 'category',
          labels: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
          grid: { display: false }
        }
      }
    }
  });
});

// Color scale similar to GitHub
function getColorForValue(value) {
  if (value === 0) return '#ebedf0';
  if (value <= 3) return '#9be9a8';
  if (value <= 6) return '#40c463';
  if (value <= 9) return '#30a14e';
  return '#216e39';
}

Heatmap Color Scale:

IntensityColorActivities
None#ebedf00
Low#9be9a81-3
Medium#40c4634-6
High#30a14e7-9
Very High#216e3910+

EJS Template Structure

The dashboard used EJS for server-side rendering:

<!-- views/dashboard.ejs -->
<!DOCTYPE html>
<html>
<head>
  <title>Activity Dashboard</title>
  <link rel="stylesheet" href="/css/bootstrap.min.css">
  <link rel="stylesheet" href="/css/dashboard.css">
</head>
<body>
  <div class="container-fluid">
    <div class="row">
      <!-- Sidebar -->
      <nav class="col-md-2 sidebar">
        <%- include('partials/sidebar') %>
      </nav>

      <!-- Main content -->
      <main class="col-md-10 main-content">
        <h1>Activity Overview</h1>

        <!-- Summary cards -->
        <div class="row mb-4">
          <div class="col-md-3">
            <div class="card">
              <div class="card-body">
                <h5>Total Activities</h5>
                <p class="display-4"><%= summary.total %></p>
              </div>
            </div>
          </div>
          <!-- More summary cards... -->
        </div>

        <!-- Activity heatmap -->
        <div class="card">
          <div class="card-header">
            <h5>Activity Heatmap - <%= year %></h5>
          </div>
          <div class="card-body">
            <canvas id="activityHeatmap"></canvas>
          </div>
        </div>

        <!-- Recent activity table -->
        <div class="card mt-4">
          <div class="card-header">
            <h5>Recent Activity</h5>
          </div>
          <div class="card-body">
            <table class="table" id="activityTable">
              <thead>
                <tr>
                  <th>Date</th>
                  <th>Action</th>
                  <th>Product</th>
                  <th>Duration</th>
                </tr>
              </thead>
              <tbody>
                <% activities.forEach(function(activity) { %>
                  <tr>
                    <td><%= formatDate(activity.timestamp) %></td>
                    <td><%= activity.action %></td>
                    <td><%= activity.productId.name %></td>
                    <td><%= activity.metadata.duration %>s</td>
                  </tr>
                <% }); %>
              </tbody>
            </table>
          </div>
        </div>
      </main>
    </div>
  </div>

  <script src="/js/jquery.min.js"></script>
  <script src="/js/bootstrap.bundle.min.js"></script>
  <script src="/js/chart.min.js"></script>
  <script src="/js/dashboard.js"></script>
  <script>
    // Pass server data to client
    window.heatmapData = <%- JSON.stringify(heatmapData) %>;
  </script>
</body>
</html>

Lessons Learned

1. Let the Database Do the Work

My biggest mistake was pulling raw data into Node.js and processing it there. MongoDB's aggregation pipeline is incredibly powerful—use it.

Before: App Memory ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ (All data loaded)
After:  App Memory ▓▓▓░░░░░░░░░░░░░░░░░ (Only results)

2. Indexes Are Not Optional

Without proper indexes, MongoDB scans every document. With indexes, it jumps directly to matching records.

Without Index: O(n) - Scans all documents
With Index:    O(log n) - Binary search

For 100,000 documents:
Without: ~100,000 comparisons
With:    ~17 comparisons

3. Server-Side Rendering Has Its Place

While modern apps trend toward SPAs, EJS and server-side rendering made sense for this internal tool:

  • Simpler architecture
  • Better initial load performance
  • Easier to secure (no exposed API endpoints)
  • jQuery was sufficient for interactivity

4. Delivery Over Perfection (Sometimes)

As an intern focused on getting the project done, I made tradeoffs:

  • Basic caching instead of Redis
  • Simple authentication instead of OAuth
  • Manual deployment instead of CI/CD

These were the right calls for the timeline and context. Perfect is the enemy of done.

5. Performance Problems Compound

A 3-second query might seem acceptable, but:

  • User requests summary page → 3 seconds
  • User requests detailed view → 3 more seconds
  • User filters by date range → 3 more seconds

Bad performance creates frustration that multiplies.


What I'd Do Differently Today

1. Use Time-Series Optimizations

MongoDB now has better time-series collection support:

db.createCollection("activities", {
  timeseries: {
    timeField: "timestamp",
    metaField: "metadata",
    granularity: "hours"
  }
});

2. Implement Proper Pagination

// Cursor-based pagination instead of skip/limit
async function getActivities(userId, lastId, limit = 50) {
  const query = { userId };

  if (lastId) {
    query._id = { $lt: lastId };
  }

  return Activity.find(query)
    .sort({ _id: -1 })
    .limit(limit);
}

3. Add Request-Level Caching

// Redis for shared cache across instances
const Redis = require('ioredis');
const redis = new Redis();

async function getCachedData(key, ttl, fetchFn) {
  const cached = await redis.get(key);
  if (cached) return JSON.parse(cached);

  const data = await fetchFn();
  await redis.setex(key, ttl, JSON.stringify(data));
  return data;
}

4. Consider Pre-Aggregation

For dashboards that don't need real-time data:

// Nightly job to pre-compute daily summaries
const dailySummary = await Activity.aggregate([
  { $match: { timestamp: { $gte: yesterday, $lt: today } } },
  { $group: {
    _id: { userId: '$userId', date: { $dateToString: { format: '%Y-%m-%d', date: '$timestamp' } } },
    count: { $sum: 1 },
    totalDuration: { $sum: '$metadata.duration' }
  }},
  { $merge: { into: 'daily_summaries' } }
]);

Dashboard Layout Overview

┌─────────────────────────────────────────────────────────────────┐
│  ACTIVITY DASHBOARD                                    [User ▼] │
├─────────┬───────────────────────────────────────────────────────┤
│         │                                                       │
│  NAV    │   SUMMARY CARDS                                       │
│         │  ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐  │
│ Overview│  │  Total   │ │  Today   │ │  Week    │ │  Trend   │  │
│ Reports │  │  12,453  │ │    47    │ │   312    │ │   +12%   │  │
│ Settings│  └──────────┘ └──────────┘ └──────────┘ └──────────┘  │
│         │                                                       │
│         │   ACTIVITY HEATMAP                                    │
│         │  ┌─────────────────────────────────────────────────┐  │
│         │  │ Jan  Feb  Mar  Apr  May  Jun  Jul  Aug  Sep ... │  │
│         │  │ ░░░▓░░▓▓░░░▓▓▓░░░▓░▓▓░▓▓▓░░░░▓▓░░▓▓▓▓░░░▓▓░░░  │  │
│         │  │ ░▓▓░░▓░░▓▓░░░▓░▓▓░░▓▓▓░░░▓▓▓░░░▓░░▓░░▓▓▓░░▓▓▓  │  │
│         │  │ ... (7 rows for days of week)                   │  │
│         │  └─────────────────────────────────────────────────┘  │
│         │                                                       │
│         │   RECENT ACTIVITY                                     │
│         │  ┌─────────────────────────────────────────────────┐  │
│         │  │ Date       │ Action  │ Product    │ Duration    │  │
│         │  ├────────────┼─────────┼────────────┼─────────────┤  │
│         │  │ 2024-01-15 │ view    │ Product A  │ 45s         │  │
│         │  │ 2024-01-15 │ click   │ Product B  │ 12s         │  │
│         │  │ 2024-01-14 │ download│ Product A  │ 3s          │  │
│         │  └─────────────────────────────────────────────────┘  │
│         │                                                       │
└─────────┴───────────────────────────────────────────────────────┘

Final Thoughts

This project was my crash course in real-world development. I learned that:

  • Performance matters even for internal tools
  • Indexes are your best friend in MongoDB
  • Aggregation pipelines are more powerful than you think
  • Simple stacks (EJS, jQuery) can build functional products
  • Done is better than perfect when you're learning

Two months of building this dashboard taught me more than months of tutorials could. The 3-5 second load times were painful, but debugging them taught me to think about data flow, database optimization, and user experience.

If you're early in your career and get a challenging project like this—embrace it. The struggle is where the learning happens.


Have questions about building dashboards with Node.js and MongoDB? Feel free to reach out on LinkedIn or Twitter.

Building a GitHub-Style Activity Dashboard: Lessons from My First Internship - Ravi Ranjan