Engineering Blog

Technical insights from Grid Dynamics engineers

Self-Service Analytics: How I Automated Daily Usage Reports in Under 3 Hours

Self-Service Analytics: How I Automated Daily Usage Reports in Under 3 Hours

Volodymyr Voyevidka · Nov 18, 2025

The Challenge: Flying Blind

We built Cerebra—an internal AI platform for Grid Dynamics. Engineers were using it daily, chatting with AI agents, solving problems, experimenting with different approaches. But we had a critical blind spot: we had no idea how much it was actually being used.

How many conversations happened yesterday? Which features were people gravitating toward? Who were the power users? These weren't just vanity metrics—they were essential questions for understanding product-market fit and guiding our development priorities.

The traditional path would be familiar to anyone in a large organization: write requirements, create a ticket, assign it to someone, wait for capacity, review designs, iterate, wait some more. Best case? A few weeks. Realistic case? A few months.

I decided to take a different path: build it myself in a few hours.

The Self-Service Decision

Here's what I had going for me:

  • Cerebra already stored chat history in Firestore (Google's NoSQL database)
  • We had N8N deployed (an open-source workflow automation platform)
  • I had an AI pair programmer (Gemini) available 24/7

The goal was simple: wake up every morning to an email with yesterday's usage statistics. No dashboards to check, no queries to run—just actionable insights delivered automatically.

Timeline? Started at 9 PM, had the first report in my inbox by 11:30 PM. Just 2.5 hours from idea to working automation.

Daily cerebra usage report email

Building with an AI Pair Programmer

This is where the story gets interesting. I didn't start by opening N8N and figuring everything out from scratch. Instead, I opened a chat with Gemini and described what I wanted to build.

My opening prompt was straightforward:

"I have Cerebra, which stores chat history in Firestore. I want to create an N8N pipeline to calculate stats and send a daily email with usage metrics."

What followed was an iterative design session. Gemini proposed the initial architecture:

  1. Three parallel Firestore queries for different time periods:

    • Last Business Day (LBD) - yesterday's activity
    • Current Week (CW) - week-to-date trends
    • Current Month (CM) - monthly patterns
  2. Data aggregation to calculate metrics per period

  3. Merge the results into a single report

  4. Format as HTML email and send

Gemini ChatGemini Chat Continued
Screenshot showing the initial conversation with Gemini about the architectureScreenshot showing the initial conversation with Gemini about the architecture

The Iteration Dance

The first design worked, but had issues. The real value came from iterating with Gemini:

Challenge 1: Merge Node Confusion

My first attempt used the Merge Node's "Combine" mode with "All Possible Combinations" setting. It created random combinations instead of a clean data structure.

Gemini's solution: Switch to "Append" mode, which preserves order based on connection sequence.

Challenge 2: Message Counting Logic

Initially, the code counted both user messages AND agent responses. For usage analytics, I only cared about user activity—how many questions people were asking.

My feedback to Gemini:

"I don't like the implementation of total messages. I'm not interested in how many messages came from agent, only how many user messages were submitted."

Gemini immediately refactored the counting logic to filter for msg.actor === 'User' only.

Challenge 3: Report Layout

The first email was a wall of text. I wanted better visual organization.

My request:

"I want for each section to have top users and usage by agent in two columns side by side."

Gemini redesigned the HTML to use a table-based two-column layout for maximum email client compatibility.

Screenshot showing one of the key iterations, like the two-column layout discussion

This back-and-forth took maybe 30 minutes. Each iteration made the solution better, and I never had to wait for "someone" to be available.

The Technical Implementation

Let me walk you through what actually got built. Don't worry—this isn't a full tutorial, but understanding the architecture shows how simple modern automation can be.

The N8N Pipeline

The workflow looks like this:

[Schedule Trigger - Daily 8AM] [Firestore: Query LBD] ──┐ [Firestore: Query CW] ──┼─→ [Aggregate Nodes] ──→ [Merge Node] [Firestore: Query CM] ──┘ ↓ [Code: Process Data] [Code: Generate HTML] [Send Email]

Screenshot of the actual N8N canvas showing the nodes and connections

Three parallel Firestore queries pull chat sessions for each time period. Each query filters by date range using simple date math (N8N expressions make this trivial).

Aggregate nodes convert the Firestore response arrays into clean single objects with a data field.

The Merge Node combines all three into one array, preserving order.

The Core Logic: Processing Usage Data

Here's where the magic happens—a single JavaScript function that processes chat sessions and extracts meaningful metrics:

function processPeriodData(chatSessions) { if (!chatSessions || chatSessions.length === 0) { return { uniqueChats: 0, uniqueUsers: 0, totalMessages: 0, messagesPerUser: 0, topUsers: [], usageByAgent: [], }; } const metrics = { uniqueChats: chatSessions.length, uniqueUsers: new Set(), totalMessages: 0, userMessageCounts: {}, agentUsage: {}, }; for (const session of chatSessions) { const userId = session.userId; metrics.uniqueUsers.add(userId); // Count ONLY user messages (not agent responses) const userMessagesCount = session.messages ? session.messages.filter((msg) => msg.actor === "User").length : 0; metrics.totalMessages += userMessagesCount; metrics.userMessageCounts[userId] = (metrics.userMessageCounts[userId] || 0) + userMessagesCount; const agentName = session.agentName || "Unknown Agent"; metrics.agentUsage[agentName] = (metrics.agentUsage[agentName] || 0) + 1; } metrics.messagesPerUser = metrics.uniqueUsers.size > 0 ? (metrics.totalMessages / metrics.uniqueUsers.size).toFixed(1) : 0; const topUsersArray = Object.entries(metrics.userMessageCounts) .map(([userId, count]) => ({ userId, count })) .sort((a, b) => b.count - a.count) .slice(0, 10); return { uniqueChats: metrics.uniqueChats, uniqueUsers: metrics.uniqueUsers.size, totalMessages: metrics.totalMessages, messagesPerUser: metrics.messagesPerUser, topUsers: topUsersArray, usageByAgent: Object.entries(metrics.agentUsage) .map(([agentName, count]) => ({ agentName, count })) .sort((a, b) => b.count - a.count), }; }

This function runs three times—once for each time period—producing a structured report object.

From Data to Email

The final step transforms the report into a clean HTML email. Using table-based layouts (for email client compatibility), the template creates a professional report with:

  • Emoji section headers (🗓️ Yesterday, 📈 This Week, 📅 This Month)
  • Metric tables showing key numbers at a glance
  • Two-column layouts for Top Users and Agent Usage
  • Sorted lists so the most important data stands out

Screenshot of the actual email as received, showing the full report with all three sections

Every morning at 8 AM, N8N runs this pipeline automatically. Four minutes later, the report lands in my inbox.

What I Learned

Ten years ago, this would have required backend developers, database admins, and DevOps engineers. Today? A workflow tool, JavaScript, and an AI assistant. The bottleneck isn't technical anymore—it's permission and access. Organizations that give teams self-service tools will move faster than those that gatekeep everything.

The AI didn't write this for me—it compressed a 2.5-hour project into what might have been 8 hours without it through collaborative problem-solving at the speed of thought. I still made all the decisions about metrics, structure, and when to ship. The first version was basic (no charts, no fancy dashboards), and that was perfect. I shipped it the same evening, got feedback, and can now iterate.

Here's the truth: the best code is code you don't wait for. When you can solve your own problems with self-service tools, you eliminate queue time entirely and learn more by building than by writing tickets.

The Bigger Picture

This wasn't just about getting usage stats for Cerebra. It was a proof point for how work can—and should—happen in 2025:

Problems should be solved by the people closest to them, using accessible tools, with AI assistance, without requiring approvals or tickets for every small automation.

This is the promise of the self-service revolution: anyone can build solutions to their own problems. You don't need to be a software engineer (though it helps). You don't need permissions from five different teams. You need:

  • Tools that are accessible (N8N, Zapier, Make.com)
  • Data that's queryable (Firestore, SQL databases, APIs)
  • AI that can help design solutions (Gemini, ChatGPT, Claude)
  • A culture that encourages experimentation

Conclusion: From Idea to Production in One Evening

When I started (9 PM)By 11:30 PM
A problem (no visibility into Cerebra usage)A working pipeline
Access to data (Firestore)A clean report in my inbox
Access to tools (N8N)Actionable insights about our platform
An AI assistant (Gemini)A repeatable pattern for future automation

No tickets. No dependencies. No waiting.

This is how modern development should feel. Fast, autonomous, iterative. The tools are ready. The AI assistants are available. The only question is: are you empowered to use them?

If you're in a position to give your team this kind of access—do it. If you're on a team that has these tools—use them. The gap between "I wish we had data on this" and "here's the automated daily report" has collapsed to an evening.

Go build something.


Want to learn more about self-service automation and AI-assisted development? Read our take on methodology in AI-assisted development, or explore our agentic frameworks overview to see how we're pushing these capabilities even further.