اگر تصور می‌کنید با جستجوی کلمه‌ی Mobile در User-Agent می‌توانید موبایل را از دسکتاپ تشخیص دهید، احتمالاً تا امروز درصد قابل توجهی از کاربران iPad Pro را اشتباهاً دسکتاپ ثبت کرده‌اید و گزارش‌های موبایل و دسکتاپ سایت‌تان هرگز دقیق نبوده است.

چرا تشخیص دستگاه، فقط یک گزارش آماری نیست؟

در یکی از پروژه‌های فروشگاهی که چند سال پیش روی آن کار می‌کردم، تیم محصول تصمیم گرفت طراحی صفحه‌ی تسویه را ساده‌تر کند، چون آمار نشان می‌داد «۷۸ درصد کاربران با دسکتاپ خرید می‌کنند». سه ماه بعد از تغییر، فروش موبایل ۳۰ درصد افت کرد. بررسی دقیق‌تر نشان داد سیستم تشخیص دستگاه، iPad Pro را به‌عنوان دسکتاپ شناسایی می‌کرد و همه‌ی خریدهای تبلت در سمت دسکتاپ ثبت می‌شد. تیم محصول بر اساس داده‌ی اشتباه تصمیم گرفته بود و هزینه‌اش را با فروش از دست رفته پرداخت.

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

دلیل اول، تجربه‌ی کاربر. اگر بدانید کاربر از موبایل وارد شده، می‌توانید نسخه‌ی سبک‌تری از صفحه را بارگذاری کنید، تصاویر را با ابعاد مناسب سرو کنید و منابع غیرضروری را حذف کنید. این تصمیم‌ها مستقیماً روی سرعت بارگذاری و تجربه‌ی کاربر اثر می‌گذارند.

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

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

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

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

تکامل تشخیص دستگاه؛ از regex ساده تا Client Hints

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

موج اول، User-Agentهای ساده. در دهه‌ی ۲۰۰۰، مرورگرهای موبایل اولیه مثل Opera Mini خودشان را با کلمات مشخص مثل Mobile معرفی می‌کردند. یک regex ساده کافی بود.

موج دوم، iPhone و iPad. اپل در ۲۰۰۷ iPhone را معرفی کرد و User-Agent آن شامل iPhone بود. در ۲۰۱۰، iPad معرفی شد و User-Agent آن شامل iPad بود. تشخیص هنوز ساده بود.

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

موج چهارم، iPad Pro و حالت دسکتاپ. در ۲۰۱۶، اپل iPad Pro را معرفی کرد و ادعا کرد این دستگاه می‌تواند جایگزین لپ‌تاپ باشد. User-Agent آن، دیگر شامل iPad نبود، بلکه خودش را Macintosh معرفی می‌کرد. این تغییر، تمام کتابخانه‌های تشخیص دستگاه را شکست.

موج پنجم، Client Hints. گوگل در ۲۰۲۰، یک API جدید به نام User-Agent Client Hints معرفی کرد که به سایت‌ها اجازه می‌دهد اطلاعات دستگاه را به‌صورت ساختاریافته دریافت کنند. این API، جایگزین تدریجی User-Agent است، ولی هنوز پشتیبانی کاملی در همه‌ی مرورگرها ندارد. مفهوم Client Hints در ویکی‌پدیا توضیح داده شده است.

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

لایه‌ی اول؛ regex روی User-Agent

ارزان‌ترین و سریع‌ترین لایه، regex روی User-Agent است. این لایه، بخش بزرگی از دستگاه‌ها را در کمتر از یک میلی‌ثانیه تشخیص می‌دهد.

# analytics/utils/device.py

import re


MOBILE_PATTERN = re.compile(
    r"Mobile|Android.*Mobile|iPhone|iPod|BlackBerry|"
    r"IEMobile|Opera Mini|Windows Phone|webOS",
    re.IGNORECASE,
)

TABLET_PATTERN = re.compile(
    r"iPad|Tablet|PlayBook|Silk|"
    r"(Android(?!.*Mobile))|Kindle|Nexus 7|Nexus 10|"
    r"SM-T|GT-P|SCH-I800",
    re.IGNORECASE,
)

BOT_PATTERN = re.compile(
    r"bot|crawl|spider|slurp",
    re.IGNORECASE,
)


def detect_device_type(user_agent: str) -> str:
    if not user_agent:
        return "unknown"

    ua = user_agent.strip()

    if BOT_PATTERN.search(ua):
        return "bot"

    if TABLET_PATTERN.search(ua):
        return "tablet"

    if MOBILE_PATTERN.search(ua):
        return "mobile"

    # iPad Pro که خودش را Macintosh معرفی می‌کند
    if _is_ipad_pro(ua):
        return "tablet"

    return "desktop"


def _is_ipad_pro(ua: str) -> bool:
    """
    iPad Pro (2016+) خودش را Macintosh معرفی می‌کند، ولی
    در هدرهای بعدی نشانه‌هایی از iPad دارد.
    """
    if "Macintosh" not in ua:
        return False
    # الگوی تشخیص در سمت کلاینت یا با هدرهای تکمیلی
    return False  # در لایه‌ی دوم تشخیص داده می‌شود

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

بخش اول، موبایل‌های iOS. iPhone و iPod خودشان را با نام مشخص معرفی می‌کنند. ولی iPad نه، به‌خاطر تغییرات اپل در ۲۰۱۶.

بخش دوم، موبایل‌های اندرویدی. اکثر موبایل‌های اندروید شامل کلمه‌ی Mobile در User-Agent هستند. ولی تبلت‌های اندرویدی این کلمه را ندارند، به‌همین‌دلیل با یک negative lookahead تشخیص داده می‌شوند: (Android(?!.*Mobile)).

بخش سوم، دستگاه‌های خاص. PlayBook، Silk، Kindle و چند دستگاه دیگر با نام مشخص.

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

لایه‌ی دوم؛ تشخیص سمت کلاینت با JavaScript

regex روی User-Agent، به‌تنهایی نمی‌تواند همه‌ی دستگاه‌ها را تشخیص دهد. مخصوصاً iPad Pro که خودش را Macintosh معرفی می‌کند. برای این حالت‌ها، به تشخیص سمت کلاینت نیاز دارید.

مرورگرها در سمت کلاینت، چندین سیگنال مفید دارند که در سمت سرور در دسترس نیستند.

سیگنال اول، navigator.maxTouchPoints. این مقدار، تعداد نقاط لمسی که دستگاه پشتیبانی می‌کند را برمی‌گرداند. اگر بیش از صفر باشد، دستگاه لمسی است. iPad Pro در حالت دسکتاپ، همچنان maxTouchPoints > 0 دارد، پس تشخیص داده می‌شود.

// analytics/static/analytics/device-detect.js

function detectDeviceType() {
  const ua = navigator.userAgent || "";
  const touchPoints = navigator.maxTouchPoints || 0;
  const isTouchDevice = touchPoints > 0;

  // تشخیص iPad Pro
  if (/Macintosh/.test(ua) && isTouchDevice) {
    return "tablet";
  }

  // تشخیص iPad (نسخه‌های قدیمی)
  if (/iPad/.test(ua)) {
    return "tablet";
  }

  // تشخیص iPadOS 13+ که خودش را Mac می‌معرفی می‌کند
  if (navigator.platform === "MacIntel" && isTouchDevice) {
    return "tablet";
  }

  // تشخیص موبایل
  if (/Mobile|iPhone|Android.*Mobile|iPod/.test(ua)) {
    return "mobile";
  }

  // تشخیص تبلت اندروید
  if (/Android/.test(ua) && !/Mobile/.test(ua)) {
    return "tablet";
  }

  // اگر صفحه کوچک است، احتمالاً موبایل
  if (window.innerWidth < 768 && isTouchDevice) {
    return "mobile";
  }

  if (window.innerWidth < 1024 && isTouchDevice) {
    return "tablet";
  }

  return "desktop";
}

این کد، در سه نقطه از regex سمت سرور دقیق‌تر عمل می‌کند.

نقطه‌ی اول، تشخیص iPad Pro. با ترکیب Macintosh و maxTouchPoints، این دستگاه به‌درستی تبلت شناسایی می‌شود.

نقطه‌ی دوم، تشخیص iPadOS 13+. این نسخه از iPadOS، خودش را MacIntel معرفی می‌کند، ولی با maxTouchPoints قابل تشخیص است.

نقطه‌ی سوم، fallback بر اساس عرض صفحه. اگر User-Agent فریب‌دهنده بود، می‌توان از عرض viewport و لمسی بودن استفاده کرد.

این کد، در قالب یک فایل JavaScript در سایت قرار می‌گیرد و نتیجه را به endpoint beacon ارسال می‌کند. اگر با الگوی ذخیره‌ی PageView و Interaction کار کرده باشید، می‌دانید که این ساختار با یک API endpoint واحد پیاده می‌شود.

تشخیص سمت کلاینت، دقت بالاتری دارد، ولی همیشه در دسترس نیست. اگر کاربر JavaScript را غیرفعال کرده باشد، فقط تشخیص سمت سرور در اختیار شماست. این دو لایه، مکمل هم هستند، نه جایگزین.

لایه‌ی سوم؛ ترکیب سیگنال‌ها و fingerprinting

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

سیگنال اول، Client Hints. اگر مرورگر از Client Hints پشتیبانی می‌کند، می‌توانید مستقیماً نوع دستگاه را از هدر Sec-CH-UA-Mobile استخراج کنید. مقدار این هدر، ?0 یا ?1 است که به‌ترتیب یعنی دسکتاپ یا موبایل.

def detect_from_client_hints(request) -> str | None:
    mobile = request.META.get("HTTP_SEC_CH_UA_MOBILE", "")
    platform = request.META.get("HTTP_SEC_CH_UA_PLATFORM", "")

    if mobile == "?1":
        return "mobile"
    if mobile == "?0":
        # می‌تواند دسکتاپ یا تبلت باشد
        if "iPad" in platform or "Android" in platform:
            return "tablet"
        return "desktop"

    return None

سیگنال دوم، هدر User-Agent. همان regex لایه‌ی اول.

سیگنال سوم، سیگنال سمت کلاینت. نتیجه‌ی beacon که از JavaScript می‌آید.

سیگنال چهارم، امتیاز اطمینان. برای هر سیگنال، یک امتیاز اطمینان محاسبه می‌کنیم. سپس با ترکیب سیگنال‌ها، یک تصمیم نهایی می‌گیریم.

def detect_device_full(request, client_signal: str = None) -> dict:
    signals = {}

    # سیگنال Client Hints
    ch = detect_from_client_hints(request)
    if ch:
        signals["client_hints"] = {"value": ch, "confidence": 95}

    # سیگنال User-Agent
    ua = request.META.get("HTTP_USER_AGENT", "")
    ua_type = detect_device_type(ua)
    signals["user_agent"] = {"value": ua_type, "confidence": 70}

    # سیگنال کلاینت
    if client_signal:
        signals["client"] = {"value": client_signal, "confidence": 90}

    # تصمیم نهایی بر اساس بالاترین اطمینان
    best = max(signals.values(), key=lambda s: s["confidence"])

    return {
        "device_type": best["value"],
        "confidence": best["confidence"],
        "signals": signals,
    }

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

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

حالت‌های مرزی؛ iPad Pro، تبلت‌های اندرویدی و دستگاه‌های تاشو

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

حالت اول، iPad Pro. از ۲۰۱۶ به بعد، iPad Pro خودش را Macintosh معرفی می‌کند. تنها راه تشخیص، استفاده از maxTouchPoints در سمت کلاینت است. اگر این نکته را نادیده بگیرید، تمام کاربران iPad Pro به‌عنوان دسکتاپ ثبت می‌شوند.

حالت دوم، تبلت‌های اندرویدی. بعضی تبلت‌های اندرویدی خودشان را موبایل معرفی می‌کنند، چون سازنده این‌طور تنظیم کرده. مثلاً برخی تبلت‌های سامسونگ در حالت portrait، خودشان را موبایل معرفی می‌کنند. راه‌حل: ترکیب User-Agent با عرض viewport.

حالت سوم، دستگاه‌های تاشو. دستگاه‌هایی مثل Samsung Galaxy Fold و Z Flip، در حالت بسته خودشان را موبایل و در حالت باز خودشان را تبلت معرفی می‌کنند. این دستگاه‌ها، یک چالش جدید برای تشخیص دستگاه هستند. راه‌حل: ذخیره‌ی نوع دستگاه در لحظه‌ی بازدید، نه در سطح Visitor.

حالت چهارم، Amazon Silk و Kindle. دستگاه‌های Kindle Fire خودشان را با Silk معرفی می‌کنند، ولی بعضی مدل‌ها این کلمه را ندارند. راه‌حل: ترکیب الگوهای Silk، Kindle و KFAPWI.

دستگاهUser-Agentتشخیص درستدام
iPhone 15iPhonemobileهیچ
iPad Pro 2022Macintoshtabletاشتباهاً desktop
Galaxy Tab S9Android + Mobiletabletاشتباهاً mobile
Galaxy Z Fold 5Android + Mobilemobile یا tabletوابسته به حالت
Kindle FireSilktabletاگر Silk نباشد، unknown
Surface ProWindows + Touchdesktop یا tabletدوگانه

این جدول، نشان می‌دهد که تشخیص دستگاه، ذاتاً یک مسئله‌ی چندلایه است. هیچ الگوریتم واحدی نمی‌تواند همه‌ی این حالت‌ها را پوشش دهد. راه‌حل عملی، ترکیب چند سیگنال و به‌روزرسانی دوره‌ای آن‌ها است. اگر می‌خواهید جزئیات بیشتری درباره‌ی حالت‌های مرزی ببینید، استخراج نوع و نسخه مرورگر از User-Agent راهنمای دقیقی است.

تشخیص تعامل لمسی در برابر نشانگر موس

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

سه سیگنال مفید در این زمینه:

سیگنال اول، navigator.maxTouchPoints. اگر بیش از صفر باشد، دستگاه لمسی است.

سیگنال دوم، window.matchMedia("(pointer: fine)"). اگر این مقدار true باشد، دستگاه یک نشانگر دقیق مثل موس دارد.

سیگنال سوم، window.matchMedia("(hover: hover)"). اگر true باشد، دستگاه از hover پشتیبانی می‌کند (مثل دسکتاپ).

function detectInputType() {
  const hasTouch = navigator.maxTouchPoints > 0;
  const hasFinePointer = window.matchMedia("(pointer: fine)").matches;
  const hasHover = window.matchMedia("(hover: hover)").matches;

  if (hasTouch && hasFinePointer && hasHover) {
    return "hybrid";  // مثل Surface Pro
  }
  if (hasTouch && !hasHover) {
    return "touch_only";  // مثل موبایل و تبلت
  }
  if (!hasTouch && hasFinePointer) {
    return "mouse";  // مثل دسکتاپ
  }
  return "unknown";
}

این سیگنال، برای طراحی UI بسیار مفید است. مثلاً اگر کاربر از موس استفاده می‌کند، می‌توانید از hover effect استفاده کنید. اگر از لمس استفاده می‌کند، دکمه‌ها باید بزرگ‌تر باشند. اگر از هر دو (hybrid)، باید هر دو حالت را پشتیبانی کنید.

در پروژه‌های واقعی، هیچ‌وقت فقط به نوع دستگاه اکتفا نکنید. همیشه نوع تعامل را هم در نظر بگیرید. یک لپ‌تاپ با صفحه‌ی لمسی، هم دسکتاپ است، هم لمسی.

نوع اتصال، viewport و سیگنال‌های مکمل

علاوه بر نوع دستگاه، چند سیگنال مکمل وجود دارد که تصویر را کامل‌تر می‌کند.

سیگنال اول، نوع اتصال. API navigator.connection در مرورگرهای مبتنی بر Chromium، نوع اتصال (4g، 3g، 2g، slow-2g) و effectiveType را برمی‌گرداند. این داده، به شما اجازه می‌دهد تجربه‌ی سایت را بر اساس سرعت شبکه تنظیم کنید.

function getConnectionType() {
  const conn = navigator.connection
    || navigator.mozConnection
    || navigator.webkitConnection;

  if (!conn) return null;

  return {
    effectiveType: conn.effectiveType || null,
    downlink: conn.downlink || null,
    rtt: conn.rtt || null,
    saveData: conn.saveData || false,
  };
}

سیگنال دوم، ابعاد viewport. window.innerWidth و window.innerHeight ابعاد ناحیه‌ی قابل مشاهده را برمی‌گردانند. این داده، برای طراحی responsive و برای تشخیص نوع دستگاه مفید است.

سیگنال سوم، device pixel ratio. window.devicePixelRatio نسبت پیکسل‌های فیزیکی به پیکسل‌های CSS را برمی‌گرداند. روی نمایشگرهای Retina، این مقدار ۲ یا ۳ است.

سیگنال چهارم، جهت صفحه. window.screen.orientation یا window.orientation (در مرورگرهای قدیمی‌تر) جهت دستگاه را برمی‌گرداند: portrait یا landscape.

این سیگنال‌ها، همه در لایه‌ی کلاینت جمع‌آوری می‌شوند و از طریق beacon به سرور ارسال می‌شوند. اگر می‌خواهید بدانید چطور این beacon را پیاده کنید، ذخیره‌ی PageView و Interaction الگوی کاملی ارائه می‌دهد.

معماری نهایی؛ سرویس، middleware و API beacon

حالا بیایید همه‌ی لایه‌ها را در یک معماری نهایی ترکیب کنیم. این معماری، از سه بخش اصلی تشکیل شده است.

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

# analytics/services/device.py

from analytics.utils.device import (
    detect_device_type,
    detect_from_client_hints,
)


def resolve_device(request, client_signal: str = None) -> dict:
    """
    تصمیم نهایی درباره نوع دستگاه.
    """
    signals = []

    ch_type = detect_from_client_hints(request)
    if ch_type:
        signals.append({
            "source": "client_hints",
            "value": ch_type,
            "confidence": 95,
        })

    ua = request.META.get("HTTP_USER_AGENT", "")
    ua_type = detect_device_type(ua)
    if ua_type != "unknown":
        signals.append({
            "source": "user_agent",
            "value": ua_type,
            "confidence": 70,
        })

    if client_signal:
        signals.append({
            "source": "client",
            "value": client_signal,
            "confidence": 90,
        })

    if not signals:
        return {
            "device_type": "unknown",
            "confidence": 0,
            "signals": [],
        }

    best = max(signals, key=lambda s: s["confidence"])

    return {
        "device_type": best["value"],
        "confidence": best["confidence"],
        "signals": signals,
    }

بخش دوم، middleware. این لایه، سرویس را در لحظه‌ی درخواست صدا می‌زند و نتیجه را در Visitor ذخیره می‌کند. اگر با الگوی تعریف و کاربرد services.py در جنگو کار کرده باشید، این جداسازی برایتان آشناست.

# analytics/middleware.py

class VisitorTrackingMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        from analytics.services.device import resolve_device

        if request.method == "GET" and not self._should_skip(request):
            device_info = resolve_device(request)
            request._device_info = device_info

        response = self.get_response(request)
        return response

بخش سوم، API beacon. این endpoint، سیگنال سمت کلاینت را دریافت می‌کند و نتیجه را در دیتابیس به‌روز می‌کند.

# analytics/api.py

@require_POST
@csrf_exempt
def track_device(request):
    payload = json.loads(request.body or b"{}")
    client_signal = payload.get("device_type")

    visit_id = request.session.get("_analytics_visit_id")
    if not visit_id:
        return JsonResponse({"ok": False})

    visit = Visit.objects.filter(pk=visit_id).first()
    if not visit:
        return JsonResponse({"ok": False})

    from analytics.services.device import resolve_device

    device_info = resolve_device(request, client_signal)

    # اگر سیگنال کلاینت اطمینان بالاتری دارد، آن را اعمال کن
    if device_info["confidence"] >= 90:
        Visit.objects.filter(pk=visit.pk).update(
            device_type_override=device_info["device_type"],
        )
        Visitor.objects.filter(pk=visit.visitor_id).update(
            device_type=device_info["device_type"],
        )

    return JsonResponse({"ok": True})

این معماری، سه مزیت کلیدی دارد. اول، منطق تشخیص در یک سرویس متمرکز است. دوم، middleware فقط مسئول هماهنگی است. سوم، سیگنال سمت کلاینت می‌تواند تشخیص سرور را اصلاح کند.

طراحی داده برای ذخیره‌ی نوع دستگاه

ذخیره‌ی نوع دستگاه، به‌اندازه‌ی خود تشخیص اهمیت دارد. سه فیلد کلیدی را در نظر بگیرید.

فیلد اول، device_type. مقدار نهایی: desktop، mobile، tablet، bot، unknown. این فیلد در مدل Visitor و در مدل Visit ذخیره می‌شود.

فیلد دوم، device_confidence. سطح اطمینان تصمیم، بین ۰ تا ۱۰۰. این فیلد به شما اجازه می‌دهد تصمیم‌های مرزی را بازبینی کنید.

فیلد سوم، device_signals. یک فیلد JSON که همه‌ی سیگنال‌های خام را ذخیره می‌کند. این فیلد برای دیباگ و بازبینی دوره‌ای حیاتی است.

class Visit(models.Model):
    # ... فیلدهای دیگر
    device_type_override = models.CharField(
        max_length=20, blank=True,
    )
    device_confidence = models.PositiveSmallIntegerField(default=0)
    device_signals = models.JSONField(default=dict, blank=True)

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

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

کالیبراسیون؛ جایی که پروژه‌ها شکست می‌خورند

تشخیص دستگاه، فقط با نوشتن کد به‌دست نمی‌آید. باید مدل خود را با داده‌ی واقعی سایت کالیبره کنید. سه مرحله را در نظر بگیرید.

مرحله‌ی اول، جمع‌آوری داده‌ی برچسب‌دار. برای یک هفته، همه‌ی سیگنال‌ها را با هم ذخیره کنید. سپس به‌طور دستی، ۵۰۰ تا ۱۰۰۰ نمونه را برچسب بزنید: واقعاً موبایل بوده یا دسکتاپ یا تبلت.

مرحله‌ی دوم، مقایسه‌ی مدل با برچسب‌ها. دقت مدل خود را با برچسب‌ها مقایسه کنید. اگر دقت زیر ۹۰٪ است، جایی از مدل اشتباه است.

مرحله‌ی سوم، تنظیم آستانه‌ها. اگر آستانه‌ی اطمینان برای تصمیم‌گیری را روی ۹۰ گذاشته‌اید، ولی مدل شما در واقعیت ۸۵٪ دقت دارد، آستانه را تنظیم کنید.

در یکی از پروژه‌ها، بعد از کالیبراسیون متوجه شدیم که تعداد قابل توجهی از کاربران، دستگاه‌های Hybrid دارند (لپ‌تاپ با صفحه‌ی لمسی). مدل اولیه، این‌ها را به‌عنوان دسکتاپ یا تبلت دسته‌بندی می‌کرد، ولی با کالیبراسیون، یک دسته‌ی جدید hybrid اضافه کردیم.

هر مدل تشخیص، بدون کالیبراسیون دوره‌ای، در طول زمان منحرف می‌شود. تجهیزات کاربران تغییر می‌کند، مرورگرها به‌روز می‌شوند و User-Agentها هم تغییر می‌کنند.

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

تست‌نویسی سناریوهای مختلف دستگاه

تشخیص دستگاه، به‌خاطر تنوع حالت‌ها، به تست‌های گسترده‌ای نیاز دارد. سه سطح تست را در نظر بگیرید.

سطح اول، تست واحد regex. برای هر الگو، یک تست جدا بنویسید:

import pytest
from analytics.utils.device import detect_device_type


@pytest.mark.parametrize("ua,expected", [
    ("Mozilla/5.0 (iPhone; CPU iPhone OS 17_0)", "mobile"),
    ("Mozilla/5.0 (iPad; CPU OS 17_0)", "tablet"),
    ("Mozilla/5.0 (Linux; Android 13; SM-S918B) AppleWebKit", "mobile"),
    ("Mozilla/5.0 (Linux; Android 13; SM-X700) AppleWebKit", "tablet"),
    ("Mozilla/5.0 (Windows NT 10.0; Win64; x64)", "desktop"),
    ("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)", "desktop"),
    ("Mozilla/5.0 (compatible; Googlebot/2.1)", "bot"),
    ("", "unknown"),
])
def test_detect_device_type(ua, expected):
    assert detect_device_type(ua) == expected

سطح دوم، تست سرویس. با RequestFactory، یک درخواست ساختگی بسازید و سرویس را تست کنید:

from django.test import RequestFactory
from analytics.services.device import resolve_device


def test_resolve_device_with_client_hints():
    request = RequestFactory().get("/")
    request.META["HTTP_SEC_CH_UA_MOBILE"] = "?1"

    result = resolve_device(request)
    assert result["device_type"] == "mobile"
    assert result["confidence"] >= 90

سطح سوم، تست یکپارچه. با django.test.Client، یک درخواست کامل بفرستید و بررسی کنید که Visitor با نوع دستگاه درست ساخته شده است:

@pytest.mark.django_db
def test_tracking_creates_mobile_visitor(client):
    from analytics.models import Visitor

    client.get(
        "/",
        HTTP_USER_AGENT="Mozilla/5.0 (iPhone; CPU iPhone OS 17_0)",
    )

    visitor = Visitor.objects.first()
    assert visitor.device_type == "mobile"

این سه سطح تست، به شما اجازه می‌دهند که در طول زمان، تغییرات را با اطمینان اعمال کنید.

حریم خصوصی و ملاحظات قانونی

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

سه اصل را در نظر بگیرید.

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

اصل دوم، عدم اشتراک‌گذاری با اشخاص ثالث. داده‌های تشخیص دستگاه، حتی اگر به‌نظر بی‌ضرر باشند، نباید با سرویس‌های تبلیغاتی یا سایر اشخاص ثالث به اشتراک گذاشته شوند.

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

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

anti-patternهای رایج در تشخیص دستگاه

در بازبینی پروژه‌های مختلف، این اشتباهات را زیاد دیده‌ام:

۱. تصمیم‌گیری فقط بر اساس یک سیگنال. اگر فقط از User-Agent استفاده کنید، iPad Pro را اشتباهاً دسکتاپ ثبت می‌کنید.

۲. عدم ذخیره‌ی سیگنال‌های خام. اگر فقط نتیجه‌ی نهایی را ذخیره کنید، در آینده نمی‌توانید مدل را بهبود دهید.

۳. عدم کالیبراسیون دوره‌ای. مدل را یک‌بار می‌سازید و بعد فراموش می‌کنید. بعد از یک سال، دقت مدل به‌شدت افتاده است.

۴. استفاده از کتابخانه‌های عمومی بدون تست. کتابخانه‌های تشخیص User-Agent، همه‌ی حالت‌های مرزی را پوشش نمی‌دهند. باید آن‌ها را با داده‌ی سایت خودتان تست کنید.

۵. ذخیره‌ی device_type در سطح Visitor به‌جای Visit. یک کاربر می‌تواند در طول زمان از دستگاه‌های مختلف استفاده کند. اگر نوع دستگاه را فقط در Visitor ذخیره کنید، این تغییرات را از دست می‌دهید.

۶. عدم مدیریت دستگاه‌های ناشناخته. دسته‌ی unknown باید حتماً وجود داشته باشد، ولی اگر درصد آن بالاست، یعنی مدل شما ناقص است.

۷. تشخیص دستگاه در ویو به‌جای middleware. اگر در ویو تشخیص دهید، مجبورید آن را در هر ویو تکرار کنید. اگر با الگوی نوشتن Middleware سفارشی در جنگو کار کرده باشید، می‌دانید که این کار اشتباه است.

۸. عدم هماهنگی با CDN. اگر سایت پشت CDN است، بعضی هدرها ممکن است تغییر کنند. این نکته را در تشخیص خود در نظر بگیرید.

۹. نادیده‌گرفتن دستگاه‌های تاشو. این دستگاه‌ها در چند سال آینده رایج‌تر خواهند شد و باید از ابتدا برای آن‌ها آماده باشید.

۱۰. عدم تفکیک دستگاه‌های Hybrid. بعضی دستگاه‌ها هم لمسی هستند و هم دارای موس. اگر این‌ها را به‌عنوان دسکتاپ دسته‌بندی کنید، سیگنال مهمی را از دست می‌دهید.

پرسش‌های پرتکرار درباره‌ی تشخیص دستگاه در جنگو

آیا می‌توانم از کتابخانه‌ی user-agents پایتون استفاده کنم؟ بله، این کتابخانه بسیار مفید است، ولی توصیه می‌کنم آن را با لایه‌ی کلاینت ترکیب کنید، چون این کتابخانه هم iPad Pro را به‌درستی تشخیص نمی‌دهد.

آیا Client Hints جایگزین User-Agent می‌شود؟ به‌تدریج، بله. گوگل در حال کاهش اطلاعات User-Agent است، ولی فرآیند آن چند سال طول می‌کشد. تا آن زمان، باید هر دو را پشتیبانی کنید.

آیا باید نوع دستگاه را در Visitor یا Visit ذخیره کنم؟ در هر دو. Visitor برای تشخیص کلی، Visit برای تغییرات در طول زمان. اگر کاربر از دستگاه‌های مختلف بازدید می‌کند، در Visit ثبت می‌شود.

چطور iPad Pro را در سمت سرور تشخیص دهم؟ در سمت سرور، به‌طور مستقیم قابل تشخیص نیست. باید از سیگنال سمت کلاینت (maxTouchPoints) استفاده کنید که از طریق beacon به سرور می‌رسد.

آیا باید دستگاه‌های Wearable (ساعت‌های هوشمند) را تشخیص دهم؟ اگر سایت شما روی این دستگاه‌ها بازدید می‌شود، بله. اکثر ساعت‌های هوشمند از سیستمعامل‌های مشابه موبایل استفاده می‌کنند و به‌عنوان موبایل تشخیص داده می‌شوند.

چطور دستگاه‌های قدیمی را تشخیص دهم؟ User-Agent آن‌ها معمولاً شامل نام مشخص است. می‌توانید یک لیست از دستگاه‌های قدیمی با سهم بازار قابل توجه را در regex اضافه کنید. ولی توصیه می‌کنم ابتدا بر اساس داده‌ی واقعی سایت خودتان تصمیم بگیرید.

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

آیا تشخیص دستگاه روی سئو اثر دارد؟ به‌طور مستقیم نه، ولی اگر نسخه‌ی موبایل سایت شما با نسخه‌ی دسکتاپ متفاوت باشد، گوگل ترجیح می‌دهد نسخه‌ی موبایل را به‌عنوان نسخه‌ی اصلی در نظر بگیرد. این مفهوم به mobile-first indexing معروف است.

چطور مطمئن شوم که مدل من درست کار می‌کند؟ سه راه: اول، تست واحد regex. دوم، تست سرویس با درخواست‌های ساختگی. سوم، کالیبراسیون دوره‌ای با داده‌ی برچسب‌دار.

آیا می‌توانم دستگاه را بر اساس ابعاد viewport تشخیص دهم؟ به‌تنهایی نه، ولی به‌عنوان یک سیگنال مکمل مفید است. مثلاً viewport زیر ۷۶۸ پیکسل اغلب موبایل است، بین ۷۶۸ و ۱۰۲۴ تبلت، و بالاتر از ۱۰۲۴ دسکتاپ. ولی این قاعده در دستگاه‌های تاشو و iPad Pro اشتباه می‌شود.

چطور از نوع دستگاه برای بهینه‌سازی استفاده کنم؟ سه راه: اول، سرو کردن تصاویر با ابعاد مناسب. دوم، حذف منابع غیرضروری در نسخه‌ی موبایل. سوم، تنظیم چیدمان صفحه بر اساس عرض viewport.

آیا باید دستگاه را در Content-Type پاسخ درج کنم؟ نه، این اطلاعات در هدر Vary پاسخ درج می‌شود اگر روی دستگاه تغییر محتوا داشته باشید. این نکته برای کش CDN اهمیت دارد.

چطور دستگاه‌های Tablet را از Mobile تفکیک کنم؟ سه سیگنال: اول، regex روی User-Agent. دوم، عرض viewport (معمولاً بین ۷۶۸ تا ۱۰۲۴). سوم، Client Hints اگر شامل platform باشد.

آیا دستگاه‌های IoT هم به‌عنوان بازدیدکننده ثبت می‌شوند؟ اگر این دستگاه‌ها مرورگر داشته باشند، بله. ولی اغلب IoTها مرورگر کامل ندارند و به‌عنوان unknown یا bot تشخیص داده می‌شوند.

نگاهی از منظر مهندس داده در مقیاس بزرگ

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

مفهوم اول، latency بودجه. تشخیص دستگاه نباید بیش از ۲ میلی‌ثانیه به هر درخواست اضافه کند. اگر لایه‌های شما بیش از این مقدار زمان می‌گیرند، باید به فکر معماری توزیع‌شده باشید. یک راه‌حل رایج، ذخیره‌ی نتیجه‌ی تشخیص در یک cache با TTL بالا است. برای هر ترکیب IP+UA، نتیجه‌ی تشخیص را برای ۲۴ ساعت کش کنید.

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

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

نکته‌ی آخر: در مقیاس بالا، دقت مدل، در نهایت به کیفیت داده‌ی برچسب‌دار بستگی دارد. بدون یک فرایند بازبینی دقیق، مدل شما در طول زمان به‌تدریج منحرف می‌شود. سرمایه‌گذاری در فرایند برچسب‌گذاری و بازبینی، در مقیاس بزرگ چند برابر جواب می‌دهد.

پرسشی که در پایان باید پاسخ دهید

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

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