From bd7d3751a494a9429e4247b1c69b8cdf71c5061f Mon Sep 17 00:00:00 2001 From: Ayu Date: Tue, 3 Sep 2024 22:37:46 +0300 Subject: [PATCH] fix(tracker): send unload event on page hidden instead (#139) * fix(tracker): send unload event on page hidden instead * perf(tracker); remove unnecessary root param * test: unlock firefox unload test --- tracker/README.md | 10 ++++----- tracker/dist/click-events.js | 22 ++++++-------------- tracker/dist/click-events.min.js | 2 +- tracker/dist/click-events.page-events.js | 22 ++++++-------------- tracker/dist/click-events.page-events.min.js | 2 +- tracker/dist/default.js | 22 ++++++-------------- tracker/dist/default.min.js | 2 +- tracker/dist/page-events.js | 22 ++++++-------------- tracker/dist/page-events.min.js | 2 +- tracker/src/tracker.js | 22 ++++++-------------- tracker/tests/helpers/load-unload.js | 5 ++--- 11 files changed, 41 insertions(+), 92 deletions(-) diff --git a/tracker/README.md b/tracker/README.md index 42a22d4..4eecdbc 100644 --- a/tracker/README.md +++ b/tracker/README.md @@ -8,11 +8,11 @@ The minified gzipped tracker is less than 1KB. The size is measured in its compr Our tracker is designed with compression in mind, given that web traffic is usually compressed. For example, certain optimisation techniques like inlining globals with shorter variable names are avoided, as they may decrease the uncompressed size of the tracker but result in an increase in the compressed size due to how dictionary-based compression techniques work. -| File | Size | Compressed (gzip) | Compressed (brotli) | -| --------------------- | -------------------- | -------------------- | ------------------- | -| `default.min.js` | 1574 bytes (1.54 KB) | 792 bytes (0.77 KB) | 639 bytes (0.62 KB) | -| `page-events.min.js` | 1812 bytes (1.77 KB) | 916 bytes (0.89 KB) | 751 bytes (0.73 KB) | -| `click-events.min.js` | 2053 bytes (2.00 KB) | 1003 bytes (0.98 KB) | 810 bytes (0.79 KB) | +| File | Size | Compressed (gzip) | Compressed (brotli) | +| --------------------- | -------------------- | ------------------- | ------------------- | +| `default.min.js` | 1512 bytes (1.48 KB) | 766 bytes (0.75 KB) | 620 bytes (0.61 KB) | +| `page-events.min.js` | 1750 bytes (1.71 KB) | 890 bytes (0.87 KB) | 731 bytes (0.71 KB) | +| `click-events.min.js` | 1991 bytes (1.94 KB) | 977 bytes (0.95 KB) | 792 bytes (0.77 KB) | The listed sizes only show the size of the tracker itself with one specific feature. When combining multiple features, the size of the tracker will relatively increase (although some features may share code with each other). diff --git a/tracker/dist/click-events.js b/tracker/dist/click-events.js index c17980e..8624685 100644 --- a/tracker/dist/click-events.js +++ b/tracker/dist/click-events.js @@ -80,16 +80,11 @@ */ let isUnique = true; - /** - * A temporary variable to store the start time of the page when it is hidden. - */ - let hiddenStartTime = 0; - /** * The total time the user has had the page hidden. * It also signifies the start epoch time of the page. */ - let hiddenTotalTime = Date.now(); + let startTime = Date.now(); /** * Ensure only the unload beacon is called once. @@ -117,8 +112,7 @@ // However, isFirstVisit will be called on each page load, so we don't need to reset it. isUnique = false; uid = generateUid(); - hiddenStartTime = 0; - hiddenTotalTime = Date.now(); + startTime = Date.now(); isUnloadCalled = false; }; @@ -259,7 +253,7 @@ ({ "b": uid, "e": "unload", - "m": Date.now() - hiddenTotalTime, + "m": Date.now() - startTime, }), ), ); @@ -331,19 +325,15 @@ addEventListener( 'visibilitychange', () => { - if (document.visibilityState == 'hidden') { + if (document.hidden) { // Page is hidden, record the current time. - hiddenStartTime = Date.now(); - } else { - // Page is visible, subtract the hidden time to calculate the total time hidden. - hiddenTotalTime += Date.now() - hiddenStartTime; - hiddenStartTime = 0; + sendUnloadBeacon(); } }, { capture: true }, ); - pingCache(host + 'event/ping?root').then((response) => { + pingCache(host + 'event/ping').then((response) => { // The response is a boolean indicating if the user is unique or not. isUnique = response; diff --git a/tracker/dist/click-events.min.js b/tracker/dist/click-events.min.js index d2a072e..cb9905f 100644 --- a/tracker/dist/click-events.min.js +++ b/tracker/dist/click-events.min.js @@ -1 +1 @@ -!function(){if(!document)return;const t=document.currentScript,e=t.getAttribute("data-api")?`${location.protocol}//${t.getAttribute("data-api")}`:t.src.replace(/[^\/]+$/,"api/"),n=()=>Date.now().toString(36)+Math.random().toString(36).substr(2);let o=n(),a=!0,i=0,r=Date.now(),c=!1;const s=history.pushState,d=history.replaceState,p=()=>{a=!1,o=n(),i=0,r=Date.now(),c=!1},l=t=>function(e,n,o){o&&location.pathname!==new URL(o,location.href).pathname?(m(),p(),t.apply(this,arguments),h()):t.apply(this,arguments)},u=t=>new Promise((e=>{const n=new XMLHttpRequest;n.onload=()=>{e(0==n.responseText)},n.open("GET",t),n.setRequestHeader("Content-Type","text/plain"),n.send()})),h=async()=>{u(e+"event/ping?u="+encodeURIComponent(location.host+location.pathname)).then((t=>{fetch(e+"event/hit",{method:"POST",body:JSON.stringify({b:o,e:"load",u:location.href,r:document.referrer,p:a,q:t,t:Intl.DateTimeFormat().resolvedOptions().timeZone}),mode:"no-cors"})}))},m=()=>{c||navigator.sendBeacon(e+"event/hit",JSON.stringify({b:o,e:"unload",m:Date.now()-r})),c=!0},g=t=>{if(t.button>1||!(t.target instanceof HTMLElement))return;const n=(t.target.getAttribute("data-m:click")||"").split(";").reduce(((t,e)=>{const[n,o]=e.split("=").map((t=>t.trim()));return n&&o&&(t[n]=o),t}),{});Object.keys(n).length>0&&fetch(e+"event/hit",{method:"POST",body:JSON.stringify({b:o,e:"custom",g:location.hostname,d:n}),mode:"no-cors"})};for(const t of document.querySelectorAll("[data-m\\:click]"))t.addEventListener("click",g),t.addEventListener("auxclick",g);"onpagehide"in self?addEventListener("pagehide",m,{capture:!0}):(addEventListener("beforeunload",m,{capture:!0}),addEventListener("unload",m,{capture:!0})),addEventListener("visibilitychange",(()=>{"hidden"==document.visibilityState?i=Date.now():(r+=Date.now()-i,i=0)}),{capture:!0}),u(e+"event/ping?root").then((e=>{a=e,h(),t.getAttribute("data-hash")?addEventListener("hashchange",h,{capture:!0}):(history.pushState=l(s),history.replaceState=l(d),addEventListener("popstate",(()=>{m(),p(),h()}),{capture:!0}))}))}(); \ No newline at end of file +!function(){if(!document)return;const t=document.currentScript,e=t.getAttribute("data-api")?`${location.protocol}//${t.getAttribute("data-api")}`:t.src.replace(/[^\/]+$/,"api/"),n=()=>Date.now().toString(36)+Math.random().toString(36).substr(2);let a=n(),o=!0,i=Date.now(),r=!1;const c=history.pushState,s=history.replaceState,d=()=>{o=!1,a=n(),i=Date.now(),r=!1},p=t=>function(e,n,a){a&&location.pathname!==new URL(a,location.href).pathname?(l(),d(),t.apply(this,arguments),h()):t.apply(this,arguments)},u=t=>new Promise((e=>{const n=new XMLHttpRequest;n.onload=()=>{e(0==n.responseText)},n.open("GET",t),n.setRequestHeader("Content-Type","text/plain"),n.send()})),h=async()=>{u(e+"event/ping?u="+encodeURIComponent(location.host+location.pathname)).then((t=>{fetch(e+"event/hit",{method:"POST",body:JSON.stringify({b:a,e:"load",u:location.href,r:document.referrer,p:o,q:t,t:Intl.DateTimeFormat().resolvedOptions().timeZone}),mode:"no-cors"})}))},l=()=>{r||navigator.sendBeacon(e+"event/hit",JSON.stringify({b:a,e:"unload",m:Date.now()-i})),r=!0},m=t=>{if(t.button>1||!(t.target instanceof HTMLElement))return;const n=(t.target.getAttribute("data-m:click")||"").split(";").reduce(((t,e)=>{const[n,a]=e.split("=").map((t=>t.trim()));return n&&a&&(t[n]=a),t}),{});Object.keys(n).length>0&&fetch(e+"event/hit",{method:"POST",body:JSON.stringify({b:a,e:"custom",g:location.hostname,d:n}),mode:"no-cors"})};for(const t of document.querySelectorAll("[data-m\\:click]"))t.addEventListener("click",m),t.addEventListener("auxclick",m);"onpagehide"in self?addEventListener("pagehide",l,{capture:!0}):(addEventListener("beforeunload",l,{capture:!0}),addEventListener("unload",l,{capture:!0})),addEventListener("visibilitychange",(()=>{document.hidden&&l()}),{capture:!0}),u(e+"event/ping").then((e=>{o=e,h(),t.getAttribute("data-hash")?addEventListener("hashchange",h,{capture:!0}):(history.pushState=p(c),history.replaceState=p(s),addEventListener("popstate",(()=>{l(),d(),h()}),{capture:!0}))}))}(); \ No newline at end of file diff --git a/tracker/dist/click-events.page-events.js b/tracker/dist/click-events.page-events.js index 72ede39..9bd184b 100644 --- a/tracker/dist/click-events.page-events.js +++ b/tracker/dist/click-events.page-events.js @@ -80,16 +80,11 @@ */ let isUnique = true; - /** - * A temporary variable to store the start time of the page when it is hidden. - */ - let hiddenStartTime = 0; - /** * The total time the user has had the page hidden. * It also signifies the start epoch time of the page. */ - let hiddenTotalTime = Date.now(); + let startTime = Date.now(); /** * Ensure only the unload beacon is called once. @@ -117,8 +112,7 @@ // However, isFirstVisit will be called on each page load, so we don't need to reset it. isUnique = false; uid = generateUid(); - hiddenStartTime = 0; - hiddenTotalTime = Date.now(); + startTime = Date.now(); isUnloadCalled = false; }; @@ -267,7 +261,7 @@ ({ "b": uid, "e": "unload", - "m": Date.now() - hiddenTotalTime, + "m": Date.now() - startTime, }), ), ); @@ -339,19 +333,15 @@ addEventListener( 'visibilitychange', () => { - if (document.visibilityState == 'hidden') { + if (document.hidden) { // Page is hidden, record the current time. - hiddenStartTime = Date.now(); - } else { - // Page is visible, subtract the hidden time to calculate the total time hidden. - hiddenTotalTime += Date.now() - hiddenStartTime; - hiddenStartTime = 0; + sendUnloadBeacon(); } }, { capture: true }, ); - pingCache(host + 'event/ping?root').then((response) => { + pingCache(host + 'event/ping').then((response) => { // The response is a boolean indicating if the user is unique or not. isUnique = response; diff --git a/tracker/dist/click-events.page-events.min.js b/tracker/dist/click-events.page-events.min.js index 50b334e..410073a 100644 --- a/tracker/dist/click-events.page-events.min.js +++ b/tracker/dist/click-events.page-events.min.js @@ -1 +1 @@ -!function(){if(!document)return;const t=document.currentScript,e=t.getAttribute("data-api")?`${location.protocol}//${t.getAttribute("data-api")}`:t.src.replace(/[^\/]+$/,"api/"),n=()=>Date.now().toString(36)+Math.random().toString(36).substr(2);let o=n(),a=!0,i=0,r=Date.now(),c=!1;const d=history.pushState,s=history.replaceState,l=()=>{a=!1,o=n(),i=0,r=Date.now(),c=!1},p=t=>function(e,n,o){o&&location.pathname!==new URL(o,location.href).pathname?(g(),l(),t.apply(this,arguments),m()):t.apply(this,arguments)},u=(t,e)=>(t.getAttribute("data-m:"+e)||"").split(";").reduce(((t,e)=>{const[n,o]=e.split("=").map((t=>t.trim()));return n&&o&&(t[n]=o),t}),{}),h=t=>new Promise((e=>{const n=new XMLHttpRequest;n.onload=()=>{e(0==n.responseText)},n.open("GET",t),n.setRequestHeader("Content-Type","text/plain"),n.send()})),m=async()=>{h(e+"event/ping?u="+encodeURIComponent(location.host+location.pathname)).then((t=>{fetch(e+"event/hit",{method:"POST",body:JSON.stringify({b:o,e:"load",u:location.href,r:document.referrer,p:a,q:t,t:Intl.DateTimeFormat().resolvedOptions().timeZone,d:[...document.querySelectorAll("[data-m\\:load]")].reduce(((t,e)=>({...t,...u(e,"load")})),{})}),mode:"no-cors"})}))},g=()=>{c||navigator.sendBeacon(e+"event/hit",JSON.stringify({b:o,e:"unload",m:Date.now()-r})),c=!0},y=t=>{if(t.button>1||!(t.target instanceof HTMLElement))return;const n=u(t.target,"click");Object.keys(n).length>0&&fetch(e+"event/hit",{method:"POST",body:JSON.stringify({b:o,e:"custom",g:location.hostname,d:n}),mode:"no-cors"})};for(const t of document.querySelectorAll("[data-m\\:click]"))t.addEventListener("click",y),t.addEventListener("auxclick",y);"onpagehide"in self?addEventListener("pagehide",g,{capture:!0}):(addEventListener("beforeunload",g,{capture:!0}),addEventListener("unload",g,{capture:!0})),addEventListener("visibilitychange",(()=>{"hidden"==document.visibilityState?i=Date.now():(r+=Date.now()-i,i=0)}),{capture:!0}),h(e+"event/ping?root").then((e=>{a=e,m(),t.getAttribute("data-hash")?addEventListener("hashchange",m,{capture:!0}):(history.pushState=p(d),history.replaceState=p(s),addEventListener("popstate",(()=>{g(),l(),m()}),{capture:!0}))}))}(); \ No newline at end of file +!function(){if(!document)return;const t=document.currentScript,e=t.getAttribute("data-api")?`${location.protocol}//${t.getAttribute("data-api")}`:t.src.replace(/[^\/]+$/,"api/"),n=()=>Date.now().toString(36)+Math.random().toString(36).substr(2);let a=n(),o=!0,r=Date.now(),i=!1;const c=history.pushState,d=history.replaceState,s=()=>{o=!1,a=n(),r=Date.now(),i=!1},l=t=>function(e,n,a){a&&location.pathname!==new URL(a,location.href).pathname?(m(),s(),t.apply(this,arguments),h()):t.apply(this,arguments)},p=(t,e)=>(t.getAttribute("data-m:"+e)||"").split(";").reduce(((t,e)=>{const[n,a]=e.split("=").map((t=>t.trim()));return n&&a&&(t[n]=a),t}),{}),u=t=>new Promise((e=>{const n=new XMLHttpRequest;n.onload=()=>{e(0==n.responseText)},n.open("GET",t),n.setRequestHeader("Content-Type","text/plain"),n.send()})),h=async()=>{u(e+"event/ping?u="+encodeURIComponent(location.host+location.pathname)).then((t=>{fetch(e+"event/hit",{method:"POST",body:JSON.stringify({b:a,e:"load",u:location.href,r:document.referrer,p:o,q:t,t:Intl.DateTimeFormat().resolvedOptions().timeZone,d:[...document.querySelectorAll("[data-m\\:load]")].reduce(((t,e)=>({...t,...p(e,"load")})),{})}),mode:"no-cors"})}))},m=()=>{i||navigator.sendBeacon(e+"event/hit",JSON.stringify({b:a,e:"unload",m:Date.now()-r})),i=!0},g=t=>{if(t.button>1||!(t.target instanceof HTMLElement))return;const n=p(t.target,"click");Object.keys(n).length>0&&fetch(e+"event/hit",{method:"POST",body:JSON.stringify({b:a,e:"custom",g:location.hostname,d:n}),mode:"no-cors"})};for(const t of document.querySelectorAll("[data-m\\:click]"))t.addEventListener("click",g),t.addEventListener("auxclick",g);"onpagehide"in self?addEventListener("pagehide",m,{capture:!0}):(addEventListener("beforeunload",m,{capture:!0}),addEventListener("unload",m,{capture:!0})),addEventListener("visibilitychange",(()=>{document.hidden&&m()}),{capture:!0}),u(e+"event/ping").then((e=>{o=e,h(),t.getAttribute("data-hash")?addEventListener("hashchange",h,{capture:!0}):(history.pushState=l(c),history.replaceState=l(d),addEventListener("popstate",(()=>{m(),s(),h()}),{capture:!0}))}))}(); \ No newline at end of file diff --git a/tracker/dist/default.js b/tracker/dist/default.js index 249b153..497a7c9 100644 --- a/tracker/dist/default.js +++ b/tracker/dist/default.js @@ -80,16 +80,11 @@ */ let isUnique = true; - /** - * A temporary variable to store the start time of the page when it is hidden. - */ - let hiddenStartTime = 0; - /** * The total time the user has had the page hidden. * It also signifies the start epoch time of the page. */ - let hiddenTotalTime = Date.now(); + let startTime = Date.now(); /** * Ensure only the unload beacon is called once. @@ -117,8 +112,7 @@ // However, isFirstVisit will be called on each page load, so we don't need to reset it. isUnique = false; uid = generateUid(); - hiddenStartTime = 0; - hiddenTotalTime = Date.now(); + startTime = Date.now(); isUnloadCalled = false; }; @@ -243,7 +237,7 @@ ({ "b": uid, "e": "unload", - "m": Date.now() - hiddenTotalTime, + "m": Date.now() - startTime, }), ), ); @@ -273,19 +267,15 @@ addEventListener( 'visibilitychange', () => { - if (document.visibilityState == 'hidden') { + if (document.hidden) { // Page is hidden, record the current time. - hiddenStartTime = Date.now(); - } else { - // Page is visible, subtract the hidden time to calculate the total time hidden. - hiddenTotalTime += Date.now() - hiddenStartTime; - hiddenStartTime = 0; + sendUnloadBeacon(); } }, { capture: true }, ); - pingCache(host + 'event/ping?root').then((response) => { + pingCache(host + 'event/ping').then((response) => { // The response is a boolean indicating if the user is unique or not. isUnique = response; diff --git a/tracker/dist/default.min.js b/tracker/dist/default.min.js index 8e8bbfa..85baeef 100644 --- a/tracker/dist/default.min.js +++ b/tracker/dist/default.min.js @@ -1 +1 @@ -!function(){if(!document)return;const t=document.currentScript,e=t.getAttribute("data-api")?`${location.protocol}//${t.getAttribute("data-api")}`:t.src.replace(/[^\/]+$/,"api/"),n=()=>Date.now().toString(36)+Math.random().toString(36).substr(2);let a=n(),o=!0,i=0,r=Date.now(),s=!1;const d=history.pushState,p=history.replaceState,c=()=>{o=!1,a=n(),i=0,r=Date.now(),s=!1},h=t=>function(e,n,a){a&&location.pathname!==new URL(a,location.href).pathname?(m(),c(),t.apply(this,arguments),l()):t.apply(this,arguments)},u=t=>new Promise((e=>{const n=new XMLHttpRequest;n.onload=()=>{e(0==n.responseText)},n.open("GET",t),n.setRequestHeader("Content-Type","text/plain"),n.send()})),l=async()=>{u(e+"event/ping?u="+encodeURIComponent(location.host+location.pathname)).then((t=>{fetch(e+"event/hit",{method:"POST",body:JSON.stringify({b:a,e:"load",u:location.href,r:document.referrer,p:o,q:t,t:Intl.DateTimeFormat().resolvedOptions().timeZone}),mode:"no-cors"})}))},m=()=>{s||navigator.sendBeacon(e+"event/hit",JSON.stringify({b:a,e:"unload",m:Date.now()-r})),s=!0};"onpagehide"in self?addEventListener("pagehide",m,{capture:!0}):(addEventListener("beforeunload",m,{capture:!0}),addEventListener("unload",m,{capture:!0})),addEventListener("visibilitychange",(()=>{"hidden"==document.visibilityState?i=Date.now():(r+=Date.now()-i,i=0)}),{capture:!0}),u(e+"event/ping?root").then((e=>{o=e,l(),t.getAttribute("data-hash")?addEventListener("hashchange",l,{capture:!0}):(history.pushState=h(d),history.replaceState=h(p),addEventListener("popstate",(()=>{m(),c(),l()}),{capture:!0}))}))}(); \ No newline at end of file +!function(){if(!document)return;const e=document.currentScript,t=e.getAttribute("data-api")?`${location.protocol}//${e.getAttribute("data-api")}`:e.src.replace(/[^\/]+$/,"api/"),n=()=>Date.now().toString(36)+Math.random().toString(36).substr(2);let a=n(),o=!0,i=Date.now(),r=!1;const s=history.pushState,d=history.replaceState,p=()=>{o=!1,a=n(),i=Date.now(),r=!1},c=e=>function(t,n,a){a&&location.pathname!==new URL(a,location.href).pathname?(l(),p(),e.apply(this,arguments),u()):e.apply(this,arguments)},h=e=>new Promise((t=>{const n=new XMLHttpRequest;n.onload=()=>{t(0==n.responseText)},n.open("GET",e),n.setRequestHeader("Content-Type","text/plain"),n.send()})),u=async()=>{h(t+"event/ping?u="+encodeURIComponent(location.host+location.pathname)).then((e=>{fetch(t+"event/hit",{method:"POST",body:JSON.stringify({b:a,e:"load",u:location.href,r:document.referrer,p:o,q:e,t:Intl.DateTimeFormat().resolvedOptions().timeZone}),mode:"no-cors"})}))},l=()=>{r||navigator.sendBeacon(t+"event/hit",JSON.stringify({b:a,e:"unload",m:Date.now()-i})),r=!0};"onpagehide"in self?addEventListener("pagehide",l,{capture:!0}):(addEventListener("beforeunload",l,{capture:!0}),addEventListener("unload",l,{capture:!0})),addEventListener("visibilitychange",(()=>{document.hidden&&l()}),{capture:!0}),h(t+"event/ping").then((t=>{o=t,u(),e.getAttribute("data-hash")?addEventListener("hashchange",u,{capture:!0}):(history.pushState=c(s),history.replaceState=c(d),addEventListener("popstate",(()=>{l(),p(),u()}),{capture:!0}))}))}(); \ No newline at end of file diff --git a/tracker/dist/page-events.js b/tracker/dist/page-events.js index 4583137..fd230bd 100644 --- a/tracker/dist/page-events.js +++ b/tracker/dist/page-events.js @@ -80,16 +80,11 @@ */ let isUnique = true; - /** - * A temporary variable to store the start time of the page when it is hidden. - */ - let hiddenStartTime = 0; - /** * The total time the user has had the page hidden. * It also signifies the start epoch time of the page. */ - let hiddenTotalTime = Date.now(); + let startTime = Date.now(); /** * Ensure only the unload beacon is called once. @@ -117,8 +112,7 @@ // However, isFirstVisit will be called on each page load, so we don't need to reset it. isUnique = false; uid = generateUid(); - hiddenStartTime = 0; - hiddenTotalTime = Date.now(); + startTime = Date.now(); isUnloadCalled = false; }; @@ -267,7 +261,7 @@ ({ "b": uid, "e": "unload", - "m": Date.now() - hiddenTotalTime, + "m": Date.now() - startTime, }), ), ); @@ -297,19 +291,15 @@ addEventListener( 'visibilitychange', () => { - if (document.visibilityState == 'hidden') { + if (document.hidden) { // Page is hidden, record the current time. - hiddenStartTime = Date.now(); - } else { - // Page is visible, subtract the hidden time to calculate the total time hidden. - hiddenTotalTime += Date.now() - hiddenStartTime; - hiddenStartTime = 0; + sendUnloadBeacon(); } }, { capture: true }, ); - pingCache(host + 'event/ping?root').then((response) => { + pingCache(host + 'event/ping').then((response) => { // The response is a boolean indicating if the user is unique or not. isUnique = response; diff --git a/tracker/dist/page-events.min.js b/tracker/dist/page-events.min.js index 5610255..cfb6f3e 100644 --- a/tracker/dist/page-events.min.js +++ b/tracker/dist/page-events.min.js @@ -1 +1 @@ -!function(){if(!document)return;const t=document.currentScript,e=t.getAttribute("data-api")?`${location.protocol}//${t.getAttribute("data-api")}`:t.src.replace(/[^\/]+$/,"api/"),n=()=>Date.now().toString(36)+Math.random().toString(36).substr(2);let a=n(),o=!0,r=0,i=Date.now(),d=!1;const s=history.pushState,c=history.replaceState,p=()=>{o=!1,a=n(),r=0,i=Date.now(),d=!1},u=t=>function(e,n,a){a&&location.pathname!==new URL(a,location.href).pathname?(m(),p(),t.apply(this,arguments),h()):t.apply(this,arguments)},l=t=>new Promise((e=>{const n=new XMLHttpRequest;n.onload=()=>{e(0==n.responseText)},n.open("GET",t),n.setRequestHeader("Content-Type","text/plain"),n.send()})),h=async()=>{l(e+"event/ping?u="+encodeURIComponent(location.host+location.pathname)).then((t=>{fetch(e+"event/hit",{method:"POST",body:JSON.stringify({b:a,e:"load",u:location.href,r:document.referrer,p:o,q:t,t:Intl.DateTimeFormat().resolvedOptions().timeZone,d:[...document.querySelectorAll("[data-m\\:load]")].reduce(((t,e)=>{return{...t,...(n=e,(n.getAttribute("data-m:load")||"").split(";").reduce(((t,e)=>{const[n,a]=e.split("=").map((t=>t.trim()));return n&&a&&(t[n]=a),t}),{}))};var n}),{})}),mode:"no-cors"})}))},m=()=>{d||navigator.sendBeacon(e+"event/hit",JSON.stringify({b:a,e:"unload",m:Date.now()-i})),d=!0};"onpagehide"in self?addEventListener("pagehide",m,{capture:!0}):(addEventListener("beforeunload",m,{capture:!0}),addEventListener("unload",m,{capture:!0})),addEventListener("visibilitychange",(()=>{"hidden"==document.visibilityState?r=Date.now():(i+=Date.now()-r,r=0)}),{capture:!0}),l(e+"event/ping?root").then((e=>{o=e,h(),t.getAttribute("data-hash")?addEventListener("hashchange",h,{capture:!0}):(history.pushState=u(s),history.replaceState=u(c),addEventListener("popstate",(()=>{m(),p(),h()}),{capture:!0}))}))}(); \ No newline at end of file +!function(){if(!document)return;const t=document.currentScript,e=t.getAttribute("data-api")?`${location.protocol}//${t.getAttribute("data-api")}`:t.src.replace(/[^\/]+$/,"api/"),n=()=>Date.now().toString(36)+Math.random().toString(36).substr(2);let a=n(),o=!0,r=Date.now(),i=!1;const d=history.pushState,s=history.replaceState,c=()=>{o=!1,a=n(),r=Date.now(),i=!1},p=t=>function(e,n,a){a&&location.pathname!==new URL(a,location.href).pathname?(l(),c(),t.apply(this,arguments),h()):t.apply(this,arguments)},u=t=>new Promise((e=>{const n=new XMLHttpRequest;n.onload=()=>{e(0==n.responseText)},n.open("GET",t),n.setRequestHeader("Content-Type","text/plain"),n.send()})),h=async()=>{u(e+"event/ping?u="+encodeURIComponent(location.host+location.pathname)).then((t=>{fetch(e+"event/hit",{method:"POST",body:JSON.stringify({b:a,e:"load",u:location.href,r:document.referrer,p:o,q:t,t:Intl.DateTimeFormat().resolvedOptions().timeZone,d:[...document.querySelectorAll("[data-m\\:load]")].reduce(((t,e)=>{return{...t,...(n=e,(n.getAttribute("data-m:load")||"").split(";").reduce(((t,e)=>{const[n,a]=e.split("=").map((t=>t.trim()));return n&&a&&(t[n]=a),t}),{}))};var n}),{})}),mode:"no-cors"})}))},l=()=>{i||navigator.sendBeacon(e+"event/hit",JSON.stringify({b:a,e:"unload",m:Date.now()-r})),i=!0};"onpagehide"in self?addEventListener("pagehide",l,{capture:!0}):(addEventListener("beforeunload",l,{capture:!0}),addEventListener("unload",l,{capture:!0})),addEventListener("visibilitychange",(()=>{document.hidden&&l()}),{capture:!0}),u(e+"event/ping").then((e=>{o=e,h(),t.getAttribute("data-hash")?addEventListener("hashchange",h,{capture:!0}):(history.pushState=p(d),history.replaceState=p(s),addEventListener("popstate",(()=>{l(),c(),h()}),{capture:!0}))}))}(); \ No newline at end of file diff --git a/tracker/src/tracker.js b/tracker/src/tracker.js index 209aeae..ed3779b 100644 --- a/tracker/src/tracker.js +++ b/tracker/src/tracker.js @@ -80,16 +80,11 @@ */ let isUnique = true; - /** - * A temporary variable to store the start time of the page when it is hidden. - */ - let hiddenStartTime = 0; - /** * The total time the user has had the page hidden. * It also signifies the start epoch time of the page. */ - let hiddenTotalTime = Date.now(); + let startTime = Date.now(); /** * Ensure only the unload beacon is called once. @@ -117,8 +112,7 @@ // However, isFirstVisit will be called on each page load, so we don't need to reset it. isUnique = false; uid = generateUid(); - hiddenStartTime = 0; - hiddenTotalTime = Date.now(); + startTime = Date.now(); isUnloadCalled = false; }; @@ -271,7 +265,7 @@ ({ "b": uid, "e": "unload", - "m": Date.now() - hiddenTotalTime, + "m": Date.now() - startTime, }), ), ); @@ -345,19 +339,15 @@ addEventListener( 'visibilitychange', () => { - if (document.visibilityState == 'hidden') { + if (document.hidden) { // Page is hidden, record the current time. - hiddenStartTime = Date.now(); - } else { - // Page is visible, subtract the hidden time to calculate the total time hidden. - hiddenTotalTime += Date.now() - hiddenStartTime; - hiddenStartTime = 0; + sendUnloadBeacon(); } }, { capture: true }, ); - pingCache(host + 'event/ping?root').then((response) => { + pingCache(host + 'event/ping').then((response) => { // The response is a boolean indicating if the user is unique or not. isUnique = response; diff --git a/tracker/tests/helpers/load-unload.js b/tracker/tests/helpers/load-unload.js index df479ee..47430b8 100644 --- a/tracker/tests/helpers/load-unload.js +++ b/tracker/tests/helpers/load-unload.js @@ -18,7 +18,7 @@ const loadUnloadTests = (name) => { const expectedRequests = [ { method: 'GET', - url: '/api/event/ping?root', + url: '/api/event/ping', status: 200, responseBody: '0', }, @@ -64,11 +64,10 @@ const loadUnloadTests = (name) => { postData: { e: 'unload', }, - ignoreBrowsers: ['firefox'], // TODO: Investigate why this request is not sent in Firefox (works outside Playwright) }, { method: 'GET', - url: '/api/event/ping?root', + url: '/api/event/ping', status: 200, responseBody: '1', },