ভিজিবিলিটি স্টেট ট্রানজিশন: নির্ভরযোগ্য বিকন ছাড়াই পেজ ভিউ মাপা
pagehide, visibilitychange আর Beacon API প্রতিটি ব্রাউজারে আলাদা আচরণ করে। কুকি বা স্থায়ী শনাক্তকারী ছাড়াই সেশনের সমাপ্তি নির্ভরযোগ্যভাবে নথিভুক্ত করার উপায় দেখুন।
কোনো পেজ দেখা হয়েছে — এটুকু নথিভুক্ত করা সহজ। কিন্তু ব্যবহারকারী আসলে কখন চলে গেলেন তা নথিভুক্ত করা — নির্ভরযোগ্যভাবে, ট্যাব বদল, ব্যাকগ্রাউন্ডে চলে যাওয়া আর হঠাৎ বন্ধ হওয়ার মধ্যেও — ওয়েব অ্যানালিটিক্সের অন্যতম কঠিন সমস্যা। এটি গুরুত্বপূর্ণ, কারণ এনগেজমেন্ট মেট্রিক (অবস্থানকাল, বাউন্স, স্ক্রল গভীরতা) নির্ভর করে একটি পরিচ্ছন্ন সেশন-সমাপ্তি সংকেত ধরতে পারার উপর। ভুল করলে হয় ডেটা হারাবেন, নয়তো তা ফুলিয়ে ফেলবেন।
Monoid এটি সমাধান করে কুকি, localStorage কিংবা কোনো স্থায়ী শনাক্তকারী ছাড়াই। এই সীমাবদ্ধতা আসলে কাজটাকে সহজ করে দেয়: পেজ লোডের মধ্যে দিয়ে কোনো সেশন মিলিয়ে নেওয়ার দরকার আমাদের কখনোই হয় না, তাই আমাদের একটাই চাওয়া — প্রতি পেজ লাইফসাইকেলে একটি নির্ভুল চূড়ান্ত নমুনা পাঠানো। ব্রাউজারের প্রিমিটিভগুলো কেমন আচরণ করে, ফাঁদগুলো কোথায়, আর এর মধ্যে Monoid-এর নিজস্ব ট্র্যাকার আসলে কোনটি ব্যবহার করে, তা নিচে দেওয়া হলো।
unload কেন ভুল ইভেন্ট
ঐতিহাসিক পদ্ধতি ছিল unload বা beforeunload শোনা এবং একটি সিনক্রোনাস XHR পাঠানো। আজ এটি রীতিমতো ক্ষতিকর। মোবাইলে unload ইভেন্ট নির্ভরযোগ্য নয়: ব্যবহারকারী ট্যাবটি ব্যাকগ্রাউন্ডে পাঠালে আর OS সেটি পুনরুদ্ধার করলে unload প্রায়ই একেবারেই চলে না। আরও খারাপ, unload হ্যান্ডলার নিবন্ধন করলে বেশ কয়েকটি ব্রাউজারে পেজটি ব্যাক/ফরওয়ার্ড ক্যাশে (bfcache) থেকে বাদ পড়ে যায়, যা নেভিগেশনের কর্মক্ষমতা নষ্ট করে।
web.dev-এর Page Lifecycle API নির্দেশনা স্পষ্ট: visibilitychange-এর hidden-এ যাওয়াকেই আপনার দেখা শেষ নির্ভরযোগ্য ইভেন্ট ধরুন। মোবাইলে unload বা pagehide চলবেই — এমন ভরসা করবেন না।
visibilitychange ও pagehide-এর জুটি
শক্তপোক্ত প্যাটার্ন হলো visibilitychange শোনা এবং document.visibilityState === 'hidden' যাচাই করা, সঙ্গে গৌণ সংকেত হিসেবে pagehide। পেজ যখনই লুকানো অবস্থায় যায়, যা পাঠানোর তা ফ্লাশ করে দিন। Page Visibility API-র MDN ডকুমেন্টেশন নিশ্চিত করে যে ট্যাব ব্যাকগ্রাউন্ডে গেলে বা ব্রাউজার মিনিমাইজ হলে visibilitychange চলে — "ব্যবহারকারী আর তাকিয়ে নেই"-এর সবচেয়ে কাছাকাছি বিকল্প।
জটিলতাটি হলো: লুকানো অবস্থায় যাওয়া এক সেশনেই একাধিকবার ঘটতে পারে (ট্যাব ছেড়ে যাওয়া, ফিরে আসা, আবার ছেড়ে যাওয়া)। তাই আপনার ফ্লাশকে idempotent করতে হবে, debounce করতে হবে, অথবা নিজের ঘড়ি রিসেট করতে হবে যাতে প্রতিটি ট্রিগার ক্রমবর্ধমান মোট না দিয়ে একটি নতুন, ওভারল্যাপবিহীন অংশ জানায়। Monoid শেষোক্ত পথটি নেয়: অবস্থানকাল নমুনার পেছনের টাইমারটি প্রতিটি visibilitychange ট্রানজিশনে রিসেট হয়, ফলে একই পেজ লাইফসাইকেলে পরে ঘটা দ্বিতীয় hidden ইভেন্ট কেবল শেষ ফ্লাশের পর থেকে সময়টুকুই জানায়। Monoid visibilitychange শোনে (আগে থেকে থাকা beforeunload ফলব্যাকের পাশাপাশি) কিন্তু আলাদা করে pagehide লিসেনার যোগ করে না — যেসব ক্ষেত্রে গুরুত্বপূর্ণ, সেখানে দুটি যথেষ্ট পরিমাণে ওভারল্যাপ করে যে দ্বিতীয় লিসেনার নিচের দ্বৈত-গণনা সমস্যাটিই আবার নিয়ে আসবে, উল্লেখযোগ্যভাবে বেশি পরিসর কভার না করেই।
মৃতপ্রায় পেজ থেকে ডেটা পাঠানো: 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-এ ফলব্যাক করুন — Monoid আসলে এটি উল্টে দেয়, যার কারণ পরের অংশে ব্যাখ্যা করা হয়েছে।
একটি ন্যূনতম, কুকি-বিহীন বাস্তবায়ন
উপরের সবকিছু একত্র করা একটি রেফারেন্স প্যাটার্ন এখানে দেওয়া হলো — আগে sendBeacon, এর ফলব্যাক হিসেবে fetch keepalive, যা visibilitychange ও pagehide উভয় দ্বারাই ট্রিগার হয়, সঙ্গে একটি গার্ড যাতে পুনরাবৃত্ত ট্রিগার দ্বৈত-পাঠানো না ঘটায়:
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-এর নিজস্ব ট্র্যাকার সম্পর্কিত কিন্তু ভিন্ন একটি পথ নেয়: কেবল fetch keepalive (কোনো sendBeacon নয় — এটি ক্রস-অরিজিন ইনস্টলের জন্য প্রয়োজনীয় mode: 'cors' প্রকাশ করতে পারে না), কোনো sent গার্ড নেই, এবং একবার ট্রিগার হয়ে চুপ হয়ে যাওয়ার বদলে প্রতিটি ভিজিবিলিটি ট্রানজিশনে রিসেট হওয়া একটি টাইমার (স্পষ্টতার জন্য নিচে সম্মতি ও Do Not Track ফিল্টারিং বাদ দেওয়া হয়েছে):
function dur() {
fetch('/duration', {
method: 'POST',
body: JSON.stringify({ site_id: siteId, duration_ms: Date.now() - t }),
headers: { 'Content-Type': 'application/json' },
keepalive: true,
mode: 'cors',
});
}
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') dur();
t = Date.now();
});
window.addEventListener('beforeunload', dur);
প্রতিটি ট্রানজিশনে t রিসেট করা ঠিক সেই কাজই করে যা ওপরের sent গার্ড করে, তবে কোনো ফ্ল্যাগ ছাড়াই: প্রতিটি কল কেবল ঘড়ি সর্বশেষ রিসেট হওয়ার পর থেকে অংশটুকুই জানায়, তাই একই ট্যাব বন্ধ হওয়ার সময় visibilitychange-এর পর আসা beforeunload, দ্বৈত-গণনার বদলে একটি প্রকৃত অংশ ও একটি নিরীহ প্রায়-শূন্য অংশ জানায়।
কেন এটি প্রাইভেসি-ফার্স্ট অ্যানালিটিক্সের সঙ্গে মানানসই
যেহেতু Monoid কখনো সেশনগুলোকে জোড়া লাগায় না, প্রতিটি রিকোয়েস্টই একটি সম্পূর্ণ, ফেলে দেওয়ার মতো পর্যবেক্ষণ। রক্ষা করার মতো কোনো ক্রস-পেজ শনাক্তকারী নেই, তাই কোনো নমুনা হারানো বা দ্বৈত হওয়া আপনার ক্ষতি একটি ডেটা পয়েন্ট, বিকৃত কোনো ব্যবহারকারী-প্রোফাইল নয়। Monoid প্রকৃতপক্ষে যে visibilitychange ও beforeunload প্যাটার্ন বিতরণ করে তা অবস্থানকালের নির্ভুল পরিমাপ দেয়, আবার পেজকে দ্রুত ও bfcache-বান্ধব রাখে — যা নিজেই সেই Core Web Vitals উন্নত করে, যেগুলো হয়তো আপনি মাপতেই চাইছেন।
প্রস্থানটুকু মাপুন, মানুষটিকে নয়। ব্রাউজার আপনাকে প্রয়োজনীয় সবকিছু আগে থেকেই দিয়ে রেখেছে।
Comments
Loading comments…