При запуске MVP считаем вначале не клики вообще, а деньги и время. Деньги потому, что до серьёзных вложений полезно быстро и по возможности бесплатно проверить, нужен ли проект рынку. Время потому, что его легко потратить не на сам MVP, а на подключение Яндекс.Метрики, Google Analytics, событий, воронок, отдельной базы и прочей обвязки. В итоге идея ещё не проверена, а вокруг неё уже начинает расти аналитическая система.

Рассмотрим простую схему с 1-2 быстрыми метриками, которые напрямую проверяют УТП или главный пользовательский сценарий. Пользователь нажал кнопку покупки. Начал создавать проект. Зарегистрировался. Перешёл в Telegram. Этого уже хватает, чтобы понять, работает ли сценарий и есть ли живой отклик.

Получаем сразу три плюса. Бесплатно проверяем гипотезу, экономим время на старте и делаем один универсальный инструмент, который потом можно использовать для любого количества своих MVP без новых подключений и переделок.

Разберем именно такой вариант. Маленький Django-бэк один раз деплоится на простом хостинге, принимает события через пиксель, хранит их в SQLite и отдаёт статистику JSON-ответом. Дальше во всех новых фронтах меняются только названия event и src.

Особенно удобно это в тех случаях, когда фронт живёт на бесплатном или засыпающем хостинге. У free web services на Render сервис уходит в spin-down после 15 минут простоя, а файловая система там ephemeral, поэтому локальный SQLite для таких счётчиков работать не будет. В качестве простого примера отдельного маленького бэка можно использовать PythonAnywhere, где есть бесплатный аккаунт с одним web app. Но сама идея не привязана к этим площадкам и повторяется практически где угодно.

Фронт отправляет запрос на t.gif, бэк увеличивает счётчик в таблице event + src, а потом по закрытому ключу можно посмотреть JSON со всей накопленной статистикой.

Frontend → /t.gif?e=...&src=... → Django → SQLite
Просмотр результата → /api/stats?k=...

Параметр e это имя события, src это источник, экран, проект или любой удобный маркер. Например, один и тот же бэк может одновременно собирать такие события:

buy_click + landing_page
create_project_click + demo_project
signup_click + mvp_app
telegram_open + portfolio_site

Бэк разворачивается один раз и дальше живёт сам по себе. На новых MVP меняются только вызовы на фронте.

Модель

Данных здесь нужно совсем немного: eventsrccountupdated_at.

# 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}"

Одна строка в таблице соответствует одной паре event + src.

Минимальный settings.py

Для идеи показываем только базу, хосты и ключ для чтения статистики.

# pythonanywhere_hub/settings.py
from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent

ALLOWED_HOSTS = ["127.0.0.1", "localhost", "your-domain.com"]

DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.sqlite3",
        "NAME": BASE_DIR / "db.sqlite3",
    }
}

TRACKER_ADMIN_KEY = "read_stats_key_123"

Этот ключ можно вынести в .env, но для самого паттерна это уже вторично.

Endpoint для записи события

Запись делается через пиксель. Это удобно, потому что не нужно тащить лишнюю CORS-обвязку или отдельный POST-клиент. Фронт просто создаёт new Image() и отправляет запрос на t.gif.

# 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=="
)


def _clean(value: str, max_len: int) -> str:
    value = (value or "").strip()
    return value[:max_len] if value else ""


def _gif_response() -> HttpResponse:
    response = HttpResponse(_GIF_1x1, content_type="image/gif")
    response["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0"
    return response


@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()


@require_GET
def stats(request):
    admin_key = request.GET.get("k", "")
    if admin_key != getattr(settings, "TRACKER_ADMIN_KEY", ""):
        return JsonResponse({"detail": "Forbidden"}, status=403)

    src = _clean(request.GET.get("src", ""), 120)
    event = _clean(request.GET.get("e", ""), 120)

    qs = Counter.objects.all().order_by("-count")

    if src:
        qs = qs.filter(src=src)
    if event:
        qs = qs.filter(event=event)

    data = [
        {
            "event": c.event,
            "src": c.src,
            "count": c.count,
            "updated_at": c.updated_at.isoformat(),
        }
        for c in qs[:500]
    ]
    return JsonResponse({"items": data})

Две важные детали. Во-первых, запись делается атомарно через F("count") + 1. Во-вторых, новый фронт не требует никаких изменений на бэке. Достаточно просто начать отправлять новые event и src.

Маршруты

# tracker/urls.py
from django.urls import path
from .views import pixel, stats

urlpatterns = [
    path("t.gif", pixel, name="pixel"),
    path("api/stats", stats, name="stats"),
]

Фронтовый helper

На фронте нужен маленький универсальный helper, который можно переносить из проекта в проект без изменений.

// 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()}`;
};

Пример для Next.js

Допустим, в MVP важно понять, нажимают ли пользователи на кнопку создания проекта. Добавляем в код одну строку с track и вызов будет выглядеть так:

// 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");
        }}
      >
        Создать проект
      </div>
    </section>
  );
}

После этого статистику можно посмотреть сразу в браузере простым GET-запросом:

https://your-domain.com/api/stats?k=read\_stats\_key\_123

Ответ может выглядеть так:

{
  "items": [
    {
      "event": "buy_click",
      "src": "landing_page",
      "count": 8,
      "updated_at": "2026-03-20T19:41:02.000000+00:00"
    },
    {
      "event": "create_project_click",
      "src": "demo_project",
      "count": 5,
      "updated_at": "2026-03-21T08:30:11.000000+00:00"
    },
    {
      "event": "telegram_open",
      "src": "portfolio_site",
      "count": 3,
      "updated_at": "2026-03-18T15:17:58.000000+00:00"
    },
    {
      "event": "signup_click",
      "src": "mvp_app",
      "count": 2,
      "updated_at": "2026-03-16T19:45:21.000000+00:00"
    }
  ]
}

                  

Это и есть главное практическое преимущество такого решения. Один маленький бэк может обслуживать сразу несколько фронтов, лендингов, демо-проектов и MVP. Деплой делается один раз, а потом вся новая работа происходит уже на стороне фронта.

Такой трекер удобен именно для MVP

Такой подход заставляет точно сформулировать, какое действие действительно считается ключевым. Не абстрактную вовлечённость, не красивые цифры вообще, а конкретный переход, клик или шаг, без которого сама гипотеза ничего не значит.

Заодно получается ещё несколько бонусов. Backend делается один раз и потом переиспользуется. Не нужна отдельная аналитическая база. Не нужно подключать тяжёлые SDK. Результат можно быстро посмотреть прямо в JSON. На старте этого часто достаточно, чтобы получить большую часть практической пользы без полноценной аналитической системы.

Понятно, что у такого подхода есть пределы. Он не заменяет воронки, retention, сегментацию, уникальных пользователей, A/B-тесты и полноценную продуктовую аналитику. Но для стадии MVP это обычно и не требуется. Там важнее как можно быстрее и дешевле понять, происходит ли ключевое действие вообще.

Если проект пойдёт дальше, сервис легко дорастить. Можно добавить фильтрацию по дням, выгрузку CSV, простую защиту от слишком частых запросов, отдельную админ-страницу или перенос на PostgreSQL. Но всё это уже следующий этап. На старте важнее сначала убедиться, что сам сценарий работает.

Подытожим.

Для MVP одна сильная метрика часто полезнее, чем двадцать второстепенных. Если продукт только проверяет гипотезу, большой аналитический стек на старте не всегда помогает. Иногда он просто съедает время и внимание. Гораздо полезнее бывает сделать один маленький универсальный инструмент, который считает ключевое действие и переиспользуется во всех новых проектах. Если нужное событие происходит, значит в сценарии есть жизнь. Если нет, дорабатывать, скорее всего, нужно не дашборд, а сам MVP.