विज़िबिलिटी स्टेट ट्रांज़िशन: भरोसेमंद बीकन के बिना पेज व्यू मापना
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 की कुछ सीमाएँ हैं जिनका सम्मान करना चाहिए:
- पेलोड का आकार. विनिर्देश उपयोगकर्ता एजेंट को इजाज़त देता है कि वह कार्यान्वयन-निर्धारित सीमा (आमतौर पर 64 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 सेशनों को कभी आपस में सिलता नहीं, हर रिक्वेस्ट एक पूर्ण, फेंक देने योग्य प्रेक्षण है। बचाने के लिए कोई क्रॉस-पेज पहचानकर्ता है ही नहीं, इसलिए किसी सैंपल का छूट जाना या दोहराया जाना आपको एक डेटा बिंदु का नुक़सान देता है, बिगड़ी हुई उपयोगकर्ता प्रोफ़ाइल का नहीं। visibilitychange और beforeunload का यह पैटर्न, जिसे Monoid असल में वितरित करता है, ठहराव का सटीक माप देता है और साथ ही पेज को तेज़ तथा bfcache-अनुकूल रखता है — जो अपने आप उन्हीं Core Web Vitals को सुधारता है जिन्हें शायद आप मापने की कोशिश कर रहे हैं।
जाने को मापिए, व्यक्ति को नहीं। ब्राउज़र आपको ज़रूरत की हर चीज़ पहले से दे रहा है।
Comments
Loading comments…