From b8709fbf92308b0ed9b5c2645ab284373bb67b79 Mon Sep 17 00:00:00 2001 From: velixio <270455167+velixio@users.noreply.github.com> Date: Tue, 3 Nov 2026 05:45:50 +0530 Subject: [PATCH 1/3] tracker: send events in no-cors mode so redirects are followed A cors-mode fetch stops at a cross-origin redirect without CORS headers, which beacons followed. no-cors with a string body stays a simple request. --- internal/tracker/tracker.full.min.js | 2 +- internal/tracker/tracker.js | 6 ++++-- internal/tracker/tracker.min.js | 2 +- internal/tracker/tracker.runtime.min.js | 2 +- internal/tracker/tracker_runtime_test.js | 2 +- web/src/lib/tracker-full.test.ts | 4 ++-- 6 files changed, 10 insertions(+), 8 deletions(-) diff --git a/internal/tracker/tracker.full.min.js b/internal/tracker/tracker.full.min.js index 5ea2967d..250ab290 100644 --- a/internal/tracker/tracker.full.min.js +++ b/internal/tracker/tracker.full.min.js @@ -1 +1 @@ -(()=>{var e,t,s,o,i,a,r,c,l,u,m,f,p,g,v,w,C=document.currentScript,n=C.dataset;if(!n.site||navigator.webdriver||!C.hasAttribute("data-allow-localhost")&&/(^|\.)localhost$|^127\.|^\[::1\]$/.test(location.hostname))return;o=n.manual=="true",c=!o,g=location.href,w=n.api||"/api/public/v1/events";try{f=localStorage.supermetry_ignore}catch{}m=location.href,l=0,s=o?0:document.hidden?0:Date.now(),r=0,a=!o&&!0;function b(){return!f&&!navigator.globalPrivacyControl&&navigator.doNotTrack!="1"&&window.doNotTrack!="1"&&!u&&!t}function y(e){if(!b())return;e=JSON.stringify(e),fetch(w,{method:"POST",body:e,keepalive:!0}).catch(()=>0)}function k(e){return n.hash=="true"?e:e.split("#")[0]}function A(t){var s,o=e.beforeSend||window[n.beforeSend];if(typeof o!="function")return t;try{s=o({name:t.name,url:t.url,referrer:t.referrer,props:t.props,engagement_ms:t.engagement_ms,scroll_pct:t.scroll_pct})}catch{return}if(!s||typeof s!="object")return;return["name","url","referrer","props","engagement_ms","scroll_pct"].forEach(e=>{e in s&&(t[e]=s[e])}),t}v=n.consent=="required",t=v?[]:0;function _(e){if(u)return;if(t){if(t.length==20){var n=t.findIndex(e=>e.name!="pageview");t.splice(n<0?0:n,1)}t.push(e);return}e=A(e),e&&y(e)}function d(e,t,s,o,a,r){var c={v:1,ts:Date.now(),id:(crypto.randomUUID?.()||Math.random().toString(16)).replace(/\W/g,""),site:n.site,name:e,url:m,referrer:document.referrer,screen_width:screen.width,props:t||{},engagement_ms:s||0,scroll_pct:o||0,traits:r};a&&(c.revenue=a),i&&(c.profile_id=i),_(c);return;y(c)}function h(){if(!c)return;s&&(l+=Date.now()-s,s=0),O(),a&&(d("engagement",{},Math.round(l),r),l=r=0,a=!1)}function O(){var e=Math.max(document.documentElement.scrollHeight,document.body?.scrollHeight||0);e&&(r=Math.max(r,Math.min(100,Math.round(100*(scrollY+innerHeight)/e))))}function x(e,t){c=!0,g=location.href,m=e||location.href,l=r=0,s=document.hidden?0:Date.now(),a=!0,d("pageview",t)}function j(){if(o||k(location.href)==k(g))return;e._nav.forEach(e=>e()),h(),x()}function E(e){var t=history[e];history[e]=(...e)=>{t.apply(history,e),j()}}E("pushState"),addEventListener("popstate",j),n.hash=="true"&&addEventListener("hashchange",j),E("replaceState"),addEventListener("scroll",()=>{if(!c)return;O(),a=!0},{passive:!0}),document.addEventListener("visibilitychange",()=>c?document.hidden?h():(s=Date.now(),a=!0):0),addEventListener("pagehide",h),e=window.supermetry=window.supermetry||{},e._nav=[],e._post=y,e._allowed=b,p=n=>()=>{f=n,n&&(i=0,t&&(t=[]),e._clearHeatmap?.());try{n?localStorage.supermetry_ignore=1:delete localStorage.supermetry_ignore}catch{}e._privacyState=!n,e._privacy?.(!n)},e.optOut=p(1),e.optIn=p(0),e.pageview=e=>{var t=e?.url;try{t=t?new URL(t,location.href).href:location.href}catch{return}h(),x(t,e?.props)},e.consent=n=>{if(u=!n,n||(i=0,e._clearHeatmap?.()),e._privacyState=!!n,e._privacy?.(!!n),!v)return;var s=t;u=!n,t=n?0:[],n&&s&&s.forEach(_)},e.track=(e,t,n)=>d(e,t,0,0,n?.revenue),e.identify=(e,t={})=>{if(n.profiles=="false"||!b())return;if(e===null)return i=0;if(e!==String(e)||!e||e.length>128||!t||typeof t!="object"||Array.isArray(t))return;var s,o=0;for(s in t)if(++o>20||!s.length||s.length>64||typeof t[s]!="string"||t[s].length>256)return;i=e,d("identify",0,0,0,0,t)},o||d("pageview")})(),(()=>{var e,o,i,l,d,u,h,f=document.currentScript,t=f.dataset,s=window.supermetry,n=s?.track;if(!n)return;function j(e){if(e.button>1)return;var o,i,s=e.target.closest?.("a");if(!s)return;o={url:(s.origin+s.pathname+s.hash).slice(0,500)},t.outbound!="false"&&s.protocol.indexOf("http")==0&&s.host!=location.host&&n("Outbound Link: Click",o),i=(t.downloadExt||"pdf,xlsx,docx,csv,txt,zip,rar,7z,gz,dmg,exe,msi,pkg,apk,mp3,mp4,mov,avi,wav,key,pptx,ppt,xls,doc,epub,iso").toLowerCase().split(","),t.downloads!="false"&&i.indexOf(s.pathname.toLowerCase().split(".").pop())>=0&&n("File Download",o)}function v(e){if(t.clicks=="false")return;for(s=e.target,r=0;s&&r<4;r++,s=s.parentElement){if(c=s.getAttribute?.("data-supermetry-event"),c){for(var s,o,i,a,r,c,u,l={},d=0,h=0,m=0;d700)break;l[i]=u,h++}n(c.slice(0,75),l);return}if(a=g(s),a){n(a.name.slice(0,75),a.props);return}}}function g(e){var t,n,s,i,o={},a=0;for(i of e.classList||[]){if(i.indexOf("supermetry-event-")!=0)continue;n=i.slice(17).replace(/\+/g," "),t=n.indexOf("="),t<0?s=s||n:t&&t<=64&&n.length-t-1<=256&&++a<=20&&(o[n.slice(0,t)]=n.slice(t+1))}return s&&{name:s,props:o}}if(document.addEventListener("click",j),document.addEventListener("auxclick",j),document.addEventListener("click",v),document.addEventListener("submit",v),t.forms!="false"&&document.addEventListener("submit",e=>{var s,t=e.target;if(!t||t.tagName!="FORM"||t.getAttribute?.("data-supermetry-event")||g(t))return;s=(t.getAttribute("action")||"").split(/[?#]/)[0].slice(0,500),n("Form: Submit",{form_id:(t.getAttribute("id")||"").slice(0,100),form_name:(t.getAttribute("name")||"").slice(0,100),action:s})},!0),t[404]!="false"&&(f.hasAttribute("data-404-page")||document.querySelector('meta[name="supermetry:404"]'))&&n("404",{path:location.pathname.slice(0,500)}),t.vitals!="false"&&window.PerformanceObserver){function p(t,n){for(var s,o=0;o=1e3||s.startTime-u>=5e3)&&(u=s.startTime,i=0),i+=s.value,d=s.startTime,l=Math.max(l,i),e.cls=""+Math.round(l*1e3)}else t.kind==4?s.name=="first-contentful-paint"&&(e.fcp=""+Math.round(s.startTime)):s.interactionId&&(e.inp=""+Math.max(e.inp||0,Math.round(s.duration)))}function c(e,t,n){try{var s=new PerformanceObserver(e=>p(s,e.getEntries()));return s.kind=t,s.observe({type:e,buffered:n,durationThreshold:40}),o.push(s),s}catch{}}function r(e){o.forEach((t,n)=>{if(!t||e&&t.kind!=e)return;t.takeRecords&&p(t,t.takeRecords()),t.disconnect(),o[n]=0})}function b(t){if(o&&r(),o=[],e={},l=u=d=i=0,h=!1,!t){c("largest-contentful-paint",1,!0),c("paint",4,!0);try{var n=performance.getEntriesByType("navigation")[0],s=n&&n.responseStart-n.startTime;s>=0&&(e.ttfb=""+Math.round(s))}catch{}}c("layout-shift",2,!t),c("event",3,!t)}function m(){if(h)return;h=!0,r(),(e.lcp||e.cls||e.inp)&&n("Web Vitals",e)}document.addEventListener("pointerdown",()=>r(1),!0),document.addEventListener("keydown",()=>r(1),!0),document.addEventListener("visibilitychange",()=>document.hidden&&m()),addEventListener("pagehide",m),s._nav.push(()=>{m(),b(1)}),b()}if(t.heatmap=="true"){let e=[],n=location.href;s._clearHeatmap=()=>{e=[]};const o=document.documentElement;function a(e,t){return Math.max(0,Math.min(t,Math.round(e)))}function _(t){if(!s._allowed()){e=[];return}const n=o.scrollWidth||1;e.length<200&&e.push([a(t.pageX*1e3/n,1e3),a(t.pageY,1e5)])}function y(){if(!s._allowed()){e=[];return}if(!e.length)return;const i={site:t.site,name:"heatmap",url:n,screen_width:screen.width,clicks:e,doc_h:a(o.scrollHeight,1e5),vw:a(innerWidth,1e4)};e=[],s._post(i)}document.addEventListener("click",_),addEventListener("pagehide",y),s._nav.push(()=>{y(),n=location.href})}})(),(()=>{var n,o=document.currentScript,t=o.dataset,e=window.supermetry;if(!e||!t.replay)return;n=e._privacyState??(t.replay=="never"||t.replay=="auto"&&t.consent!="required");function s(){var s,o,i,a,r,c,l=navigator.connection;if(!n||!e._allowed()||e._replay||l?.saveData||/^(slow-)?2g$/.test(l?.effectiveType))return;s=new URL(t.api||"/api/public/v1/events",location.href),i=s.pathname.match(/^(.*)\/api\/public\/v1\/events\/?$/),a=s.origin+(i?i[1]:""),r=s.pathname.replace(/\/$/,""),o=r.endsWith("/e/"+t.site)?s.origin+r:"",e._replay={s:t.site,o:a,p:o},c=document.createElement("script"),c.src=o?o+"/r.js":a+"/replay.js",document.head.appendChild(c)}e.replay={trigger:t=>e._replayTrigger=t||"manual"},e._privacy=t=>{n=t,e._replayConsent=t,e._replayDenied=!t,t?s():e._replayStop?.()},n&&s()})() \ No newline at end of file +(()=>{var e,t,s,o,i,a,r,c,l,u,m,f,p,g,v,w,C=document.currentScript,n=C.dataset;if(!n.site||navigator.webdriver||!C.hasAttribute("data-allow-localhost")&&/(^|\.)localhost$|^127\.|^\[::1\]$/.test(location.hostname))return;o=n.manual=="true",c=!o,g=location.href,w=n.api||"/api/public/v1/events";try{f=localStorage.supermetry_ignore}catch{}m=location.href,l=0,s=o?0:document.hidden?0:Date.now(),r=0,a=!o&&!0;function b(){return!f&&!navigator.globalPrivacyControl&&navigator.doNotTrack!="1"&&window.doNotTrack!="1"&&!u&&!t}function y(e){if(!b())return;e=JSON.stringify(e),fetch(w,{method:"POST",body:e,keepalive:!0,mode:"no-cors"}).catch(()=>0)}function k(e){return n.hash=="true"?e:e.split("#")[0]}function A(t){var s,o=e.beforeSend||window[n.beforeSend];if(typeof o!="function")return t;try{s=o({name:t.name,url:t.url,referrer:t.referrer,props:t.props,engagement_ms:t.engagement_ms,scroll_pct:t.scroll_pct})}catch{return}if(!s||typeof s!="object")return;return["name","url","referrer","props","engagement_ms","scroll_pct"].forEach(e=>{e in s&&(t[e]=s[e])}),t}v=n.consent=="required",t=v?[]:0;function _(e){if(u)return;if(t){if(t.length==20){var n=t.findIndex(e=>e.name!="pageview");t.splice(n<0?0:n,1)}t.push(e);return}e=A(e),e&&y(e)}function d(e,t,s,o,a,r){var c={v:1,ts:Date.now(),id:(crypto.randomUUID?.()||Math.random().toString(16)).replace(/\W/g,""),site:n.site,name:e,url:m,referrer:document.referrer,screen_width:screen.width,props:t||{},engagement_ms:s||0,scroll_pct:o||0,traits:r};a&&(c.revenue=a),i&&(c.profile_id=i),_(c);return;y(c)}function h(){if(!c)return;s&&(l+=Date.now()-s,s=0),O(),a&&(d("engagement",{},Math.round(l),r),l=r=0,a=!1)}function O(){var e=Math.max(document.documentElement.scrollHeight,document.body?.scrollHeight||0);e&&(r=Math.max(r,Math.min(100,Math.round(100*(scrollY+innerHeight)/e))))}function x(e,t){c=!0,g=location.href,m=e||location.href,l=r=0,s=document.hidden?0:Date.now(),a=!0,d("pageview",t)}function j(){if(o||k(location.href)==k(g))return;e._nav.forEach(e=>e()),h(),x()}function E(e){var t=history[e];history[e]=(...e)=>{t.apply(history,e),j()}}E("pushState"),addEventListener("popstate",j),n.hash=="true"&&addEventListener("hashchange",j),E("replaceState"),addEventListener("scroll",()=>{if(!c)return;O(),a=!0},{passive:!0}),document.addEventListener("visibilitychange",()=>c?document.hidden?h():(s=Date.now(),a=!0):0),addEventListener("pagehide",h),e=window.supermetry=window.supermetry||{},e._nav=[],e._post=y,e._allowed=b,p=n=>()=>{f=n,n&&(i=0,t&&(t=[]),e._clearHeatmap?.());try{n?localStorage.supermetry_ignore=1:delete localStorage.supermetry_ignore}catch{}e._privacyState=!n,e._privacy?.(!n)},e.optOut=p(1),e.optIn=p(0),e.pageview=e=>{var t=e?.url;try{t=t?new URL(t,location.href).href:location.href}catch{return}h(),x(t,e?.props)},e.consent=n=>{if(u=!n,n||(i=0,e._clearHeatmap?.()),e._privacyState=!!n,e._privacy?.(!!n),!v)return;var s=t;u=!n,t=n?0:[],n&&s&&s.forEach(_)},e.track=(e,t,n)=>d(e,t,0,0,n?.revenue),e.identify=(e,t={})=>{if(n.profiles=="false"||!b())return;if(e===null)return i=0;if(e!==String(e)||!e||e.length>128||!t||typeof t!="object"||Array.isArray(t))return;var s,o=0;for(s in t)if(++o>20||!s.length||s.length>64||typeof t[s]!="string"||t[s].length>256)return;i=e,d("identify",0,0,0,0,t)},o||d("pageview")})(),(()=>{var e,o,i,l,d,u,h,f=document.currentScript,t=f.dataset,s=window.supermetry,n=s?.track;if(!n)return;function j(e){if(e.button>1)return;var o,i,s=e.target.closest?.("a");if(!s)return;o={url:(s.origin+s.pathname+s.hash).slice(0,500)},t.outbound!="false"&&s.protocol.indexOf("http")==0&&s.host!=location.host&&n("Outbound Link: Click",o),i=(t.downloadExt||"pdf,xlsx,docx,csv,txt,zip,rar,7z,gz,dmg,exe,msi,pkg,apk,mp3,mp4,mov,avi,wav,key,pptx,ppt,xls,doc,epub,iso").toLowerCase().split(","),t.downloads!="false"&&i.indexOf(s.pathname.toLowerCase().split(".").pop())>=0&&n("File Download",o)}function v(e){if(t.clicks=="false")return;for(s=e.target,r=0;s&&r<4;r++,s=s.parentElement){if(c=s.getAttribute?.("data-supermetry-event"),c){for(var s,o,i,a,r,c,u,l={},d=0,h=0,m=0;d700)break;l[i]=u,h++}n(c.slice(0,75),l);return}if(a=g(s),a){n(a.name.slice(0,75),a.props);return}}}function g(e){var t,n,s,i,o={},a=0;for(i of e.classList||[]){if(i.indexOf("supermetry-event-")!=0)continue;n=i.slice(17).replace(/\+/g," "),t=n.indexOf("="),t<0?s=s||n:t&&t<=64&&n.length-t-1<=256&&++a<=20&&(o[n.slice(0,t)]=n.slice(t+1))}return s&&{name:s,props:o}}if(document.addEventListener("click",j),document.addEventListener("auxclick",j),document.addEventListener("click",v),document.addEventListener("submit",v),t.forms!="false"&&document.addEventListener("submit",e=>{var s,t=e.target;if(!t||t.tagName!="FORM"||t.getAttribute?.("data-supermetry-event")||g(t))return;s=(t.getAttribute("action")||"").split(/[?#]/)[0].slice(0,500),n("Form: Submit",{form_id:(t.getAttribute("id")||"").slice(0,100),form_name:(t.getAttribute("name")||"").slice(0,100),action:s})},!0),t[404]!="false"&&(f.hasAttribute("data-404-page")||document.querySelector('meta[name="supermetry:404"]'))&&n("404",{path:location.pathname.slice(0,500)}),t.vitals!="false"&&window.PerformanceObserver){function p(t,n){for(var s,o=0;o=1e3||s.startTime-u>=5e3)&&(u=s.startTime,i=0),i+=s.value,d=s.startTime,l=Math.max(l,i),e.cls=""+Math.round(l*1e3)}else t.kind==4?s.name=="first-contentful-paint"&&(e.fcp=""+Math.round(s.startTime)):s.interactionId&&(e.inp=""+Math.max(e.inp||0,Math.round(s.duration)))}function c(e,t,n){try{var s=new PerformanceObserver(e=>p(s,e.getEntries()));return s.kind=t,s.observe({type:e,buffered:n,durationThreshold:40}),o.push(s),s}catch{}}function r(e){o.forEach((t,n)=>{if(!t||e&&t.kind!=e)return;t.takeRecords&&p(t,t.takeRecords()),t.disconnect(),o[n]=0})}function b(t){if(o&&r(),o=[],e={},l=u=d=i=0,h=!1,!t){c("largest-contentful-paint",1,!0),c("paint",4,!0);try{var n=performance.getEntriesByType("navigation")[0],s=n&&n.responseStart-n.startTime;s>=0&&(e.ttfb=""+Math.round(s))}catch{}}c("layout-shift",2,!t),c("event",3,!t)}function m(){if(h)return;h=!0,r(),(e.lcp||e.cls||e.inp)&&n("Web Vitals",e)}document.addEventListener("pointerdown",()=>r(1),!0),document.addEventListener("keydown",()=>r(1),!0),document.addEventListener("visibilitychange",()=>document.hidden&&m()),addEventListener("pagehide",m),s._nav.push(()=>{m(),b(1)}),b()}if(t.heatmap=="true"){let e=[],n=location.href;s._clearHeatmap=()=>{e=[]};const o=document.documentElement;function a(e,t){return Math.max(0,Math.min(t,Math.round(e)))}function _(t){if(!s._allowed()){e=[];return}const n=o.scrollWidth||1;e.length<200&&e.push([a(t.pageX*1e3/n,1e3),a(t.pageY,1e5)])}function y(){if(!s._allowed()){e=[];return}if(!e.length)return;const i={site:t.site,name:"heatmap",url:n,screen_width:screen.width,clicks:e,doc_h:a(o.scrollHeight,1e5),vw:a(innerWidth,1e4)};e=[],s._post(i)}document.addEventListener("click",_),addEventListener("pagehide",y),s._nav.push(()=>{y(),n=location.href})}})(),(()=>{var n,o=document.currentScript,t=o.dataset,e=window.supermetry;if(!e||!t.replay)return;n=e._privacyState??(t.replay=="never"||t.replay=="auto"&&t.consent!="required");function s(){var s,o,i,a,r,c,l=navigator.connection;if(!n||!e._allowed()||e._replay||l?.saveData||/^(slow-)?2g$/.test(l?.effectiveType))return;s=new URL(t.api||"/api/public/v1/events",location.href),i=s.pathname.match(/^(.*)\/api\/public\/v1\/events\/?$/),a=s.origin+(i?i[1]:""),r=s.pathname.replace(/\/$/,""),o=r.endsWith("/e/"+t.site)?s.origin+r:"",e._replay={s:t.site,o:a,p:o},c=document.createElement("script"),c.src=o?o+"/r.js":a+"/replay.js",document.head.appendChild(c)}e.replay={trigger:t=>e._replayTrigger=t||"manual"},e._privacy=t=>{n=t,e._replayConsent=t,e._replayDenied=!t,t?s():e._replayStop?.()},n&&s()})() \ No newline at end of file diff --git a/internal/tracker/tracker.js b/internal/tracker/tracker.js index d27fbccc..e03f201b 100644 --- a/internal/tracker/tracker.js +++ b/internal/tracker/tracker.js @@ -41,11 +41,13 @@ payload = JSON.stringify(payload); // fetch(keepalive) survives page exit like sendBeacon, but blockers file // it as a regular request instead of a "ping", which lists block - // wholesale. A string body keeps it a simple request: no preflight. + // wholesale. no-cors with a string body keeps it a simple request (no + // preflight) that follows redirects without CORS headers, as beacons do. fetch(endpoint, { method: "POST", body: payload, - keepalive: true + keepalive: true, + mode: "no-cors" }).catch(() => 0); } diff --git a/internal/tracker/tracker.min.js b/internal/tracker/tracker.min.js index 427c7a0c..566fbe7e 100644 --- a/internal/tracker/tracker.min.js +++ b/internal/tracker/tracker.min.js @@ -1 +1 @@ -(()=>{var t,n,s,a,r,c,l,d,u,h,m,y,i=document.currentScript,e=i.dataset;if(!e.site||navigator.webdriver||!i.hasAttribute("data-allow-localhost")&&/(^|\.)localhost$|^127\.|^\[::1\]$/.test(location.hostname))return;if(e.consent=="required"||e.manual=="true"||e.beforeSend){try{h=i.cloneNode(),h.src=i.src+(i.src.includes("?")?"&":"?")+"full=1",document.head.appendChild(h)}catch{}return}u=location.href,y=e.api||"/api/public/v1/events";try{m=localStorage.supermetry_ignore}catch{}l=location.href,a=0,t=document.hidden?0:Date.now(),s=0,n=!0;function j(){return!m&&!navigator.globalPrivacyControl&&navigator.doNotTrack!="1"&&window.doNotTrack!="1"}function _(e){if(!j())return;e=JSON.stringify(e),fetch(y,{method:"POST",body:e,keepalive:!0}).catch(()=>0)}function g(t){return e.hash=="true"?t:t.split("#")[0]}function o(t,n,s,o,i,a){var r={v:1,ts:Date.now(),id:(crypto.randomUUID?.()||Math.random().toString(16)).replace(/\W/g,""),site:e.site,name:t,url:l,referrer:document.referrer,screen_width:screen.width,props:n||{},engagement_ms:s||0,scroll_pct:o||0,traits:a};i&&(r.revenue=i),c&&(r.profile_id=c),_(r)}function f(){t&&(a+=Date.now()-t,t=0),p(),n&&(o("engagement",{},Math.round(a),s),a=s=0,n=!1)}function p(){var e=Math.max(document.documentElement.scrollHeight,document.body?.scrollHeight||0);e&&(s=Math.max(s,Math.min(100,Math.round(100*(scrollY+innerHeight)/e))))}function w(e,i){u=location.href,l=e||location.href,a=s=0,t=document.hidden?0:Date.now(),n=!0,o("pageview",i)}function v(){if(g(location.href)==g(u))return;f(),w()}function b(e){var t=history[e];history[e]=(...e)=>{t.apply(history,e),v()}}b("pushState"),addEventListener("popstate",v),b("replaceState"),addEventListener("scroll",()=>{p(),n=!0},{passive:!0}),document.addEventListener("visibilitychange",()=>document.hidden?f():(t=Date.now(),n=!0)),addEventListener("pagehide",f),r=window.supermetry=window.supermetry||{},d=e=>()=>{m=e;try{e?localStorage.supermetry_ignore=1:delete localStorage.supermetry_ignore}catch{}},r.optOut=d(1),r.optIn=d(0),r.track=(e,t,n)=>o(e,t,0,0,n?.revenue),r.identify=(t,n={})=>{if(e.profiles=="false"||!j())return;if(t===null)return c=0;if(t!==String(t)||!t||t.length>128||!n||typeof n!="object"||Array.isArray(n))return;var s,i=0;for(s in n)if(++i>20||!s.length||s.length>64||typeof n[s]!="string"||n[s].length>256)return;c=t,o("identify",0,0,0,0,n)},o("pageview")})() \ No newline at end of file +(()=>{var t,n,s,a,r,c,l,d,u,h,m,y,i=document.currentScript,e=i.dataset;if(!e.site||navigator.webdriver||!i.hasAttribute("data-allow-localhost")&&/(^|\.)localhost$|^127\.|^\[::1\]$/.test(location.hostname))return;if(e.consent=="required"||e.manual=="true"||e.beforeSend){try{h=i.cloneNode(),h.src=i.src+(i.src.includes("?")?"&":"?")+"full=1",document.head.appendChild(h)}catch{}return}u=location.href,y=e.api||"/api/public/v1/events";try{m=localStorage.supermetry_ignore}catch{}l=location.href,a=0,t=document.hidden?0:Date.now(),s=0,n=!0;function j(){return!m&&!navigator.globalPrivacyControl&&navigator.doNotTrack!="1"&&window.doNotTrack!="1"}function _(e){if(!j())return;e=JSON.stringify(e),fetch(y,{method:"POST",body:e,keepalive:!0,mode:"no-cors"}).catch(()=>0)}function g(t){return e.hash=="true"?t:t.split("#")[0]}function o(t,n,s,o,i,a){var r={v:1,ts:Date.now(),id:(crypto.randomUUID?.()||Math.random().toString(16)).replace(/\W/g,""),site:e.site,name:t,url:l,referrer:document.referrer,screen_width:screen.width,props:n||{},engagement_ms:s||0,scroll_pct:o||0,traits:a};i&&(r.revenue=i),c&&(r.profile_id=c),_(r)}function f(){t&&(a+=Date.now()-t,t=0),p(),n&&(o("engagement",{},Math.round(a),s),a=s=0,n=!1)}function p(){var e=Math.max(document.documentElement.scrollHeight,document.body?.scrollHeight||0);e&&(s=Math.max(s,Math.min(100,Math.round(100*(scrollY+innerHeight)/e))))}function w(e,i){u=location.href,l=e||location.href,a=s=0,t=document.hidden?0:Date.now(),n=!0,o("pageview",i)}function v(){if(g(location.href)==g(u))return;f(),w()}function b(e){var t=history[e];history[e]=(...e)=>{t.apply(history,e),v()}}b("pushState"),addEventListener("popstate",v),b("replaceState"),addEventListener("scroll",()=>{p(),n=!0},{passive:!0}),document.addEventListener("visibilitychange",()=>document.hidden?f():(t=Date.now(),n=!0)),addEventListener("pagehide",f),r=window.supermetry=window.supermetry||{},d=e=>()=>{m=e;try{e?localStorage.supermetry_ignore=1:delete localStorage.supermetry_ignore}catch{}},r.optOut=d(1),r.optIn=d(0),r.track=(e,t,n)=>o(e,t,0,0,n?.revenue),r.identify=(t,n={})=>{if(e.profiles=="false"||!j())return;if(t===null)return c=0;if(t!==String(t)||!t||t.length>128||!n||typeof n!="object"||Array.isArray(n))return;var s,i=0;for(s in n)if(++i>20||!s.length||s.length>64||typeof n[s]!="string"||n[s].length>256)return;c=t,o("identify",0,0,0,0,n)},o("pageview")})() \ No newline at end of file diff --git a/internal/tracker/tracker.runtime.min.js b/internal/tracker/tracker.runtime.min.js index 33ecbab2..855c07d3 100644 --- a/internal/tracker/tracker.runtime.min.js +++ b/internal/tracker/tracker.runtime.min.js @@ -1 +1 @@ -(()=>{var e,t,s,o,i,a,r,c,l,u,m,f,p,g,v,w,C=document.currentScript,n=C.dataset;if(!n.site||navigator.webdriver||!C.hasAttribute("data-allow-localhost")&&/(^|\.)localhost$|^127\.|^\[::1\]$/.test(location.hostname))return;o=n.manual=="true",c=!o,g=location.href,w=n.api||"/api/public/v1/events";try{f=localStorage.supermetry_ignore}catch{}m=location.href,l=0,s=o?0:document.hidden?0:Date.now(),r=0,a=!o&&!0;function b(){return!f&&!navigator.globalPrivacyControl&&navigator.doNotTrack!="1"&&window.doNotTrack!="1"&&!u&&!t}function y(e){if(!b())return;e=JSON.stringify(e),fetch(w,{method:"POST",body:e,keepalive:!0}).catch(()=>0)}function k(e){return n.hash=="true"?e:e.split("#")[0]}function A(t){var s,o=e.beforeSend||window[n.beforeSend];if(typeof o!="function")return t;try{s=o({name:t.name,url:t.url,referrer:t.referrer,props:t.props,engagement_ms:t.engagement_ms,scroll_pct:t.scroll_pct})}catch{return}if(!s||typeof s!="object")return;return["name","url","referrer","props","engagement_ms","scroll_pct"].forEach(e=>{e in s&&(t[e]=s[e])}),t}v=n.consent=="required",t=v?[]:0;function _(e){if(u)return;if(t){if(t.length==20){var n=t.findIndex(e=>e.name!="pageview");t.splice(n<0?0:n,1)}t.push(e);return}e=A(e),e&&y(e)}function d(e,t,s,o,a,r){var c={v:1,ts:Date.now(),id:(crypto.randomUUID?.()||Math.random().toString(16)).replace(/\W/g,""),site:n.site,name:e,url:m,referrer:document.referrer,screen_width:screen.width,props:t||{},engagement_ms:s||0,scroll_pct:o||0,traits:r};a&&(c.revenue=a),i&&(c.profile_id=i),_(c);return;y(c)}function h(){if(!c)return;s&&(l+=Date.now()-s,s=0),O(),a&&(d("engagement",{},Math.round(l),r),l=r=0,a=!1)}function O(){var e=Math.max(document.documentElement.scrollHeight,document.body?.scrollHeight||0);e&&(r=Math.max(r,Math.min(100,Math.round(100*(scrollY+innerHeight)/e))))}function x(e,t){c=!0,g=location.href,m=e||location.href,l=r=0,s=document.hidden?0:Date.now(),a=!0,d("pageview",t)}function j(){if(o||k(location.href)==k(g))return;e._nav.forEach(e=>e()),h(),x()}function E(e){var t=history[e];history[e]=(...e)=>{t.apply(history,e),j()}}E("pushState"),addEventListener("popstate",j),n.hash=="true"&&addEventListener("hashchange",j),E("replaceState"),addEventListener("scroll",()=>{if(!c)return;O(),a=!0},{passive:!0}),document.addEventListener("visibilitychange",()=>c?document.hidden?h():(s=Date.now(),a=!0):0),addEventListener("pagehide",h),e=window.supermetry=window.supermetry||{},e._nav=[],e._post=y,e._allowed=b,p=n=>()=>{f=n,n&&(i=0,t&&(t=[]),e._clearHeatmap?.());try{n?localStorage.supermetry_ignore=1:delete localStorage.supermetry_ignore}catch{}e._privacyState=!n,e._privacy?.(!n)},e.optOut=p(1),e.optIn=p(0),e.pageview=e=>{var t=e?.url;try{t=t?new URL(t,location.href).href:location.href}catch{return}h(),x(t,e?.props)},e.consent=n=>{if(u=!n,n||(i=0,e._clearHeatmap?.()),e._privacyState=!!n,e._privacy?.(!!n),!v)return;var s=t;u=!n,t=n?0:[],n&&s&&s.forEach(_)},e.track=(e,t,n)=>d(e,t,0,0,n?.revenue),e.identify=(e,t={})=>{if(n.profiles=="false"||!b())return;if(e===null)return i=0;if(e!==String(e)||!e||e.length>128||!t||typeof t!="object"||Array.isArray(t))return;var s,o=0;for(s in t)if(++o>20||!s.length||s.length>64||typeof t[s]!="string"||t[s].length>256)return;i=e,d("identify",0,0,0,0,t)},o||d("pageview")})() \ No newline at end of file +(()=>{var e,t,s,o,i,a,r,c,l,u,m,f,p,g,v,w,C=document.currentScript,n=C.dataset;if(!n.site||navigator.webdriver||!C.hasAttribute("data-allow-localhost")&&/(^|\.)localhost$|^127\.|^\[::1\]$/.test(location.hostname))return;o=n.manual=="true",c=!o,g=location.href,w=n.api||"/api/public/v1/events";try{f=localStorage.supermetry_ignore}catch{}m=location.href,l=0,s=o?0:document.hidden?0:Date.now(),r=0,a=!o&&!0;function b(){return!f&&!navigator.globalPrivacyControl&&navigator.doNotTrack!="1"&&window.doNotTrack!="1"&&!u&&!t}function y(e){if(!b())return;e=JSON.stringify(e),fetch(w,{method:"POST",body:e,keepalive:!0,mode:"no-cors"}).catch(()=>0)}function k(e){return n.hash=="true"?e:e.split("#")[0]}function A(t){var s,o=e.beforeSend||window[n.beforeSend];if(typeof o!="function")return t;try{s=o({name:t.name,url:t.url,referrer:t.referrer,props:t.props,engagement_ms:t.engagement_ms,scroll_pct:t.scroll_pct})}catch{return}if(!s||typeof s!="object")return;return["name","url","referrer","props","engagement_ms","scroll_pct"].forEach(e=>{e in s&&(t[e]=s[e])}),t}v=n.consent=="required",t=v?[]:0;function _(e){if(u)return;if(t){if(t.length==20){var n=t.findIndex(e=>e.name!="pageview");t.splice(n<0?0:n,1)}t.push(e);return}e=A(e),e&&y(e)}function d(e,t,s,o,a,r){var c={v:1,ts:Date.now(),id:(crypto.randomUUID?.()||Math.random().toString(16)).replace(/\W/g,""),site:n.site,name:e,url:m,referrer:document.referrer,screen_width:screen.width,props:t||{},engagement_ms:s||0,scroll_pct:o||0,traits:r};a&&(c.revenue=a),i&&(c.profile_id=i),_(c);return;y(c)}function h(){if(!c)return;s&&(l+=Date.now()-s,s=0),O(),a&&(d("engagement",{},Math.round(l),r),l=r=0,a=!1)}function O(){var e=Math.max(document.documentElement.scrollHeight,document.body?.scrollHeight||0);e&&(r=Math.max(r,Math.min(100,Math.round(100*(scrollY+innerHeight)/e))))}function x(e,t){c=!0,g=location.href,m=e||location.href,l=r=0,s=document.hidden?0:Date.now(),a=!0,d("pageview",t)}function j(){if(o||k(location.href)==k(g))return;e._nav.forEach(e=>e()),h(),x()}function E(e){var t=history[e];history[e]=(...e)=>{t.apply(history,e),j()}}E("pushState"),addEventListener("popstate",j),n.hash=="true"&&addEventListener("hashchange",j),E("replaceState"),addEventListener("scroll",()=>{if(!c)return;O(),a=!0},{passive:!0}),document.addEventListener("visibilitychange",()=>c?document.hidden?h():(s=Date.now(),a=!0):0),addEventListener("pagehide",h),e=window.supermetry=window.supermetry||{},e._nav=[],e._post=y,e._allowed=b,p=n=>()=>{f=n,n&&(i=0,t&&(t=[]),e._clearHeatmap?.());try{n?localStorage.supermetry_ignore=1:delete localStorage.supermetry_ignore}catch{}e._privacyState=!n,e._privacy?.(!n)},e.optOut=p(1),e.optIn=p(0),e.pageview=e=>{var t=e?.url;try{t=t?new URL(t,location.href).href:location.href}catch{return}h(),x(t,e?.props)},e.consent=n=>{if(u=!n,n||(i=0,e._clearHeatmap?.()),e._privacyState=!!n,e._privacy?.(!!n),!v)return;var s=t;u=!n,t=n?0:[],n&&s&&s.forEach(_)},e.track=(e,t,n)=>d(e,t,0,0,n?.revenue),e.identify=(e,t={})=>{if(n.profiles=="false"||!b())return;if(e===null)return i=0;if(e!==String(e)||!e||e.length>128||!t||typeof t!="object"||Array.isArray(t))return;var s,o=0;for(s in t)if(++o>20||!s.length||s.length>64||typeof t[s]!="string"||t[s].length>256)return;i=e,d("identify",0,0,0,0,t)},o||d("pageview")})() \ No newline at end of file diff --git a/internal/tracker/tracker_runtime_test.js b/internal/tracker/tracker_runtime_test.js index 16082bf8..9713f3be 100644 --- a/internal/tracker/tracker_runtime_test.js +++ b/internal/tracker/tracker_runtime_test.js @@ -154,7 +154,7 @@ function loadTracker({full = false, dataset = {}, withExtensions = false, prelud // Events go out with fetch(keepalive), never sendBeacon: blockers file // beacons under the "ping" type, which filter lists block wholesale. fetch(_endpoint, options) { - if (!options || options.method !== "POST" || options.keepalive !== true || typeof options.body !== "string") { + if (!options || options.method !== "POST" || options.keepalive !== true || options.mode !== "no-cors" || typeof options.body !== "string") { throw new Error(`unexpected fetch options ${JSON.stringify(options)}`); } payloads.push(JSON.parse(options.body)); diff --git a/web/src/lib/tracker-full.test.ts b/web/src/lib/tracker-full.test.ts index 3dfead70..4cb77c0a 100644 --- a/web/src/lib/tracker-full.test.ts +++ b/web/src/lib/tracker-full.test.ts @@ -47,8 +47,8 @@ function load(attributes = '', body = '', withObservers = false, navigationRespo }) Object.defineProperty(dom.window, 'fetch', { configurable: true, - value: (_url: string, init: { method?: string; body: string; keepalive?: boolean }) => { - if (init.method !== 'POST' || init.keepalive !== true) throw new Error(`unexpected fetch ${JSON.stringify(init)}`) + value: (_url: string, init: { method?: string; body: string; keepalive?: boolean; mode?: string }) => { + if (init.method !== 'POST' || init.keepalive !== true || init.mode !== 'no-cors') throw new Error(`unexpected fetch ${JSON.stringify(init)}`) payloads.push(JSON.parse(init.body) as EventPayload) return Promise.resolve({}) }, From ad45aa161fff677142f8ff0732ae983de2f1e901 Mon Sep 17 00:00:00 2001 From: velixio <270455167+velixio@users.noreply.github.com> Date: Tue, 3 Nov 2026 05:45:50 +0530 Subject: [PATCH 2/3] settings: derive proxy recipe paths from the script directory With a custom SUPERMETRY_TRACKER_PATH, per-site scripts are not under /js, so the first-party snippet and the forwarded paths use the directory from the site's real script URL. Self-hosters are told to forward a changed SUPERMETRY_EVENT_PATH. --- .../features/settings/installRecipes.test.ts | 20 ++++++++++++++++ web/src/features/settings/installRecipes.ts | 24 ++++++++++++++----- 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/web/src/features/settings/installRecipes.test.ts b/web/src/features/settings/installRecipes.test.ts index 88f0cec3..cec51b7e 100644 --- a/web/src/features/settings/installRecipes.test.ts +++ b/web/src/features/settings/installRecipes.test.ts @@ -151,3 +151,23 @@ describe('first-party proxy recipes', () => { expect(firstPartySnippet(cloud)).toBe('') }) }) + +describe('first-party proxy recipes with a custom tracker path', () => { + const custom = { publicId: 'abc123def456', scriptUrl: 'https://stats.example.org/a/abc123def456.js' } + + it('uses the configured script directory in the snippet and every proxy', () => { + expect(firstPartySnippet(custom)).toBe('') + const recipes = buildProxyRecipes(custom, { managed: false }) + const code = (id: string) => recipes.find((recipe) => recipe.id === id)?.code ?? '' + expect(code('proxy-nginx')).toContain('^/sm/(a/|px/|api/public/|') + expect(code('proxy-caddy')).toContain('/sm/a/*') + expect(code('proxy-cloudflare')).toContain('"/sm/a/"') + for (const recipe of recipes) expect(recipe.code, recipe.id).not.toContain('/sm/js/') + }) + + it('tells self-hosters to forward a changed event path', () => { + for (const recipe of buildProxyRecipes(custom, { managed: false })) { + expect(recipe.note ?? '', recipe.id).toContain('SUPERMETRY_EVENT_PATH') + } + }) +}) diff --git a/web/src/features/settings/installRecipes.ts b/web/src/features/settings/installRecipes.ts index dcaef4f5..20a1865e 100644 --- a/web/src/features/settings/installRecipes.ts +++ b/web/src/features/settings/installRecipes.ts @@ -318,17 +318,29 @@ When done, tell me which files you changed.` export const PROXY_PREFIX = '/sm' /** The snippet to use once the proxy is live: the script from the site's own domain. */ -export function firstPartySnippet({ publicId }: RecipeInput): string { - return `` +/** + * The directory per-site scripts are served from, taken from the real script + * URL: "/js" by default, or the directory of SUPERMETRY_TRACKER_PATH. + */ +function scriptDir(scriptUrl: string): string { + const pathname = new URL(scriptUrl).pathname + const dir = pathname.slice(0, pathname.lastIndexOf('/')) + return dir || '/js' } -const trustedProxiesNote = 'Self-hosted: add this server\'s address to SUPERMETRY_TRUSTED_PROXIES on your Supermetry server, so it reads the visitor address from X-Forwarded-For instead of counting every visitor as your proxy.' +export function firstPartySnippet({ publicId, scriptUrl }: RecipeInput): string { + return `` +} + +const trustedProxiesNote = 'Self-hosted: add this server\'s address to SUPERMETRY_TRUSTED_PROXIES on your Supermetry server, so it reads the visitor address from X-Forwarded-For instead of counting every visitor as your proxy. If you changed SUPERMETRY_EVENT_PATH, forward that path under /sm too.' const proxyKeyNote = 'Keep the proxy key on the server only (config file, environment or secret store). Never put it in HTML or client code.' export function buildProxyRecipes(input: RecipeInput, { managed }: { managed: boolean }): Recipe[] { const host = trackerHost(input.scriptUrl) const hostname = new URL(host).host const p = PROXY_PREFIX + // Script directory without its leading slash, as a path segment ("js/"). + const scripts = `${scriptDir(input.scriptUrl).slice(1)}/` const note = managed ? proxyKeyNote : trustedProxiesNote const nginxKey = managed ? ` proxy_set_header X-Supermetry-Proxy-Key $sm_key; @@ -338,7 +350,7 @@ export function buildProxyRecipes(input: RecipeInput, { managed }: { managed: bo set $sm_key "smpk_REPLACE_WITH_YOUR_KEY"; ` : ''}# Forward only the tracking paths, never the whole app. -location ~ ^${p}/(js/|px/|api/public/|replay\\.js$|api/replay/) { +location ~ ^${p}/(${scripts}|px/|api/public/|replay\\.js$|api/replay/) { rewrite ^${p}/(.*)$ /$1 break; proxy_pass ${host}; proxy_ssl_server_name on; @@ -352,7 +364,7 @@ ${nginxKey} proxy_set_header X-Forwarded-For $remote_addr; header_up X-Supermetry-Client-IP {client_ip} ` : '' const caddy = `your-site.example { - @supermetry path ${p}/js/* ${p}/px/* ${p}/api/public/* ${p}/replay.js ${p}/api/replay/* + @supermetry path ${p}/${scripts}* ${p}/px/* ${p}/api/public/* ${p}/replay.js ${p}/api/replay/* handle @supermetry { uri strip_prefix ${p} reverse_proxy ${host} { @@ -372,7 +384,7 @@ if (visitor) { if (visitor) headers.set('X-Forwarded-For', visitor)` const worker = `// Route: your-site.example${p}/*${managed ? ` // Secret: wrangler secret put SUPERMETRY_PROXY_KEY` : ''} -const prefixes = [${['js/', 'px/', 'api/public/', 'api/replay/'].map((path) => q(`${p}/${path}`)).join(', ')}] +const prefixes = [${[scripts, 'px/', 'api/public/', 'api/replay/'].map((path) => q(`${p}/${path}`)).join(', ')}] const files = [${q(`${p}/replay.js`)}] export default { From f98e731f59ee66b41e49c9d5afcad5a4234322ec Mon Sep 17 00:00:00 2001 From: velixio <270455167+velixio@users.noreply.github.com> Date: Tue, 3 Nov 2026 05:45:50 +0530 Subject: [PATCH 3/3] docs(changelog): proxy recipe and no-cors fixes --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a396e60..a7654cf9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ Supermetry is a self-hosted web analytics product: one Go binary and PostgreSQL, ## Unreleased +### Fixed + +- The tracker sends events with `mode: "no-cors"`. Like the old beacons, it now follows a redirect that has no CORS headers (for example from a first-party proxy) instead of dropping the event. +- The ad blocker proxy recipes in Settings → Installation use the instance's script directory. With a custom `SUPERMETRY_TRACKER_PATH`, the first-party snippet and the forwarded paths now match. The self-hosted note also mentions forwarding a changed `SUPERMETRY_EVENT_PATH`. + ## v0.1.22 (2026-11-03) ### Changed