رباتهایی که جاوااسکریپت اجرا میکنند و آمار را خراب میکنند
ماجرای روزی که بیشتر جلسههای آنالیتیکس اختصاصی ما ربات بودند، چرا فیلتر user-agent کافی نبود و چطور علامتگذاری رفتاری آمار را یکدست کرد.
در آنالیتیکس اختصاصی خودمان یک روز متوجه شدیم حدود ۷۰٪ جلسهها رباتاند. مشکل فقط شمارندهی بالا نبود. این رباتها مرورگر کامل اجرا میکردند، از فیلتر user-agent رد میشدند و نرخ تبدیل و زمان ماندن را طوری عوض میکردند که هیچ تصمیمی روی آنها قابل اتکا نبود. در این یادداشت مینویسیم چه دیدیم، چرا فیلتر قبلی جواب نداد و چه چیزی را عوض کردیم.
ماجرا و علامتها
آنالیتیکس اختصاصی ما بازدیدها را خودش جمع میکند و گزارشهایش را در چند تب نشان میدهد: بازدید، منابع ترافیک، تبدیل، زمان ماندن. آن روز عددها با انتظارمان نمیخواندند. در نهایت معلوم شد حدود ۷۰٪ جلسهها ربات بودند.
این رباتها از نوع قدیمی نبودند. یک خزندهی ساده فقط HTML میگیرد و اسکریپت آنالیتیکس را اجرا نمیکند، پس اصلاً در دادهی ما ظاهر نمیشود. اینجا ثبت بازدید دقیقاً با جاوااسکریپت سمت مرورگر انجام میشود. یعنی هر جلسهای که ثبت شده بود، مرورگری بود که اسکریپت را واقعاً اجرا کرده بود.
نتیجه روی دو شاخص بیشتر از بقیه دیده میشد. نرخ تبدیل پایین آمد، چون مخرج کسر پر از جلسههایی بود که هرگز قرار نبود چیزی بخرند. زمان ماندن هم بیمعنی شد، چون ترکیب جلسههای واقعی با جلسههای ربات میانگینی میداد که هیچکدام از دو گروه را توصیف نمیکرد.
نکات کلیدی
- حدود ۷۰٪ جلسههای یک روز ربات بودند
- رباتها مرورگر کامل اجرا میکردند و از فیلتر user-agent عبور میکردند
- نرخ تبدیل و زمان ماندن بیشترین آسیب را دیدند
ردگیری علت
اولین کاری که کردیم این بود که ببینیم فیلتر user-agent چرا کار نکرده. فیلتر وجود داشت و درست اجرا میشد، ولی این رباتها user-agent شبیه مرورگر معمولی میفرستادند. فیلتر مبتنی بر رشتهی user-agent فقط رباتهایی را میگیرد که خودشان را معرفی میکنند.
این محدودیت مخصوص سیستم ما هم نیست. گوگلآنالیتیکس هم حذف ترافیک رباتهای شناختهشده را بر اساس پژوهش خودش و فهرست IAB انجام میدهد؛ همانطور که در راهنمای رسمی آن آمده، این حذف روی فهرستی از رباتهای شناختهشده بنا شده است، نه بر تشخیص رفتار. رباتی که خودش را پنهان میکند در چنین فهرستی نیست.
پس مسئله از «کدام user-agent را ببندیم» به «کدام رفتار انسانی نیست» تغییر کرد. این هم همان درسی است که در بررسی منابع ترافیک دیگر هم دیدهایم؛ مثلاً در ماجرای ChatGPT بهعنوان اولین منبع ترافیک هم برچسب ظاهری درخواست بهتنهایی نشان نمیداد چه کسی پشت آن است.
نکات کلیدی
- فیلتر user-agent فقط رباتهایی را میگیرد که خودشان را معرفی میکنند
- فهرست رباتهای شناختهشده هم برای رباتهای پنهانکار کافی نیست
علت اصلی: ثبت بازدید فقط با «ادعای» مرورگر
علت اصلی این بود که ما هویت بازدیدکننده را از چیزی میخواندیم که خود او اعلام میکند. user-agent یک هدر است و هر کلاینتی میتواند هر مقداری در آن بگذارد. وقتی ثبت بازدید هم با اجرای جاوااسکریپت انجام میشود، یک مرورگر خودکار همهی مراحل را مثل یک کاربر عادی طی میکند.
یک سیگنال استاندارد برای این وضعیت وجود دارد: `navigator.webdriver`. طبق مستندات MDN این ویژگی فقطخواندنی مشخص میکند مرورگر زیر کنترل اتوماسیون است یا نه؛ مثلاً در Chrome وقتی با `--enable-automation` یا `--headless` اجرا شود مقدار آن true است. مشخصات W3C WebDriver هم آن را از روی پرچم webdriver-active تعریف میکند.
ولی این سیگنال داوطلبانه است. خود مشخصات آن را به شوخی «مکمل Evil Bit» (RFC 3514) مینامد. یعنی مرورگری که میخواهد پنهان بماند میتواند آن را false نشان بدهد. نتیجهاش این است که `navigator.webdriver` یک علامت مفید است، ولی تنها علامت نمیتواند باشد. باید چند علامت مستقل را کنار هم گذاشت: خود این پرچم، زمان صفر یا نزدیک صفر بین ورود و رفتن، الگوی بازدید غیرانسانی (مثلاً پیمایش منظم صفحهها بدون هیچ تعاملی) و منشأ شبکهای از رنجهای دیتاسنتر.
راهحل: علامتگذاری بعد از بازدید و حذف یکسان
تصمیم گرفتیم تشخیص را در لحظهی دریافت بازدید انجام ندهیم. جلسهها همانطور که هستند ذخیره میشوند و چند دقیقه بعد از بازدید یک کار جداگانه جلسه را بررسی و در صورت مشکوک بودن علامتگذاری میکند. دلیلش این است که بعضی علامتها، مثل زمان ماندن و الگوی بازدید، تا پایان جلسه معلوم نمیشوند. علامتگذاری هم حذف فیزیکی نیست؛ جلسه با یک پرچم نگه داشته میشود تا اگر قاعده اشتباه بود بشود آن را اصلاح کرد.
بخش دوم به همان اندازه مهم بود: همهی گزارشها باید جلسههای علامتخورده را یکسان کنار بگذارند. نمونهی سادهی منطق امتیازدهی (با مقادیر ساختگی):
// نمونهی ساده؛ آستانهها و رنجها ساختگیاند
const DATACENTER_RANGES = ['203.0.113.0/24'];
function botScore(s: Session): number {
let score = 0;
if (s.navigatorWebdriver) score += 2;
if (s.durationMs === 0) score += 1;
if (s.pageviews > 1 && s.interactions === 0) score += 1;
if (inRanges(s.ip, DATACENTER_RANGES)) score += 2;
return score;
}
// اجرا چند دقیقه بعد از پایان بازدید
async function flagSuspicious() {
const sessions = await db.sessions.findUnflagged({ olderThanMinutes: 5 });
for (const s of sessions) {
await db.sessions.update(s.id, { isBot: botScore(s) >= 2 });
}
}
// تنها نقطهی ورود برای همهی کوئریهای گزارش
export const humanSessions = () => db.sessions.where({ isBot: false });نکات کلیدی
- تشخیص با چند علامت مستقل، نه فقط navigator.webdriver
- علامتگذاری چند دقیقه بعد از بازدید، چون بعضی علامتها دیرتر کامل میشوند
- همهی گزارشها از یک نقطهی ورود مشترک برای جلسههای انسانی میخوانند
درسها و چکلیست
مهمترین درس این بود که فیلتر ربات یک ویژگی یکبارمصرف نیست. هر کوئری جدید روی دادهی بازدید باید همان فیلتر را داشته باشد، وگرنه عددهایش با تبهای دیگر نمیخواند. اگر یک گزارش جلسههای ربات را حذف کند و گزارش دیگر نه، دو عدد متفاوت برای یک شاخص میبینید و دلیلش را فقط وقتی میفهمید که کسی بپرسد. اشتباهی از این جنس را در ماجرای تست سلامتی که از مسیر واقعی جدا بود هم دیدهایم: دو مسیر که قرار بود یکی باشند، بیصدا از هم فاصله گرفتند.
اگر آنالیتیکس بخشی از یک فروشگاه اینترنتی یا وباپلیکیشن اختصاصی است، بهتر است فیلتر ربات از اول جزو طراحی داده باشد، نه وصلهای بعد از اولین عدد عجیب.
نکات کلیدی
- user-agent را هویت قطعی فرض نکنید
- navigator.webdriver را یکی از چند علامت بگیرید، نه تنها علامت
- جلسه را علامتگذاری کنید، نه اینکه فوراً حذف کنید
- همهی کوئریهای گزارش باید از یک تابع یا view مشترک بخوانند
- هر کوئری جدید را قبل از انتشار با عدد تبهای موجود مقایسه کنید
جمعبندی
رباتهایی که جاوااسکریپت اجرا میکنند با فیلتر user-agent دیده نمیشوند، چون هویتشان را خودشان اعلام میکنند. تشخیص رفتاری با چند علامت، علامتگذاری بعد از بازدید و یک فیلتر مشترک برای همهی گزارشها، آمار را دوباره قابل مقایسه کرد.
منابع
- MDN: Navigator.webdriver propertydeveloper.mozilla.org
- W3C WebDriver (Working Draft)w3.org
- Google Analytics Help: Known bot-traffic exclusionsupport.google.com