Back to Home

Event tracker for MVP on Django and SQLite: fast analytics

The article describes creating a minimalist event tracker for MVP on Django and SQLite. The solution allows quick collection of key metrics via pixel, saves time and resources at project start. Code examples for backend and Next.js frontend are provided.

Lightweight event tracker: Django + SQLite for fast MVP
Advertisement 728x90

Minimalist Event Tracker for MVP: Django, SQLite, and Tracking Pixel

When launching an MVP, it's crucial to quickly and cost-effectively validate key user actions. Instead of integrating heavy analytics platforms, deploy a lightweight Django backend that collects events via a tracking pixel and stores them in SQLite. This approach saves time, skips complex infrastructure, and lets you focus on testing your core hypothesis.

Architecture of a Simple Event Tracker

The solution centers on a minimalist backend deployed once on a hosting service to support multiple frontends. The frontend sends a GET request to the /t.gif endpoint with event and source parameters. The backend atomically increments a counter in the database. View stats via the protected /api/stats endpoint, which returns JSON data.

Key system components:

Google AdInline article slot
  • Frontend: Calls track(event, src) by creating an Image object.
  • Backend (Django): Handles the request and updates the counter in SQLite.
  • Database: Single table with event, src, count, and updated_at fields.
  • Stats API: Key-authenticated access to aggregated data.

This tracker shines for projects on free hosting where the filesystem is ephemeral and services sleep after inactivity.

Implementation with Django and SQLite

The data model is dead simple, using a unique index on the event and src pair.

# tracker/models.py
from django.db import models

class Counter(models.Model):
    event = models.CharField(max_length=120)
    src = models.CharField(max_length=120, blank=True, default="")
    count = models.PositiveBigIntegerField(default=0)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        unique_together = ("event", "src")

    def __str__(self) -> str:
        return f"{self.src}:{self.event}={self.count}"

The event logging endpoint serves a 1×1 base64-encoded pixel to sidestep CORS issues and skip extra client libraries.

Google AdInline article slot
# tracker/views.py
import base64

from django.conf import settings
from django.db import transaction
from django.db.models import F
from django.http import HttpResponse, JsonResponse
from django.views.decorators.http import require_GET

from .models import Counter

_GIF_1x1 = base64.b64decode(
    "R0lGODlhAQABAPAAAAAAAAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=="
)

@require_GET
def pixel(request):
    event = _clean(request.GET.get("e", ""), 120)
    src = _clean(request.GET.get("src", ""), 120)

    if not event:
        return _gif_response()

    with transaction.atomic():
        obj, created = Counter.objects.get_or_create(event=event, src=src)

        if created:
            obj.count = 1
            obj.save(update_fields=["count"])
        else:
            Counter.objects.filter(pk=obj.pk).update(count=F("count") + 1)

    return _gif_response()

Atomic updates via F("count") + 1 ensure data integrity under concurrent requests. The stats endpoint filters by event and source if parameters are provided.

Frontend Integration with Next.js

On the client side, a portable helper function works across projects unchanged.

// src/utils/track.ts
const TRACK_BASE = "https://your-domain.com/t.gif";

export const track = (event: string, src: string) => {
  const img = new Image();
  img.src =
    `${TRACK_BASE}?` +
    `e=${encodeURIComponent(event)}` +
    `&src=${encodeURIComponent(src)}` +
    `&t=${Date.now()}`;
};

Example in a Next.js component to track project creation button clicks:

Google AdInline article slot
// src/app/_ui/HomePageClient.tsx
"use client";

import { useRouter } from "next/navigation";
import { track } from "@/utils/track";

export default function HomePageClient() {
  const router = useRouter();

  return (
    <section className="mt-8 space-y-2">
      <div
        className="app-card app-card--soft cursor-pointer hover:border-slate-500/70 hover:bg-white/5 transition-colors"
        role="button"
        tabIndex={0}
        onClick={() => {
          router.push("/demo");
          track("create_project_click", "demo_project");
        }}
      >
        Create Project
      </div>
    </section>
  );
}

Fetch stats by hitting /api/stats with an auth key. The response lists events with trigger counts and last update times.

Pros and Cons of This Approach

A lightweight tracker for MVPs offers practical wins:

  • Fast Hypothesis Testing: Zero in on one key action instead of endless metrics.
  • Resource Savings: No need for bloated analytics tools or separate databases.
  • Versatility: One backend serves multiple frontends and projects.
  • Easy Deployment: Minimal hosting needs—perfect for free tiers.

But it has natural limits:

  • No unique user or session tracking.
  • Lacks funnels, retention, or segmentation.
  • No A/B testing or advanced reports.

Add those later, once your MVP proves viable.

Key Takeaways

  • One Metric Beats Twenty: For MVPs, nail one core action that tests your main hypothesis.
  • Lean Infrastructure: SQLite and a pixel dodge deployment headaches.
  • Atomic Operations: F("count") + 1 keeps data solid under load.
  • Portable Helper: 10 lines of client code fit any modern frontend framework.
  • Quick Launch: Deploy in hours and start collecting data right away.

This tracker won't replace full analytics suites, but it cheaply answers the big question: Is the key action happening? If not, the issue's likely the product, not missing metrics.

— Editorial Team

Advertisement 728x90

Read Next