ساخت یک اپ ردیابی بازدیدکننده در جنگو، اولین بار که با آن مواجه می‌شوی ساده به نظر می‌رسد؛ ولی به‌محض اینکه سایت به ترافیک واقعی می‌رسد، جزئیاتی مثل کوئری‌های N+1، قفل‌های دیتابیس، session، GDPR (General Data Protection Regulation) و انتخاب الگوی معماری، همه با هم ظاهر می‌شوند. این مقاله مسیر کامل ساخت چنین اپی را بر اساس تجربه‌ی عملی چند پروژه‌ی واقعی نشان می‌دهد.

چرا یک اپ اختصاصی و نه ابزار آماده؟

سؤال اولی که هر تیم فنی از خودش می‌پرسد این است: «چرا خودمان بنویسیم وقتی Google Analytics، Plausible یا Matomo وجود دارند؟» پاسخ کوتاه این است که این ابزارها برای تحلیل کسب‌وکار عالی‌اند، ولی برای سه سناریوی خاص، اپ اختصاصی انتخاب درست است.

سناریوی اول، مالکیت داده. وقتی داده‌ی رفتاری کاربران در سرور شخص ثالث ذخیره می‌شود، کنترل کامل روی نگهداری، پاک‌سازی و انطباق با قوانین نداری. برای سایت‌های ایرانی که ممکن است به‌دلیل محدودیت‌های بین‌المللی دسترسی به سرویس‌های خارجی نداشته باشند، این مسئله جدی است.

سناریوی دوم، یکپارچگی با مدل کسب‌وکار. وقتی می‌خواهی آمار رفتاری را با مدل‌های داخلی مثل کاربر، سفارش، یا اشتراک گره بزنی، ابزارهای آماده محدودیت دارند. با اپ اختصاصی می‌توانی همه را در یک کوئری SQL ترکیب کنی.

سناریوی سوم، کنترل هزینه و مقیاس. ابزارهای SaaS بر اساس ترافیک قیمت‌گذاری می‌شوند و در سایت‌های پربازدید سریع گران می‌شوند. یک اپ داخلی روی همان زیرساخت سایت اجرا می‌شود.

انتخاب بین ابزار آماده و اپ اختصاصی، یک تصمیم مهندسی نیست؛ یک تصمیم کسب‌وکاری است که باید بر اساس مالکیت داده، بودجه و نیازهای یکپارچگی گرفته شود.

پیش‌نیازهای فنی و معماری

قبل از نوشتن حتی یک خط کد، این پیش‌نیازها را در نظر داشته باش:

۱. نسخه‌ی جنگو. این ساختار با جنگو ۴.۲ و ۵.۰ تست شده است. برای جنگو ۳.۲ هم کار می‌کند، ولی توصیه می‌کنم به نسخه‌ای مهاجرت کنی که پشتیبانی بلندمدت دارد.

۲. دیتابیس. PostgreSQL انتخاب پیشنهادی است، چون از JSONField، پارتیشن‌بندی و کوئری‌های برداری پشتیبانی می‌کند. MySQL با نسخه‌ی ۵.۷ به بالا هم کار می‌کند، ولی برای جدول‌های بزرک پارتیشن‌بندی دشوارتر است.

۳. Redis یا Memcached. برای کش کردن آمارهای سنگین و session backend. اگر روی SQLite کار می‌کنی، حداقل برای کش از Redis استفاده کن.

۴. ساختار اپ. اپی که می‌سازیم مستقل است و به هیچ اپ دیگری وابستگی ندارد. این استقلال باعث می‌شود در آینده بتوانی آن را به یک پکیج قابل استفاده تبدیل کنی.

اگر پیش از این با تعریف و کاربرد services.py در جنگو آشنا شده باشی، منطق کسب‌وکار را از ابتدا درست می‌چینی. در این اپ، منطق کسب‌وکار سبک است، ولی مدیریت داده سنگین است. یعنی بخش اعظم کد در لایه‌ی مدل و middleware خواهد بود، نه در services.

ساخت اپ و تنظیمات اولیه

اولین قدم، ساخت اپ است:

python manage.py startapp analytics

سپس اپ را در settings.py اضافه کن:

INSTALLED_APPS = [
    # ...
    "analytics",
]

حالا بیایید تنظیمات اختصاصی اپ را در یک بلوک مجزا قرار بدهیم تا با بقیه‌ی تنظیمات پروژه قاطی نشود:

# =========================================================
# Analytics
# =========================================================

ANALYTICS_ONLINE_TIMEOUT = 300
ANALYTICS_SESSION_TIMEOUT = 1800
ANALYTICS_TRACK_BOTS = False

ANALYTICS_SKIP_PATHS = (
    "/admin/",
    "/panel/",
    "/static/",
    "/media/",
    "/favicon.ico",
    "/robots.txt",
    "/sitemap.xml",
)

ANALYTICS_EXCLUDE_IPS = ()
ANALYTICS_RETENTION_DAYS = 90

این تنظیمات، تمام نقاط قابل تغییر اپ را در یک جا جمع می‌کند. اگر بعداً خواستی اپ را به یک پکیج قابل استفاده تبدیل کنی، فقط باید مقادیر پیش‌فرض را تغییر بدهی.

نکته‌ی مهم درباره‌ی ANALYTICS_SKIP_PATHS: هرگز مسیر /panel/ را فراموش نکن. اگر پنل ادمین را مستثنی نکنی، خودت هم به‌عنوان بازدیدکننده ثبت می‌شوی و آمار را آلوده می‌کنی. این اشتباه را در پروژه‌های زیادی دیده‌ام که تازه شروع کرده بودند.

طراحی مدل‌های داده

مدل‌های این اپ، ستون فقرات کل سیستم هستند. سه مدل اصلی داریم: Visitor، Visit و PageView. اگر می‌خواهی جزئیات دقیق این مدل‌ها را ببینی، مقاله‌ی طراحی مدل Visitor و Visit به‌طور اختصاصی به آن پرداخته است. در اینجا خلاصه‌ای از تصمیم‌های کلیدی را مرور می‌کنیم.

تصمیم اول، جدایی Visitor از Visit. یک Visitor، یک هویت یکتا است (بر اساس fingerprint از IP و User-Agent). یک Visit، یک سشن است. این جدایی باعث می‌شود بتوانی بگویی «کاربر X در ۳۰ روز گذشته ۱۲ بار به سایت آمده» بدون اینکه بخواهی هر بار یک Visitor جدید بسازی.

تصمیم دوم، استفاده از PositiveIntegerField برای زمان‌ها. برای duration_seconds از PositiveIntegerField استفاده می‌کنیم، نه DurationField. دلیلش این است که تجمیع (aggregate) روی عدد صحیح در همه‌ی دیتابیس‌ها سریع‌تر و پایدارتر است.

تصمیم سوم، JSONField برای metadata. در مدل Interaction، یک فیلد metadata از نوع JSONField داریم که امکان ذخیره‌ی ساختارهای متنوع را بدون migration می‌دهد. این انعطاف، هزینه‌ی query روی JSON را دارد، پس فقط برای داده‌های جانبی از آن استفاده کن.

مدلهدفحجم تخمینی
Visitorهویت یکتای بازدیدکنندهکم (یکی به‌ازای هر کاربر یکتا)
Visitهر سشن بازدیدمتوسط (چند برابر Visitor)
PageViewهر بازدید از یک صفحهزیاد (چند برابر Visit)
Interactionهر تعامل با صفحهبسیار زیاد

برای مدل PageView و Interaction، حتماً ایندکس ترکیبی بگذار. در پروژه‌ای که جدول PageView به ۴۰ میلیون ردیف رسید، نبود یک ایندکس مناسب روی (visit_id, entered_at) باعث شده بود کوئری گزارش هفتگی ۱۸ ثانیه طول بکشد. بعد از افزودن ایندکس، به ۲۲۰ میلی‌ثانیه رسید.

نکته‌ی مهم درباره‌ی ذخیره‌ی Interaction: در پروژه‌های پربازدید، جدول Interaction سریع‌ترین رشد را دارد، چون هر کلیک و هر اسکرول یک ردیف می‌سازد. اگر با مشکل حجم مواجه شدی، به حذف رکوردهای تکراری و یتیم با batch delete مراجعه کن که الگوی پاک‌سازی این نوع جدول‌ها را نشان می‌دهد.

Middleware؛ قلب اپ ردیابی

Middleware جایی است که ردیابی خودکار اتفاق می‌افتد. اگر تا حالا middleware سفارشی ننوشته‌ای، پیشنهاد می‌کنم اول نوشتن Middleware سفارشی در جنگو را مطالعه کنی. اما خلاصه‌ی کار این است:

Middleware در هر درخواست GET انسانی، این کارها را انجام می‌دهد:

۱. چک مسیرهای مستثنی. اگر مسیر در ANALYTICS_SKIP_PATHS باشد، ردیابی نمی‌شود.

۲. چک IP مستثنی. برای IP خودت یا IP‌های داخلی.

۳. رد کردن بات‌ها. با تابع is_bot() که بر اساس regex کار می‌کند. جزئیات این تابع در تشخیص بات از کاربر انسانی توضیح داده شده است.

۴. ساخت یا بازیابی Visitor. بر اساس fingerprint که از IP و User-Agent ساخته می‌شود.

۵. ساخت یا بازیابی Visit. اگر session فعال در کوکی وجود دارد و منقضی نشده، همان استفاده می‌شود. وگرنه یک Visit جدید ساخته می‌شود.

۶. بستن PageView قبلی و ساخت PageView جدید. این کار در هر درخواست صفحه انجام می‌شود.

ترتیب این مراحل حیاتی است. اگر مثلاً Visitor را بعد از Visit بسازی، نمی‌توانی از اطلاعات Visitor در ساخت Visit استفاده کنی. اگر بات را بعد از ساخت Visitor رد کنی، جدول Visitor با رکوردهای بی‌استفاده پر می‌شود.

Middleware اپ ردیابی باید سبک باشد. هر کوئری اضافه در middleware، مستقیماً به زمان پاسخ همه‌ی صفحات سایت اضافه می‌شود.

نکته‌ی ظریف دیگر این است که middleware نباید هرگز باعث خطای 500 شود. همه‌ی عملیات دیتابیس را در try/except بپیچ و در صورت خطا، بی‌صدا از ردیابی صرف‌نظر کن. در پروژه‌ای، یک باگ در محاسبه‌ی fingerprint باعث شده بود همه‌ی درخواست‌ها 500 بدهند. با یک try/except ساده می‌شد جلوی این فاجعه را گرفت.

مسیرها و ساختار URL

ساختار URLها را طوری طراحی کن که با اپ‌های دیگر تداخل نداشته باشد. توصیه می‌کنم همه را زیر یک prefix مشخص قرار بدهی:

# analytics/urls.py

from django.urls import path
from . import views, api

app_name = "analytics"

urlpatterns = [
    path("panel/stat/", views.quick_view, name="quick_view"),
    path("panel/stat/yesterday/", views.yesterday_view, name="yesterday"),
    path("panel/stat/full/", views.full_list, name="full_list"),
    path("panel/stat/online/", views.online, name="online"),
    path("panel/stat/referral/", views.referral_list, name="referral"),
    path("panel/stat/visit//", views.visitor_journey, name="visitor_journey"),
    path("panel/stat/api/track/", api.track, name="api_track"),
]

و در URLconf اصلی پروژه، این اپ را قبل از هر الگوی catch-all قرار بده. این نکته را در پروژه‌های زیادی دیده‌ام که فراموش می‌شود. اگر الگوی <str:slug>/ قبل از include اپ ردیابی باشد، همه‌ی مسیرهای panel/stat/ به آن می‌افتند و خطای 404 می‌گیری. راه‌حل کامل این مشکل در رفع خطای 404 در جنگو توضیح داده شده است.

Beacon API برای داده‌های سمت مرورگر

بعضی داده‌ها فقط از سمت مرورگر قابل گرفتن هستند: رزولوشن صفحه، نوع اتصال، عمق اسکرول، کلیک کاربر و زمان دقیق خروج. برای این‌ها یک endpoint واحد می‌سازیم:

# analytics/api.py

import json
from django.http import JsonResponse
from django.utils import timezone
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST

from .models import Visit, PageView, Interaction
from .utils import is_bot


@csrf_exempt
@require_POST
def track(request):
    if is_bot(request.META.get("HTTP_USER_AGENT", "")):
        return JsonResponse({"ok": False}, status=200)

    try:
        payload = json.loads(request.body or b"{}")
    except Exception:
        payload = {}

    event = payload.get("event", "ping")
    visit_id = request.session.get("_analytics_visit_id")
    pv_id = request.session.get("_analytics_pageview_id")

    visit = Visit.objects.filter(pk=visit_id).first() if visit_id else None
    pv = PageView.objects.filter(pk=pv_id).first() if pv_id else None

    if not visit or not pv:
        return JsonResponse({"ok": False}, status=200)

    now = timezone.now()

    if event == "init":
        visit.screen_width = payload.get("sw") or None
        visit.screen_height = payload.get("sh") or None
        visit.viewport_width = payload.get("vw") or None
        visit.viewport_height = payload.get("vh") or None
        visit.connection_type = (payload.get("conn") or "")[:30]
        visit.save(update_fields=[
            "screen_width", "screen_height",
            "viewport_width", "viewport_height",
            "connection_type",
        ])

    elif event == "ping":
        visit.last_activity = now
        visit.save(update_fields=["last_activity"])

    elif event == "scroll":
        depth = int(payload.get("depth") or 0)
        if depth > pv.scroll_depth:
            pv.scroll_depth = depth
            pv.save(update_fields=["scroll_depth"])

    elif event == "click":
        Interaction.objects.create(
            page_view=pv,
            visit=visit,
            visitor=visit.visitor,
            event_type="click",
            target=(payload.get("target") or "")[:500],
            metadata=payload.get("meta") or {},
            occurred_at=now,
        )

    elif event == "exit":
        pv.left_at = now
        pv.duration_seconds = int((now - pv.entered_at).total_seconds())
        pv.is_exit = True
        pv.save(update_fields=["left_at", "duration_seconds", "is_exit"])

        visit.exit_time = now
        visit.exit_url = (payload.get("exit_url") or "")[:2000]
        visit.duration_seconds = int((now - visit.entry_time).total_seconds())
        visit.is_active = False
        visit.save(update_fields=[
            "exit_time", "exit_url",
            "duration_seconds", "is_active",
        ])

    return JsonResponse({"ok": True})

این endpoint را روی @csrf_exempt گذاشته‌ایم چون مرورگر نمی‌تواند توکن CSRF را در beacon ارسال کند. برای جلوگیری از سوءاستفاده، بهتر است rate limiting اضافه کنی. الگوی دقیق ارسال رویداد خروج با sendBeacon برای ارسال رویداد خروج توضیح داده شده است. برای رویدادهای اسکرول و کلیک هم ثبت اسکرول و کلیک کاربر مرجع کامل است.

نکته‌ی مهم درباره‌ی rate limiting: بدون آن، یک مهاجم ساده می‌تواند با ارسال هزاران درخواست در ثانیه، جدول Interaction را پر کند. حداقل ۶۰ درخواست در دقیقه برای هر IP منطقی است.

پنل ادمین و مشاهده‌ی داده‌ها

داده‌های خام در دیتابیس به‌تنهایی ارزش ندارند؛ چیزی که ارزش می‌سازد، نمایش درست آن‌ها برای تصمیم‌گیری است. حداقل یک پنل ادمین با سه ویژگی لازم داری:

۱. جستجوی سریع. جستجو بر اساس IP، مسیر و Referrer. Django admin این را با search_fields فراهم می‌کند.

۲. فیلتر بر اساس نوع و تاریخ. فیلتر بر اساس device، referrer_type و بازه‌ی زمانی.

۳. نمای سفر کاربر. یک صفحه که مسیر کامل یک Visit را نشان بدهد. اگر می‌خواهی این صفحه را زیباتر بسازی، ساخت پنل ادمین با کارت‌های quick view الگوی خوبی ارائه می‌دهد.

در پروژه‌های واقعی، پنل ادمین پیش‌فرض جنگو برای شروع کافی است. اما به‌محض اینکه تیم بازاریابی یا محصول بخواهد از داده‌ها استفاده کند، نیاز به یک پنل سفارشی با کارت‌های آماری و نمودار پیدا می‌شود. اگر رویکرد Data-Driven داری، این سرمایه‌گذاری به سرعت جواب می‌دهد.

حریم خصوصی، GDPR و مسائل قانونی

ذخیره‌ی IP و User-Agent کاربران، در بسیاری از حوزه‌های قضایی «داده‌ی شخصی» محسوب می‌شود. GDPR اروپا، CCPA کالیفرنیا و قوانین مشابه در کشورهای دیگر، الزاماتی برای ذخیره‌سازی و پردازش این داده‌ها دارند.

سه اصل را رعایت کن:

۱. حداقل داده. اگر برای تحلیل رفتاری به IP دقیق نیاز نداری، آن را هش کن. یک sha256(ip + salt) کافی است که هم یکتا بودن را حفظ کند، هم قابل بازگشت نباشد.

۲. نگهداری محدود. داده‌های رفتاری را برای همیشه نگه ندار. ۹۰ روز نقطه‌ی شروع مناسبی است. برای پاک‌سازی خودکار از کامند مدیریتی پاک‌سازی داده استفاده کن که با یک cron job روزانه اجرا شود.

۳. شفافیت. در سیاست حریم خصوصی سایت، دقیقاً بنویس چه داده‌ای ذخیره می‌کنی و چرا. این کار هم قانونی است، هم اعتماد کاربر را جلب می‌کند.

نکته‌ی مهم: اگر سایت تو مخاطب بین‌المللی دارد، احتمالاً باید با GDPR انطباق داشته باشی، حتی اگر سرور در ایران باشد. اگر مخاطب داخلی است، قوانین ایران در حوزه‌ی داده‌های شخصی در حال تکامل است و بهتر است محتاط باشی.

امنیت اپ ردیابی

یک اپ ردیابی، سطح حمله‌ی جدیدی به سایت اضافه می‌کند. حداقل این چهار مورد را رعایت کن:

۱. Rate limiting روی beacon. از django-ratelimit یا یک middleware سبک استفاده کن.

۲. اعتبارسنجی ورودی. همه‌ی فیلدهای ورودی از beacon را در سمت سرور چک کن. طول، نوع، محدوده.

۳. دسترسی به پنل. همه‌ی viewهای آماری باید با @staff_member_required محافظت شوند. برای محدودتر کردن، از permission سفارشی استفاده کن.

۴. لاگ‌گیری. هر دسترسی به داده‌های حساس را لاگ کن. اگر فردی به IP کاربران دسترسی پیدا کرد، باید بتوانی ردش را بزنی.

هر داده‌ای که ذخیره می‌کنی، یک بدهی امنیتی است. اگر به آن داده نیاز نداری، ذخیره نکن.

تست‌پذیری و ابزارهای دیباگ

اپ ردیابی به‌خاطر ماهیت خود (نوشتن در دیتابیس در هر درخواست)، تست‌پذیری چالش‌برانگیزی دارد. سه راهبرد را در نظر بگیر:

۱. جداسازی منطق از middleware. منطق ساخت Visitor و Visit را در توابع مستقل بنویس و middleware فقط این توابع را صدا بزند. این‌طوری می‌توانی بدون ساخت request واقعی، منطق را تست کنی.

۲. استفاده از pytest-django. با fixtureهای سبک می‌توانی Visitor و Visit بسازی و سناریوهای مختلف را تست کنی.

۳. Debug toolbar. تعداد کوئری‌های هر صفحه را با django-debug-toolbar ببین. هدف این است که اضافه شدن اپ ردیابی، بیشتر از ۵ کوئری به هر صفحه اضافه نکند.

نکته‌ی مهمی که در پروژه‌های زیادی دیده‌ام: بعضی تیم‌ها تست middleware را جدی نمی‌گیرند و بعد از مدتی، باگ‌هایی پیدا می‌شود که ماه‌ها در production بوده‌اند. حداقل یک تست برای هر سناریو (اولین بازدید، بازدید تکراری، بازدید بات، session منقضی) بنویس.

عملکرد در ترافیک بالا

در سایت‌های پربازدید، اپ ردیابی می‌تواند به گلوگاه تبدیل شود. راهکارهای عملی:

۱. کاهش UPDATEهای اضافی. به‌جای آپدیت last_activity در هر درخواست، فقط اگر بیشتر از ۳۰ ثانیه گذشته آپدیت کن.

۲. Session در Redis. اگر session روی دیتابیس باشد، هر درخواست یک SELECT و یک UPDATE اضافه دارد. با Redis همه در RAM است. اگر قصد بهینه‌سازی جدی داری، بهینه‌سازی جنگو برای ترافیک بالا چک‌لیست کاملی ارائه می‌دهد.

۳. Write-behind با Celery. در مقیاس بزرگ، می‌توانی نوشتن‌ها را در Redis بافر کنی و Celery هر چند ثانیه آن‌ها را در دسته به دیتابیس منتقل کند. این الگو، کوئری‌های INSERT را از O(N) به O(1) کاهش می‌دهد.

۴. پارتیشن‌بندی جدول. وقتی جدول PageView به چند ده میلیون ردیف رسید، پارتیشن‌بندی ماهانه بر اساس entered_at کمک بزرگی می‌کند. در PostgreSQL با declarative partitioning و در MySQL با RANGE partitioning.

۵. جداسازی دیتابیس read و write. اگر read و write را روی دیتابیس‌های جداگانه ببری، فشار آماری را از دیتابیس اصلی جدا کرده‌ای. این کار با database router در جنگو امکان‌پذیر است.

استقرار در production

قبل از استقرار در production، چک‌لیست زیر را کامل کن:

۱. تنظیم DEBUG = False. این واضح است ولی در پروژه‌های زیادی فراموش می‌شود.

۲. تنظیم ALLOWED_HOSTS. دامنه‌ی خود را اضافه کن.

۳. SECURE_PROXY_SSL_HEADER. اگر پشت Nginx یا Cloudflare هستی، این خط حیاتی است. بدون آن، IP همه‌ی کاربران 127.0.0.1 ثبت می‌شود.

۴. Backup خودکار. جدول‌های آماری می‌توانند بزرگ شوند. یک استراتژی backup تعریف کن.

۵. Alerting. اگر اپ ردیابی خراب شود، باید متوجه شوی. یک هشدار روی «نبود رکورد جدید در ۳۰ دقیقه گذشته» تعریف کن.

۶. مانیتورینگ حجم. رشد جدول‌ها را رصد کن. اگر جدول Interaction ماهانه ۵۰٪ رشد دارد، برنامه‌ی پاک‌سازی داشته باش.

یک نکته‌ی عملی که در چند پروژه استفاده کردم و مفید بوده: در روزهای اول، اپ را در حالت «dry-run» اجرا کن. یعنی داده‌ها را در یک دیتابیس جدا ذخیره کن و با ترافیک واقعی، حجم واقعی را اندازه بگیر. این کار از شگفتی‌های ماه دوم جلوگیری می‌کند.

anti-patternهای رایج

در بازبینی اپ‌های ردیابی مختلف، این anti-patternها را زیاد دیده‌ام:

۱. ذخیره‌ی User-Agent در قالب CharField کوتاه. User-Agent می‌تواند بیش از ۵۰۰ کاراکتر باشد. از TextField استفاده کن یا حتماً [:2000] بزن.

۲. حذف نکردن داده‌های قدیمی. بدون پاک‌سازی خودکار، جدول‌ها بی‌نهایت رشد می‌کنند.

۳. استفاده از DateTimeField(auto_now_add=True) در جایی که زمان دقیق مهم است. گاهی لازم است زمان را خودت تنظیم کنی. از default=timezone.now استفاده کن.

۴. رندر مستقیم داده‌های آماری در قالب‌های عمومی. اگر آمار را در فوتر همه‌ی صفحات نمایش می‌دهی، هر بازدیدکننده یک کوئری اضافه تولید می‌کند. اگر می‌خواهی این کار را انجام دهی، نتیجه را کش کن.

۵. نبود تمایز بین داده‌ی خام و داده‌ی تجمیع‌شده. کوئری‌های سنگین روی داده‌ی خام را می‌توانی با یک جدول تجمیعی روزانه جایگزین کنی. یک task شبانه که آمار دیروز را در جدول DailyStats بنویسد، کوئری‌های صفحه‌ی داشبورد را چند صد برابر سریع‌تر می‌کند.

۶. نداشتن صفحه‌ی referral. تحلیل اینکه کاربر از کدام سایت یا شبکه اجتماعی آمده، یکی از مفیدترین خروجی‌های اپ ردیابی است. این گزارش را جدا نگه دار و از موتورهای جستجو جدا کن. الگوی کاملش در ساختار referral قابل پیاده‌سازی است. اگر می‌خواهی بدانی چگونه Referrer را از دامنه تشخیص بدهی، طبقه‌بندی Referrer و تشخیص ورود از گوگل مرجع دقیقی است.

پرسش‌های پرتکرار درباره‌ی ساخت اپ ردیابی

آیا یک اپ ردیابی می‌تواند روی همان دیتابیس اصلی سایت اجرا شود؟ بله، در ابتدای کار و برای سایت‌های کوچک و متوسط این کار کاملاً منطقی است. ولی به‌محض اینکه حجم جدول Interaction زیاد شد یا ترافیک بالا رفت، بهتر است به یک دیتابیس جداگانه منتقل شود. این جداسازی، فشار آماری را از دیتابیس اصلی جدا می‌کند.

آیا می‌توان از همان مدل‌های User استفاده کرد؟ نه. Visitor و User دو مفهوم متفاوت هستند. یک User ممکن است چند Visitor داشته باشد (چند دستگاه) و یک Visitor ممکن است هرگز User نشود (کاربر مهمان). این تفکیک برای تحلیل دقیق ضروری است.

چطور از ذخیره‌ی مسیرهای admin و پنل جلوگیری کنیم؟ با ANALYTICS_SKIP_PATHS. این لیست را در settings.py تعریف کن و در middleware چک کن. برای پنل خودت هم حتماً /panel/ را اضافه کن.

آیا می‌توان همان اپ را برای چند سایت استفاده کرد؟ بله، ولی به یک فیلد site یا tenant نیاز داری. در این حالت، مدل‌های Visitor, Visit, PageView باید این فیلد را داشته باشند. اگر از django.contrib.sites استفاده می‌کنی، کار ساده‌تر است. برای multi-tenant واقعی، به django-tenants نگاه کن.

چند مدل برای این اپ کافی است؟ چهار مدل پیشنهادی (Visitor, Visit, PageView, Interaction) نقطه‌ی تعادل خوبی است. می‌توانی مدل‌های بیشتری اضافه کنی مثل Conversion یا Event، ولی توصیه می‌کنم از ابتدا ساده شروع کنی و بر اساس نیاز واقعی گسترش بدهی.

آیا باید داده‌ها را در S3 یا ClickHouse بریزیم؟ اگر سایت زیر ۱ میلیون بازدید ماهانه دارد، دیتابیس رابطه‌ای کافی است. برای حجم بالاتر، ClickHouse یا TimescaleDB انتخاب‌های بهتری هستند، ولی پیچیدگی عملیاتی‌شان بیشتر است. نقطه‌ی شروع خوب، PostgreSQL با پارتیشن‌بندی است.

چطور داده‌های قدیمی را آرشیو کنیم؟ قبل از حذف، داده را به یک فایل پارکت یا به S3 منتقل کن. یک کامند سفارشی بنویس که داده‌های قدیمی‌تر از ۹۰ روز را در S3 ذخیره کند و سپس از دیتابیس حذف کند. این استراتژی، هم هزینه‌ی ذخیره‌سازی را پایین می‌آورد، هم داده‌های تاریخی را حفظ می‌کند.

آیا می‌توان به‌جای دیتابیس، داده‌ها را در فایل لاگ نوشت؟ بله، ولی برای تحلیل، دیتابیس رابطه‌ای راحت‌تر است. اگر روی معماری لاگ‌محور اصرار داری، از ELK (Elasticsearch, Logstash, Kibana) یا Loki استفاده کن. برای اکثر پروژه‌ها، دیتابیس رابطه‌ای با یک جدول تجمیعی شبانه کافی است.

چطور از ذخیره‌ی مسیرهای تکراری جلوگیری کنیم؟ در PageView می‌توانی یک path_hash بسازی و اگر مسیر در ۳۰ ثانیه‌ی گذشته برای همان Visit تکرار شد، دوباره ثبت نکن. این کار در سایت‌هایی که کاربر سریع روی چند لینک کلیک می‌کند مفید است.

آیا باید URLهای beacon را در robots.txt مسدود کنیم؟ بله، این یک اقدام احتیاطی است. ربات‌ها نباید تلاش کنند که /panel/stat/api/track/ را ایندکس کنند، چون بی‌معناست. همچنین در robots.txt، مسیر /panel/ را به‌طور کامل مسدود کن.

نکته‌ای که در مستندات جنگو پیدا نمی‌کنی

در طول سال‌هایی که روی پروژه‌های آماری کار کردم، یک درس تکرارشونده داشته‌ام: اپ ردیابی، ابزاری برای تصمیم‌گیری است، نه ابزاری برای ذخیره‌ی همه‌چیز. تیم‌هایی که همه‌ی داده‌ها را بدون هدف ذخیره می‌کنند، در نهایت با هزینه‌ی نگهداری و کندی روبرو می‌شوند. تیم‌هایی که با هدف مشخص شروع می‌کنند — مثلاً «می‌خواهیم بدانیم کاربران از کدام صفحه خارج می‌شوند» — سریع‌تر به بینش می‌رسند.

پس توصیه‌ی عملی این است: قبل از نوشتن کد، سه سؤال بپرس. اول، قرار است از این داده چه تصمیمی بگیریم؟ دوم، چه داده‌ای برای آن تصمیم کافی است؟ سوم، چه داده‌ای می‌توانیم ذخیره نکنیم؟ پاسخ به این سه سؤال، معماری اپ را ساده‌تر می‌کند و از بدهی‌های آینده جلوگیری می‌کند.

درباره‌ی پیاده‌سازی، آخرین نکته: همیشه یک مسیر بازگشت داشته باش. اپ ردیابی نباید هرگز باعث شود سایت از کار بیفتد. اگر دیتابیس آماری down است، سایت باید با یک try/except بدون ردیابی ادامه بدهد. اگر session نوشتن خطا داد، نباید صفحه‌ی خطا نشان داده شود. این اصل احتیاطی، در تمام پروژه‌های production ضروری است. اگر خواستی روی نمایش آمار زنده هم کار کنی، ساخت API JSON برای نمایش آمار زنده نقطه‌ی شروع خوبی است.

اگر این ساختار را در پروژه‌ی خودت پیاده کردی و به نکته‌ای رسیدی که در این مقاله نبود — مثلاً یک باگ خاص در تعامل با session یا یک رفتار غیرمنتظره در مقیاس بزرگ — برایم جالب است بدانی. تجربه‌ی عملی تو، بیشتر از هر مستنداتی می‌تواند به خواننده‌ی بعدی کمک کند. آن را در دیدگاه‌ها بنویس.