ব্লগে ফিরে যান

ভিজিবিলিটি স্টেট ট্রানজিশন: নির্ভরযোগ্য বিকন ছাড়াই পেজ ভিউ মাপা

pagehide, visibilitychange আর Beacon API প্রতিটি ব্রাউজারে আলাদা আচরণ করে। কুকি বা স্থায়ী শনাক্তকারী ছাড়াই সেশনের সমাপ্তি নির্ভরযোগ্যভাবে নথিভুক্ত করার উপায় দেখুন।

কোনো পেজ দেখা হয়েছে — এটুকু নথিভুক্ত করা সহজ। কিন্তু ব্যবহারকারী আসলে কখন চলে গেলেন তা নথিভুক্ত করা — নির্ভরযোগ্যভাবে, ট্যাব বদল, ব্যাকগ্রাউন্ডে চলে যাওয়া আর হঠাৎ বন্ধ হওয়ার মধ্যেও — ওয়েব অ্যানালিটিক্সের অন্যতম কঠিন সমস্যা। এটি গুরুত্বপূর্ণ, কারণ এনগেজমেন্ট মেট্রিক (অবস্থানকাল, বাউন্স, স্ক্রল গভীরতা) নির্ভর করে একটি পরিচ্ছন্ন সেশন-সমাপ্তি সংকেত ধরতে পারার উপর। ভুল করলে হয় ডেটা হারাবেন, নয়তো তা ফুলিয়ে ফেলবেন।

Monoid এটি সমাধান করে কুকি, localStorage কিংবা কোনো স্থায়ী শনাক্তকারী ছাড়াই। এই সীমাবদ্ধতা আসলে কাজটাকে সহজ করে দেয়: পেজ লোডের মধ্যে দিয়ে কোনো সেশন মিলিয়ে নেওয়ার দরকার আমাদের কখনোই হয় না, তাই আমাদের একটাই চাওয়া — প্রতি পেজ লাইফসাইকেলে একটি নির্ভুল চূড়ান্ত বিকন পাঠানো। ব্রাউজারের প্রিমিটিভগুলো কেমন আচরণ করে আর ফাঁদগুলো কোথায়, তা নিচে দেওয়া হলো।

unload কেন ভুল ইভেন্ট

ঐতিহাসিক পদ্ধতি ছিল unload বা beforeunload শোনা এবং একটি সিনক্রোনাস XHR পাঠানো। আজ এটি রীতিমতো ক্ষতিকর। মোবাইলে unload ইভেন্ট নির্ভরযোগ্য নয়: ব্যবহারকারী ট্যাবটি ব্যাকগ্রাউন্ডে পাঠালে আর OS সেটি পুনরুদ্ধার করলে unload প্রায়ই একেবারেই চলে না। আরও খারাপ, unload হ্যান্ডলার নিবন্ধন করলে বেশ কয়েকটি ব্রাউজারে পেজটি ব্যাক/ফরওয়ার্ড ক্যাশে (bfcache) থেকে বাদ পড়ে যায়, যা নেভিগেশনের কর্মক্ষমতা নষ্ট করে।

web.dev-এর Page Lifecycle API নির্দেশনা স্পষ্ট: visibilitychange-এর hidden-এ যাওয়াকেই আপনার দেখা শেষ নির্ভরযোগ্য ইভেন্ট ধরুন। মোবাইলে unload বা pagehide চলবেই — এমন ভরসা করবেন না।

visibilitychangepagehide-এর জুটি

শক্তপোক্ত প্যাটার্ন হলো visibilitychange শোনা এবং document.visibilityState === 'hidden' যাচাই করা, সঙ্গে গৌণ সংকেত হিসেবে pagehide। পেজ যখনই লুকানো অবস্থায় যায়, যা পাঠানোর তা ফ্লাশ করে দিন। Page Visibility API-র MDN ডকুমেন্টেশন নিশ্চিত করে যে ট্যাব ব্যাকগ্রাউন্ডে গেলে বা ব্রাউজার মিনিমাইজ হলে visibilitychange চলে — "ব্যবহারকারী আর তাকিয়ে নেই"-এর সবচেয়ে কাছাকাছি বিকল্প।

জটিলতাটি হলো: লুকানো অবস্থায় যাওয়া এক সেশনেই একাধিকবার ঘটতে পারে (ট্যাব ছেড়ে যাওয়া, ফিরে আসা, আবার ছেড়ে যাওয়া)। তাই আপনার ফ্লাশকে idempotent করতে হবে, নয়তো debounce করতে হবে, নয়তো শেষ ফ্লাশের পর থেকে যতটুকু বদলেছে কেবল সেই ডেল্টাটুকুই পাঠাতে হবে। যেহেতু Monoid ব্যবহারকারী-ভিত্তিক কোনো অবস্থা রাখে না, আমরা একটি স্বয়ংসম্পূর্ণ পেলোড পাঠাই এবং এজকে রিকোয়েস্ট-স্কোপে ডিডুপ্লিকেট করতে দিই।

মৃতপ্রায় পেজ থেকে ডেটা পাঠানো: navigator.sendBeacon

visibilitychange-এর সময় একটি অ্যাসিনক্রোনাস fetch চালিয়ে তা শেষ হওয়ার আশা করা যায় না — ব্রাউজার তার আগেই পেজটি ভেঙে ফেলতে পারে। ঠিক এ কারণেই Beacon API স্পেসিফিকেশন রয়েছে। navigator.sendBeacon(url, data) একটি ছোট POST সারিবদ্ধ করে, যা ডকুমেন্ট চলে যাওয়ার পরেও পাঠানোর চেষ্টা ব্রাউজার নিশ্চিত করে — মূল থ্রেড আটকে না রেখে এবং নেভিগেশন দেরি না করিয়ে।

Beacon-এর কিছু সীমা আছে, যেগুলো মানা ভালো:

  • পেলোডের আকার। স্পেসিফিকেশন ইউজার এজেন্টকে অনুমতি দেয় বাস্তবায়ন-নির্ধারিত সীমার (সাধারণত ৬৪ KB) বেশি বিকন প্রত্যাখ্যান করার। পেলোড ক্ষুদ্র রাখুন — Monoid-এর পেলোড কয়েকশো বাইটের।
  • মেথড। বিকন সবসময় POST। আপনার এজ এন্ডপয়েন্ট GET আশা করলে সেটি বদলে নিন।
  • কোনো রেসপন্স নেই। ফিরতি কিছুই পড়া যায় না। পাঠিয়ে ভুলে যান।

আধুনিক ফলব্যাক হলো fetch(url, { keepalive: true }), যাকে Fetch স্ট্যান্ডার্ড সংজ্ঞায়িত করে ডকুমেন্টের চেয়ে বেশি সময় বেঁচে থাকার অনুমতি হিসেবে। keepalive আপনাকে এমন হেডার ও মেথড দেয় যা Beacon-এ নেই, তবে আকারের সর্বোচ্চ সীমা একই। আগে Beacon ব্যবহার করুন, তারপর keepalive সহ fetch-এ ফিরে যান।

একটি ন্যূনতম, কুকি-বিহীন বাস্তবায়ন

let sent = false;
function flush() {
  if (sent) return;
  sent = true;
  const payload = JSON.stringify({
    path: location.pathname,
    // কোনো শনাক্তকারী নয়, কোনো কুকি নয়, কোনো ফিঙ্গারপ্রিন্ট নয়
    visibleMs: Math.round(performance.now())
  });
  const ok = navigator.sendBeacon('/collect', payload);
  if (!ok) {
    fetch('/collect', { method: 'POST', body: payload, keepalive: true });
  }
}

document.addEventListener('visibilitychange', () => {
  if (document.visibilityState === 'hidden') flush();
});
window.addEventListener('pagehide', flush);

খেয়াল করুন এখানে কী নেই: কোনো unload লিসনার নেই, ফলে bfcache-এর যোগ্যতা অক্ষত থাকে। লোডের মাঝে সংরক্ষিত কোনো ID নেই। visibleMs-এর মানটি আসে performance.now() থেকে — একটি একমুখী ঘড়ি, যার জন্য দেয়াল-ঘড়ির টাইমস্ট্যাম্প বা স্থায়ী অবস্থা কোনোটিই লাগে না।

কেন এটি প্রাইভেসি-ফার্স্ট অ্যানালিটিক্সের সঙ্গে মানানসই

যেহেতু Monoid কখনো সেশনগুলোকে জোড়া লাগায় না, প্রতিটি বিকনই একটি সম্পূর্ণ, ফেলে দেওয়ার মতো পর্যবেক্ষণ। রক্ষা করার মতো কোনো ক্রস-পেজ শনাক্তকারী নেই, তাই একটি বিকন হারালে আপনার ক্ষতি একটি ডেটা পয়েন্ট, বিকৃত কোনো ব্যবহারকারী-প্রোফাইল নয়। visibilitychange ও Beacon-এর এই প্যাটার্ন অবস্থানকালের নির্ভুল পরিমাপ দেয়, আবার পেজকে দ্রুত ও bfcache-বান্ধব রাখে — যা নিজেই সেই Core Web Vitals উন্নত করে, যেগুলো হয়তো আপনি মাপতেই চাইছেন।

প্রস্থানটুকু মাপুন, মানুষটিকে নয়। ব্রাউজার আপনাকে প্রয়োজনীয় সবকিছু আগে থেকেই দিয়ে রেখেছে।

Sources

Comments

Loading comments…