@webority/analytics 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- var WeborityAnalytics=(()=>{var y=Object.defineProperty;var Y=Object.getOwnPropertyDescriptor;var J=Object.getOwnPropertyNames;var X=Object.prototype.hasOwnProperty;var V=(t,e)=>{for(var n in e)y(t,n,{get:e[n],enumerable:!0})},Q=(t,e,n,o)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of J(e))!X.call(t,i)&&i!==n&&y(t,i,{get:()=>e[i],enumerable:!(o=Y(e,i))||o.enumerable});return t};var W=t=>Q(y({},"__esModule",{value:!0}),t);var yt={};V(yt,{CONSENT_CHANGE_EVENT:()=>_,CONSENT_COOKIE_NAME:()=>l,captureAttribution:()=>$,getAttribution:()=>j,getConsent:()=>s,initConsent:()=>A,loadTags:()=>B,onConsentChange:()=>c,requiresOptIn:()=>k,saveConsent:()=>R});var w=Object.freeze(["AT","BE","BG","HR","CY","CZ","DK","EE","FI","FR","DE","GR","HU","IE","IT","LV","LT","LU","MT","NL","PL","PT","RO","SK","SI","ES","SE","IS","LI","NO","GB","CH"]);function k(t){let e=typeof t=="string"?t.trim().toUpperCase():"";return e.length!==2||e==="XX"||e==="T1"?!0:w.includes(e)}var l="webority_cookie_consent",_="webority-consent-change",L=365,r={initialized:!1,requiresOptIn:!0,strictOptIn:!1,adStorage:!0,cookieDays:L,cookieDomain:null,saved:null},C=new Set;function A(t){let e=t?.strictOptIn??!1;if(typeof e!="boolean")throw new Error("@webority/analytics: initConsent strictOptIn must be true or false, or left out.");if(e){if(t.requiresOptIn!==void 0&&t.requiresOptIn!==!0)throw new Error("@webority/analytics: initConsent strictOptIn asks every visitor to opt in, so requiresOptIn must be true or left out.")}else if(typeof t?.requiresOptIn!="boolean")throw new Error("@webority/analytics: initConsent needs requiresOptIn (true or false), decided from the visitor's country.");if(t.cookieDomain!==void 0&&(typeof t.cookieDomain!="string"||!t.cookieDomain.trim()))throw new Error('@webority/analytics: initConsent cookieDomain must be a domain such as ".example.com", or left out.');if(t.cookieDays!==void 0&&!(Number.isInteger(t.cookieDays)&&t.cookieDays>0))throw new Error("@webority/analytics: initConsent cookieDays must be a whole number of days above zero, or left out.");if(t.adStorage!==void 0&&typeof t.adStorage!="boolean")throw new Error("@webority/analytics: initConsent adStorage must be true or false, or left out.");r.initialized=!0,r.strictOptIn=e,r.requiresOptIn=e||t.requiresOptIn,r.adStorage=t.adStorage??!e,r.cookieDays=t.cookieDays??L,r.cookieDomain=t.cookieDomain?.trim()??null;let{saved:n,entryCount:o}=et();r.saved=n,r.cookieDomain&&o>1&&n&&q(n),tt(),r.saved&&U(r.saved)}function f(){return r.initialized}function v(){return r.strictOptIn}function x(){return r.adStorage}function s(){z("getConsent");let t=r.saved;if(t)return{essential:!0,analytics:t.analytics,functional:t.functional,marketing:t.marketing,decided:!0};let e=!r.requiresOptIn;return{essential:!0,analytics:e,functional:e,marketing:e,decided:!1}}function R(t){z("saveConsent");let e={essential:!0,analytics:t?.analytics===!0,functional:t?.functional===!0,marketing:t?.marketing===!0,timestamp:new Date().toISOString()};q(e),r.saved=e,U(e),Z(s())}function c(t){return C.add(t),()=>C.delete(t)}function Z(t){for(let e of C)try{e(t)}catch(n){queueMicrotask(()=>{throw n})}typeof window?.dispatchEvent=="function"&&typeof CustomEvent=="function"&&window.dispatchEvent(new CustomEvent(_,{detail:t}))}function z(t){if(!r.initialized)throw new Error(`@webority/analytics: call initConsent before ${t}.`)}function d(){window.dataLayer=window.dataLayer||[],window.dataLayer.push(arguments)}function tt(){if(r.strictOptIn){d("consent","default",{ad_storage:"denied",ad_user_data:"denied",ad_personalization:"denied",analytics_storage:"denied",functionality_storage:"denied",personalization_storage:"denied",security_storage:"granted",wait_for_update:500});return}d("consent","default",{ad_storage:"denied",ad_user_data:"denied",ad_personalization:"denied",analytics_storage:"denied",functionality_storage:"denied",personalization_storage:"denied",security_storage:"granted",wait_for_update:500,region:[...w]}),d("consent","default",{ad_storage:"granted",ad_user_data:"granted",ad_personalization:"granted",analytics_storage:"granted",functionality_storage:"granted",personalization_storage:"granted",security_storage:"granted"})}function U(t){let e=o=>o?"granted":"denied",n=t.marketing&&r.adStorage;d("consent","update",{analytics_storage:e(t.analytics),functionality_storage:e(t.functional),personalization_storage:e(t.functional),ad_storage:e(n),ad_user_data:e(n),ad_personalization:e(n)})}function et(){let t=`${l}=`,e=(document.cookie||"").split(";").map(o=>o.trim()).filter(o=>o.startsWith(t)),n=e.map(o=>nt(o.slice(t.length))).filter(Boolean);return{saved:ot(n),entryCount:e.length}}function nt(t){try{let e=JSON.parse(decodeURIComponent(t));return!e||typeof e!="object"?null:{essential:!0,analytics:e.analytics===!0,functional:e.functional===!0,marketing:e.marketing===!0,timestamp:e.timestamp??null}}catch{return null}}function ot(t){if(t.length===0)return null;let e=t.map(o=>typeof o.timestamp=="string"?Date.parse(o.timestamp):Number.NaN);if(e.some(Number.isNaN))return t.at(-1);let n=0;for(let o=1;o<t.length;o++)e[o]>=e[n]&&(n=o);return t[n]}function q(t){let e=typeof t.timestamp=="string"?Date.parse(t.timestamp):Number.NaN,n=(Number.isNaN(e)?Date.now():e)+r.cookieDays*24*60*60*1e3,o=window.location?.protocol==="https:"?";Secure":"";r.cookieDomain&&D(`${l}=;expires=Thu, 01 Jan 1970 00:00:00 GMT;max-age=0;path=/;SameSite=Lax${o}`);let i=r.cookieDomain?`;domain=${r.cookieDomain}`:"";D(`${l}=${encodeURIComponent(JSON.stringify(t))};expires=${new Date(n).toUTCString()};path=/${i};SameSite=Lax${o}`)}function D(t){document.cookie=t}var p="webority_attribution",rt=2160*60*60*1e3,it=200,I=500,at=Object.freeze({utm_source:"utmSource",utm_medium:"utmMedium",utm_campaign:"utmCampaign",utm_content:"utmContent",utm_term:"utmTerm",gclid:"gclid",gbraid:"gbraid",wbraid:"wbraid",fbclid:"fbclid",msclkid:"msclkid",li_fat_id:"liFatId"}),F=!1;function $(){K("captureAttribution"),ut();let t=st();if(!t)return;let e=T(h());G(h(),{firstTouch:e?.firstTouch??t,lastTouch:t})}function j(){return K("getAttribution"),T(h())}function st(){let t=new URLSearchParams(window.location.search),e={};for(let[o,i]of Object.entries(at)){let a=m(t.get(o),it);a&&(e[i]=a)}let n=m(t.get("referrer"),I)||ct();return n&&(e.referrer=n),Object.keys(e).length===0?null:(e.landingPage=m(window.location.pathname,I),e.capturedDateTimeUtc=new Date().toISOString(),e)}function ct(){let t=document.referrer;if(!t)return"";try{return new URL(t).hostname===window.location.hostname?"":m(t,I)}catch{return""}}function ut(){F||(F=!0,c(t=>{let e=t.marketing?S():E(),n=t.marketing?E():S(),o=T(e);lt(e),o&&G(n,o)}))}function h(){return s().marketing?E():S()}function E(){try{return window.localStorage}catch{return null}}function S(){try{return window.sessionStorage}catch{return null}}function T(t){try{let e=t?.getItem(p);if(!e)return null;let n=JSON.parse(e),o=M(n?.firstTouch),i=M(n?.lastTouch);return!o&&!i?(t.removeItem(p),null):{firstTouch:o??i,lastTouch:i??o}}catch{return null}}function G(t,e){try{t?.setItem(p,JSON.stringify(e))}catch{}}function lt(t){try{t?.removeItem(p)}catch{}}function M(t){if(!t||typeof t!="object")return null;let e=Date.parse(t.capturedDateTimeUtc);return Number.isFinite(e)&&Date.now()-e<=rt?t:null}function m(t,e){let n=String(t??"").trim();return n?n.slice(0,e):""}function K(t){if(!f())throw new Error(`@webority/analytics: call initConsent before ${t}.`)}var dt=["scroll","mousemove","touchstart","keydown","pointerdown"],P=1500,ft=3e3,mt=100,u={loaded:!1,clarityId:null,clarityLoaded:!1};function B(t={}){if(!f())throw new Error("@webority/analytics: call initConsent before loadTags.");if(u.loaded)return;u.loaded=!0;let{gtmId:e,clarityId:n,ahrefsKey:o}=t,i=v();if(o){let a=()=>b("https://analytics.ahrefs.com/analytics.js",{"data-key":o});i?H(g=>g.analytics,a):a()}if(e){let a=()=>pt(()=>{window.dataLayer=window.dataLayer||[],window.dataLayer.push({"gtm.start":Date.now(),event:"gtm.js"}),b(`https://www.googletagmanager.com/gtm.js?id=${encodeURIComponent(e)}`)});if(i){let g=x();H(N=>N.analytics||N.marketing&&g,a)}else a()}n&&(u.clarityId=n,s().analytics&&gt(),c(a=>{a.analytics&&O()}))}function H(t,e){if(t(s())){e();return}let n=c(o=>{t(o)&&(n(),e())})}function pt(t){let e=!1,n=()=>{e||(e=!0,t())};for(let o of dt)window.addEventListener(o,n,{once:!0,passive:!0});document.readyState==="complete"?setTimeout(n,P):window.addEventListener("load",()=>setTimeout(n,P),{once:!0})}function gt(){let t=()=>{window.scrollY>mt&&(window.removeEventListener("scroll",t),O())};window.addEventListener("scroll",t,{passive:!0}),setTimeout(O,ft)}function O(){u.clarityLoaded||!u.clarityId||(u.clarityLoaded=!0,window.clarity=window.clarity||function(){window.clarity.q=window.clarity.q||[],window.clarity.q.push(arguments)},b(`https://www.clarity.ms/tag/${encodeURIComponent(u.clarityId)}`,{crossorigin:"anonymous"}))}function b(t,e={}){let n=document.createElement("script");n.async=!0,n.src=t;for(let[o,i]of Object.entries(e))n.setAttribute(o,i);document.head.appendChild(n)}return W(yt);})();
1
+ var WeborityAnalytics=(()=>{var w=Object.defineProperty;var at=Object.getOwnPropertyDescriptor;var st=Object.getOwnPropertyNames;var ct=Object.prototype.hasOwnProperty;var ut=(t,e)=>{for(var n in e)w(t,n,{get:e[n],enumerable:!0})},dt=(t,e,n,o)=>{if(e&&typeof e=="object"||typeof e=="function")for(let r of st(e))!ct.call(t,r)&&r!==n&&w(t,r,{get:()=>e[r],enumerable:!(o=at(e,r))||o.enumerable});return t};var lt=t=>dt(w({},"__esModule",{value:!0}),t);var Ft={};ut(Ft,{CONSENT_CHANGE_EVENT:()=>N,CONSENT_COOKIE_NAME:()=>_,captureAttribution:()=>Q,channels:()=>s,events:()=>a,getAttribution:()=>I,getConsent:()=>d,initConsent:()=>$,loadTags:()=>rt,onConsentChange:()=>f,requiresOptIn:()=>B,resolveAiSource:()=>S,resolveChannel:()=>p,saveConsent:()=>j,trackSiteEvent:()=>et});var ft=["google","googleads","adwords","youtube"],mt=["meta","facebook","fb","instagram","ig","messenger"],pt=["linkedin","lnkd"],gt=["bing","microsoft","microsoftads","msn"],M=Object.freeze([["chatgpt",["chatgpt.com","chat.openai.com"]],["perplexity",["perplexity.ai"]],["gemini",["gemini.google.com"]],["copilot",["copilot.microsoft.com"]],["claude",["claude.ai"]],["you",["you.com"]],["phind",["phind.com"]],["deepseek",["deepseek.com"]],["grok",["grok.com"]],["meta_ai",["meta.ai"]],["poe",["poe.com"]]]),_t=["bing.com","duckduckgo.com","search.yahoo.com","ecosia.org","search.brave.com"],Ct=["google","yandex"],St=["facebook.com","fb.com","instagram.com","linkedin.com","lnkd.in","twitter.com","t.co","x.com","reddit.com","pinterest.com","youtube.com","youtu.be","tiktok.com","whatsapp.com","threads.net","t.me","telegram.org"],s=Object.freeze({DIRECT:"direct",GOOGLE:"google",META:"meta",LINKEDIN:"linkedin",MICROSOFT:"microsoft",OTHER:"other",AI_ASSISTANT:"ai_assistant",ORGANIC_SEARCH:"organic_search",ORGANIC_SOCIAL:"organic_social",REFERRAL:"referral"});function p(t){if(!t)return s.DIRECT;let e=u(t.utmSource);if(e){let r=C(e);if(ft.includes(r))return s.GOOGLE;if(mt.includes(r))return s.META;if(pt.includes(r))return s.LINKEDIN;if(gt.includes(r))return s.MICROSOFT}if(u(t.fbclid))return s.META;if(u(t.gclid)||u(t.gbraid)||u(t.wbraid))return s.GOOGLE;if(u(t.liFatId))return s.LINKEDIN;if(u(t.msclkid))return s.MICROSOFT;if(P(e))return s.AI_ASSISTANT;if(e||u(t.utmMedium)||u(t.utmCampaign)||u(t.utmContent)||u(t.utmTerm))return s.OTHER;let o=F(t.referrer);return o?G(o)?s.AI_ASSISTANT:Et(o)?s.ORGANIC_SEARCH:St.some(r=>z(o,r))?s.ORGANIC_SOCIAL:s.REFERRAL:s.DIRECT}function S(t){if(!t)return"";let e=P(u(t.utmSource));if(e)return e;let n=F(t.referrer);return n?G(n):""}function P(t){if(!t)return"";let e=C(t);for(let[n,o]of M)if(C(n)===e||o.some(r=>C(r)===e))return n;return""}function G(t){for(let[e,n]of M)if(n.some(o=>z(t,o)))return e;return""}function Et(t){return _t.includes(t)?!0:Ct.some(e=>{if(!t.startsWith(`${e}.`))return!1;let n=t.slice(e.length+1).split(".");return n.length===1||n.length===2&&(n[0]==="co"||n[0]==="com")})}function z(t,e){return t===e||t.endsWith(`.${e}`)}function F(t){let e=u(t);if(!e)return"";try{let n=new URL(/^[a-z][a-z0-9+.-]*:\/\//i.test(e)?e:`https://${e}`).hostname.toLowerCase();return n.startsWith("www.")?n.slice(4):n}catch{return""}}function C(t){return String(t).toLowerCase().replace(/[^\p{L}\p{N}]/gu,"")}function u(t){return String(t??"").trim()}var O=Object.freeze(["AT","BE","BG","HR","CY","CZ","DK","EE","FI","FR","DE","GR","HU","IE","IT","LV","LT","LU","MT","NL","PL","PT","RO","SK","SI","ES","SE","IS","LI","NO","GB","CH"]);function B(t){let e=typeof t=="string"?t.trim().toUpperCase():"";return e.length!==2||e==="XX"||e==="T1"?!0:O.includes(e)}var _="webority_cookie_consent",N="webority-consent-change",K=365,i={initialized:!1,requiresOptIn:!0,strictOptIn:!1,adStorage:!0,cookieDays:K,cookieDomain:null,saved:null},A=new Set;function $(t){let e=t?.strictOptIn??!1;if(typeof e!="boolean")throw new Error("@webority/analytics: initConsent strictOptIn must be true or false, or left out.");if(e){if(t.requiresOptIn!==void 0&&t.requiresOptIn!==!0)throw new Error("@webority/analytics: initConsent strictOptIn asks every visitor to opt in, so requiresOptIn must be true or left out.")}else if(typeof t?.requiresOptIn!="boolean")throw new Error("@webority/analytics: initConsent needs requiresOptIn (true or false), decided from the visitor's country.");if(t.cookieDomain!==void 0&&(typeof t.cookieDomain!="string"||!t.cookieDomain.trim()))throw new Error('@webority/analytics: initConsent cookieDomain must be a domain such as ".example.com", or left out.');if(t.cookieDays!==void 0&&!(Number.isInteger(t.cookieDays)&&t.cookieDays>0))throw new Error("@webority/analytics: initConsent cookieDays must be a whole number of days above zero, or left out.");if(t.adStorage!==void 0&&typeof t.adStorage!="boolean")throw new Error("@webority/analytics: initConsent adStorage must be true or false, or left out.");i.initialized=!0,i.strictOptIn=e,i.requiresOptIn=e||t.requiresOptIn,i.adStorage=t.adStorage??!e,i.cookieDays=t.cookieDays??K,i.cookieDomain=t.cookieDomain?.trim()??null;let{saved:n,entryCount:o}=wt();i.saved=n,i.cookieDomain&&o>1&&n&&V(n),yt(),It(),i.saved&&W(i.saved)}function E(){return i.initialized}function q(){return i.strictOptIn}function h(){return i.adStorage}function d(){Y("getConsent");let t=i.saved;if(t)return{essential:!0,analytics:t.analytics,functional:t.functional,marketing:t.marketing,decided:!0};let e=!i.requiresOptIn;return{essential:!0,analytics:e,functional:e,marketing:e,decided:!1}}function j(t){Y("saveConsent");let e={essential:!0,analytics:t?.analytics===!0,functional:t?.functional===!0,marketing:t?.marketing===!0,timestamp:new Date().toISOString()};V(e),i.saved=e,W(e),Tt(d())}function f(t){return A.add(t),()=>A.delete(t)}function Tt(t){for(let e of A)try{e(t)}catch(n){queueMicrotask(()=>{throw n})}typeof window?.dispatchEvent=="function"&&typeof CustomEvent=="function"&&window.dispatchEvent(new CustomEvent(N,{detail:t}))}function Y(t){if(!i.initialized)throw new Error(`@webority/analytics: call initConsent before ${t}.`)}function g(){window.dataLayer=window.dataLayer||[],window.dataLayer.push(arguments)}function yt(){if(i.strictOptIn){g("consent","default",{ad_storage:"denied",ad_user_data:"denied",ad_personalization:"denied",analytics_storage:"denied",functionality_storage:"denied",personalization_storage:"denied",security_storage:"granted",wait_for_update:500});return}g("consent","default",{ad_storage:"denied",ad_user_data:"denied",ad_personalization:"denied",analytics_storage:"denied",functionality_storage:"denied",personalization_storage:"denied",security_storage:"granted",wait_for_update:500,region:[...O]}),g("consent","default",{ad_storage:"granted",ad_user_data:"granted",ad_personalization:"granted",analytics_storage:"granted",functionality_storage:"granted",personalization_storage:"granted",security_storage:"granted"})}function It(){g("set","ads_data_redaction",!0),g("set","url_passthrough",!0)}function W(t){let e=o=>o?"granted":"denied",n=t.marketing&&i.adStorage;g("consent","update",{analytics_storage:e(t.analytics),functionality_storage:e(t.functional),personalization_storage:e(t.functional),ad_storage:e(n),ad_user_data:e(n),ad_personalization:e(n)})}function wt(){let t=`${_}=`,e=(document.cookie||"").split(";").map(o=>o.trim()).filter(o=>o.startsWith(t)),n=e.map(o=>Ot(o.slice(t.length))).filter(Boolean);return{saved:At(n),entryCount:e.length}}function Ot(t){try{let e=JSON.parse(decodeURIComponent(t));return!e||typeof e!="object"?null:{essential:!0,analytics:e.analytics===!0,functional:e.functional===!0,marketing:e.marketing===!0,timestamp:e.timestamp??null}}catch{return null}}function At(t){if(t.length===0)return null;let e=t.map(o=>typeof o.timestamp=="string"?Date.parse(o.timestamp):Number.NaN);if(e.some(Number.isNaN))return t.at(-1);let n=0;for(let o=1;o<t.length;o++)e[o]>=e[n]&&(n=o);return t[n]}function V(t){let e=typeof t.timestamp=="string"?Date.parse(t.timestamp):Number.NaN,n=(Number.isNaN(e)?Date.now():e)+i.cookieDays*24*60*60*1e3,o=window.location?.protocol==="https:"?";Secure":"";i.cookieDomain&&H(`${_}=;expires=Thu, 01 Jan 1970 00:00:00 GMT;max-age=0;path=/;SameSite=Lax${o}`);let r=i.cookieDomain?`;domain=${i.cookieDomain}`:"";H(`${_}=${encodeURIComponent(JSON.stringify(t))};expires=${new Date(n).toUTCString()};path=/${r};SameSite=Lax${o}`)}function H(t){document.cookie=t}var y="webority_attribution",Nt=2160*60*60*1e3,ht=200,D=500,Dt=Object.freeze({utm_source:"utmSource",utm_medium:"utmMedium",utm_campaign:"utmCampaign",utm_content:"utmContent",utm_term:"utmTerm",gclid:"gclid",gbraid:"gbraid",wbraid:"wbraid",fbclid:"fbclid",msclkid:"msclkid",li_fat_id:"liFatId"}),J=!1;function Q(){tt("captureAttribution"),kt();let t=bt();if(!t)return;let e=k(L());Z(L(),{firstTouch:e?.firstTouch??t,lastTouch:t}),Lt(t)}function Lt(t){let e={event:"acquisition_touch",acquisition_channel:p(t)};t.aiSource&&(e.ai_source=t.aiSource),window.dataLayer=window.dataLayer||[],window.dataLayer.push(e)}function I(){return tt("getAttribution"),k(L())}function bt(){let t=new URLSearchParams(window.location.search),e={};for(let[r,c]of Object.entries(Dt)){let m=T(t.get(r),ht);m&&(e[c]=m)}let n=T(t.get("referrer"),D)||Rt();if(n&&(e.referrer=n),Object.keys(e).length===0)return null;let o=S(e);return o&&(e.aiSource=o),e.landingPage=T(window.location.pathname,D),e.capturedDateTimeUtc=new Date().toISOString(),e}function Rt(){let t=document.referrer;if(!t)return"";try{return new URL(t).hostname===window.location.hostname?"":T(t,D)}catch{return""}}function kt(){J||(J=!0,f(t=>{let e=t.marketing?R():b(),n=t.marketing?b():R(),o=k(e);vt(e),o&&Z(n,o)}))}function L(){return d().marketing?b():R()}function b(){try{return window.localStorage}catch{return null}}function R(){try{return window.sessionStorage}catch{return null}}function k(t){try{let e=t?.getItem(y);if(!e)return null;let n=JSON.parse(e),o=X(n?.firstTouch),r=X(n?.lastTouch);return!o&&!r?(t.removeItem(y),null):{firstTouch:o??r,lastTouch:r??o}}catch{return null}}function Z(t,e){try{t?.setItem(y,JSON.stringify(e))}catch{}}function vt(t){try{t?.removeItem(y)}catch{}}function X(t){if(!t||typeof t!="object")return null;let e=Date.parse(t.capturedDateTimeUtc);return Number.isFinite(e)&&Date.now()-e<=Nt?t:null}function T(t,e){let n=String(t??"").trim();return n?n.slice(0,e):""}function tt(t){if(!E())throw new Error(`@webority/analytics: call initConsent before ${t}.`)}var a=Object.freeze({SIGNUP_STARTED:"signup_started",SIGNUP_COMPLETED:"signup_completed",LOGIN_SUCCEEDED:"login_succeeded",ONBOARDING_STARTED:"onboarding_started",ONBOARDING_STEP_COMPLETED:"onboarding_step_completed",ONBOARDING_COMPLETED:"onboarding_completed",SUBSCRIPTION_ACTIVATED:"subscription_activated",SUBSCRIPTION_CANCELLED:"subscription_cancelled",PAYMENT_SUCCEEDED:"payment_succeeded",PAYMENT_FAILED:"payment_failed",FEEDBACK_SUBMITTED:"feedback_submitted",LEAD_SUBMITTED:"lead_submitted",WHATSAPP_CLICK:"whatsapp_click",CALCULATOR_USE:"calculator_use",PHONE_CLICK:"phone_click",DEMO_BOOKED:"demo_booked",CONTACT_SUBMITTED:"contact_submitted",TRIAL_START_CLICK:"trial_start_click",SAMPLE_REPORT_DOWNLOAD:"sample_report_download"}),Vt=Object.freeze({signupStarted:a.SIGNUP_STARTED,signupCompleted:a.SIGNUP_COMPLETED,loginSucceeded:a.LOGIN_SUCCEEDED,onboardingStarted:a.ONBOARDING_STARTED,onboardingStepCompleted:a.ONBOARDING_STEP_COMPLETED,onboardingCompleted:a.ONBOARDING_COMPLETED,subscriptionActivated:a.SUBSCRIPTION_ACTIVATED,subscriptionCancelled:a.SUBSCRIPTION_CANCELLED,paymentSucceeded:a.PAYMENT_SUCCEEDED,paymentFailed:a.PAYMENT_FAILED,feedbackSubmitted:a.FEEDBACK_SUBMITTED,leadSubmitted:a.LEAD_SUBMITTED,whatsappClick:a.WHATSAPP_CLICK,calculatorUse:a.CALCULATOR_USE,phoneClick:a.PHONE_CLICK,demoBooked:a.DEMO_BOOKED,contactSubmitted:a.CONTACT_SUBMITTED,trialStartClick:a.TRIAL_START_CLICK,sampleReportDownload:a.SAMPLE_REPORT_DOWNLOAD});var Ut=new Set(Object.values(a));function et(t,e={}){if(!Ut.has(t)||!d().analytics)return!1;let n=I(),o={...e,event:t};for(let[r,c]of[["first",n?.firstTouch],["last",n?.lastTouch]])c&&(o[`${r}_touch_channel`]=p(c),c.aiSource&&(o[`${r}_touch_ai_source`]=c.aiSource));return window.dataLayer=window.dataLayer||[],window.dataLayer.push(o),!0}var xt=["scroll","mousemove","touchstart","keydown","pointerdown"],nt=1500,Mt=3e3,Pt=100,l={loaded:!1,clarityId:null,clarityLoaded:!1};function rt(t={}){if(!E())throw new Error("@webority/analytics: call initConsent before loadTags.");if(l.loaded)return;l.loaded=!0;let{gtmId:e,clarityId:n,ahrefsKey:o}=t,r=q();if(o){let c=()=>U("https://analytics.ahrefs.com/analytics.js",{"data-key":o});r?ot(m=>m.analytics,c):c()}if(e){let c=()=>Gt(()=>{window.dataLayer=window.dataLayer||[],window.dataLayer.push({"gtm.start":Date.now(),event:"gtm.js"}),U(`https://www.googletagmanager.com/gtm.js?id=${encodeURIComponent(e)}`)});if(r){let m=h();ot(x=>x.analytics||x.marketing&&m,c)}else c()}n&&(l.clarityId=n,d().analytics&&zt(),f(c=>{c.analytics&&!l.clarityLoaded?v():l.clarityLoaded&&it(c)}))}function ot(t,e){if(t(d())){e();return}let n=f(o=>{t(o)&&(n(),e())})}function Gt(t){let e=!1,n=()=>{e||(e=!0,t())};for(let o of xt)window.addEventListener(o,n,{once:!0,passive:!0});document.readyState==="complete"?setTimeout(n,nt):window.addEventListener("load",()=>setTimeout(n,nt),{once:!0})}function zt(){let t=()=>{window.scrollY>Pt&&(window.removeEventListener("scroll",t),v())};window.addEventListener("scroll",t,{passive:!0}),setTimeout(v,Mt)}function v(){l.clarityLoaded||!l.clarityId||(l.clarityLoaded=!0,window.clarity=window.clarity||function(){window.clarity.q=window.clarity.q||[],window.clarity.q.push(arguments)},it(d()),U(`https://www.clarity.ms/tag/${encodeURIComponent(l.clarityId)}`,{crossorigin:"anonymous"}))}function it(t){window.clarity("consentv2",{ad_Storage:t.marketing&&h()?"granted":"denied",analytics_Storage:t.analytics?"granted":"denied"})}function U(t,e={}){let n=document.createElement("script");n.async=!0,n.src=t;for(let[o,r]of Object.entries(e))n.setAttribute(o,r);document.head.appendChild(n)}return lt(Ft);})();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webority/analytics",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/analytics.js CHANGED
@@ -104,6 +104,25 @@ export function identify(publicId, props = {}) {
104
104
  });
105
105
  }
106
106
 
107
+ // PostHog group analytics (a paid add-on). Same gate as identify: nothing is sent without consent.
108
+ export function group(type, key, props = {}) {
109
+ if (!isActive()) {
110
+ logInactive("group", { type, key, ...props });
111
+ return;
112
+ }
113
+
114
+ ensurePosthogInitialized()
115
+ .then((posthog) => {
116
+ if (!posthog || !hasConsent()) {
117
+ return;
118
+ }
119
+ posthog.group(type, key, props);
120
+ })
121
+ .catch(() => {
122
+ // Load failure is retried on the next call. Do not surface it as an unhandled rejection.
123
+ });
124
+ }
125
+
107
126
  export function resetIdentity() {
108
127
  if (!state.posthogInstance) {
109
128
  return;
@@ -1,3 +1,4 @@
1
+ import { resolveAiSource, resolveChannel } from "./channels.js";
1
2
  import { getConsent, isConsentInitialized, onConsentChange } from "./consent.js";
2
3
 
3
4
  export const ATTRIBUTION_STORAGE_KEY = "webority_attribution";
@@ -40,6 +41,18 @@ export function captureAttribution() {
40
41
 
41
42
  const current = readFrom(activeStore());
42
43
  writeTo(activeStore(), { firstTouch: current?.firstTouch ?? touch, lastTouch: touch });
44
+ pushLanding(touch);
45
+ }
46
+
47
+ // Lets a Tag Manager trigger on "acquisition_touch" and GA4 segment by ai_source and channel.
48
+ // Consent Mode still decides what the container's tags may store.
49
+ function pushLanding(touch) {
50
+ const landing = { event: "acquisition_touch", acquisition_channel: resolveChannel(touch) };
51
+ if (touch.aiSource) {
52
+ landing.ai_source = touch.aiSource;
53
+ }
54
+ window.dataLayer = window.dataLayer || [];
55
+ window.dataLayer.push(landing);
43
56
  }
44
57
 
45
58
  /** { firstTouch, lastTouch }, or null when no campaign or referral visit is on record. */
@@ -69,6 +82,11 @@ function touchFromLocation() {
69
82
  return null;
70
83
  }
71
84
 
85
+ const aiSource = resolveAiSource(touch);
86
+ if (aiSource) {
87
+ touch.aiSource = aiSource;
88
+ }
89
+
72
90
  // The path only: a query string can carry personal data a visitor typed into a form.
73
91
  touch.landingPage = clip(window.location.pathname, MAX_URL_LENGTH);
74
92
  touch.capturedDateTimeUtc = new Date().toISOString();
@@ -0,0 +1,193 @@
1
+ // The browser half of Webority.Analytics' AcquisitionChannelResolver. Keep the two tables in step.
2
+ const GOOGLE_SOURCES = ["google", "googleads", "adwords", "youtube"];
3
+ const META_SOURCES = ["meta", "facebook", "fb", "instagram", "ig", "messenger"];
4
+ const LINKEDIN_SOURCES = ["linkedin", "lnkd"];
5
+ const MICROSOFT_SOURCES = ["bing", "microsoft", "microsoftads", "msn"];
6
+
7
+ const AI_ASSISTANTS = Object.freeze([
8
+ ["chatgpt", ["chatgpt.com", "chat.openai.com"]],
9
+ ["perplexity", ["perplexity.ai"]],
10
+ ["gemini", ["gemini.google.com"]],
11
+ ["copilot", ["copilot.microsoft.com"]],
12
+ ["claude", ["claude.ai"]],
13
+ ["you", ["you.com"]],
14
+ ["phind", ["phind.com"]],
15
+ ["deepseek", ["deepseek.com"]],
16
+ ["grok", ["grok.com"]],
17
+ ["meta_ai", ["meta.ai"]],
18
+ ["poe", ["poe.com"]],
19
+ ]);
20
+
21
+ const SEARCH_HOSTS = ["bing.com", "duckduckgo.com", "search.yahoo.com", "ecosia.org", "search.brave.com"];
22
+ const SEARCH_BRANDS_WITH_COUNTRY_DOMAIN = ["google", "yandex"];
23
+
24
+ const SOCIAL_HOSTS = [
25
+ "facebook.com",
26
+ "fb.com",
27
+ "instagram.com",
28
+ "linkedin.com",
29
+ "lnkd.in",
30
+ "twitter.com",
31
+ "t.co",
32
+ "x.com",
33
+ "reddit.com",
34
+ "pinterest.com",
35
+ "youtube.com",
36
+ "youtu.be",
37
+ "tiktok.com",
38
+ "whatsapp.com",
39
+ "threads.net",
40
+ "t.me",
41
+ "telegram.org",
42
+ ];
43
+
44
+ export const channels = Object.freeze({
45
+ DIRECT: "direct",
46
+ GOOGLE: "google",
47
+ META: "meta",
48
+ LINKEDIN: "linkedin",
49
+ MICROSOFT: "microsoft",
50
+ OTHER: "other",
51
+ AI_ASSISTANT: "ai_assistant",
52
+ ORGANIC_SEARCH: "organic_search",
53
+ ORGANIC_SOCIAL: "organic_social",
54
+ REFERRAL: "referral",
55
+ });
56
+
57
+ /**
58
+ * The channel of a touch, as captureAttribution stores it. utm_source wins, then click ids; an
59
+ * untagged visit is classified by its referrer's host.
60
+ */
61
+ export function resolveChannel(touch) {
62
+ if (!touch) {
63
+ return channels.DIRECT;
64
+ }
65
+
66
+ const source = clean(touch.utmSource);
67
+ if (source) {
68
+ const key = lettersAndDigits(source);
69
+ if (GOOGLE_SOURCES.includes(key)) {
70
+ return channels.GOOGLE;
71
+ }
72
+ if (META_SOURCES.includes(key)) {
73
+ return channels.META;
74
+ }
75
+ if (LINKEDIN_SOURCES.includes(key)) {
76
+ return channels.LINKEDIN;
77
+ }
78
+ if (MICROSOFT_SOURCES.includes(key)) {
79
+ return channels.MICROSOFT;
80
+ }
81
+ }
82
+
83
+ if (clean(touch.fbclid)) {
84
+ return channels.META;
85
+ }
86
+ if (clean(touch.gclid) || clean(touch.gbraid) || clean(touch.wbraid)) {
87
+ return channels.GOOGLE;
88
+ }
89
+ if (clean(touch.liFatId)) {
90
+ return channels.LINKEDIN;
91
+ }
92
+ if (clean(touch.msclkid)) {
93
+ return channels.MICROSOFT;
94
+ }
95
+
96
+ if (assistantFromSource(source)) {
97
+ return channels.AI_ASSISTANT;
98
+ }
99
+
100
+ const tagged = source || clean(touch.utmMedium) || clean(touch.utmCampaign) || clean(touch.utmContent) || clean(touch.utmTerm);
101
+ if (tagged) {
102
+ return channels.OTHER;
103
+ }
104
+
105
+ const host = referrerHost(touch.referrer);
106
+ if (!host) {
107
+ return channels.DIRECT;
108
+ }
109
+ if (assistantFromHost(host)) {
110
+ return channels.AI_ASSISTANT;
111
+ }
112
+ if (isSearchHost(host)) {
113
+ return channels.ORGANIC_SEARCH;
114
+ }
115
+ return SOCIAL_HOSTS.some((social) => hostIs(host, social)) ? channels.ORGANIC_SOCIAL : channels.REFERRAL;
116
+ }
117
+
118
+ /** The AI assistant ("chatgpt", "perplexity", "gemini", "copilot", "claude"...) behind a touch, or "". */
119
+ export function resolveAiSource(touch) {
120
+ if (!touch) {
121
+ return "";
122
+ }
123
+ const fromSource = assistantFromSource(clean(touch.utmSource));
124
+ if (fromSource) {
125
+ return fromSource;
126
+ }
127
+ const host = referrerHost(touch.referrer);
128
+ return host ? assistantFromHost(host) : "";
129
+ }
130
+
131
+ function assistantFromSource(source) {
132
+ if (!source) {
133
+ return "";
134
+ }
135
+ const key = lettersAndDigits(source);
136
+ for (const [name, hosts] of AI_ASSISTANTS) {
137
+ if (lettersAndDigits(name) === key || hosts.some((host) => lettersAndDigits(host) === key)) {
138
+ return name;
139
+ }
140
+ }
141
+ return "";
142
+ }
143
+
144
+ function assistantFromHost(host) {
145
+ for (const [name, hosts] of AI_ASSISTANTS) {
146
+ if (hosts.some((assistantHost) => hostIs(host, assistantHost))) {
147
+ return name;
148
+ }
149
+ }
150
+ return "";
151
+ }
152
+
153
+ // google.com, google.co.in, yandex.ru, bing.com, search.yahoo.com: the search front end itself, never
154
+ // another product of the brand (docs.google.com, mail.google.com). The caller has stripped "www.".
155
+ function isSearchHost(host) {
156
+ if (SEARCH_HOSTS.includes(host)) {
157
+ return true;
158
+ }
159
+ return SEARCH_BRANDS_WITH_COUNTRY_DOMAIN.some((brand) => {
160
+ if (!host.startsWith(`${brand}.`)) {
161
+ return false;
162
+ }
163
+ const domain = host.slice(brand.length + 1).split(".");
164
+ return domain.length === 1 || (domain.length === 2 && (domain[0] === "co" || domain[0] === "com"));
165
+ });
166
+ }
167
+
168
+ function hostIs(host, domain) {
169
+ return host === domain || host.endsWith(`.${domain}`);
170
+ }
171
+
172
+ function referrerHost(referrer) {
173
+ const value = clean(referrer);
174
+ if (!value) {
175
+ return "";
176
+ }
177
+ try {
178
+ const host = new URL(/^[a-z][a-z0-9+.-]*:\/\//i.test(value) ? value : `https://${value}`).hostname.toLowerCase();
179
+ return host.startsWith("www.") ? host.slice(4) : host;
180
+ } catch {
181
+ return "";
182
+ }
183
+ }
184
+
185
+ function lettersAndDigits(value) {
186
+ return String(value)
187
+ .toLowerCase()
188
+ .replace(/[^\p{L}\p{N}]/gu, "");
189
+ }
190
+
191
+ function clean(value) {
192
+ return String(value ?? "").trim();
193
+ }
package/src/consent.js CHANGED
@@ -67,6 +67,7 @@ export function initConsent(options) {
67
67
  }
68
68
 
69
69
  pushConsentDefaults();
70
+ pushAdSignalSettings();
70
71
  if (consentState.saved) {
71
72
  pushConsentUpdate(consentState.saved);
72
73
  }
@@ -194,6 +195,14 @@ function pushConsentDefaults() {
194
195
  });
195
196
  }
196
197
 
198
+ // Both apply only while ad_storage is denied, so they are safe to set for every visitor: Google
199
+ // redacts ad click identifiers in requests, and url_passthrough keeps gclid and friends on the
200
+ // URLs we link to, so a denied visitor's click is not lost between pages.
201
+ function pushAdSignalSettings() {
202
+ gtag("set", "ads_data_redaction", true);
203
+ gtag("set", "url_passthrough", true);
204
+ }
205
+
197
206
  function pushConsentUpdate(saved) {
198
207
  const state = (granted) => (granted ? "granted" : "denied");
199
208
  const ads = saved.marketing && consentState.adStorage;
package/src/index.js CHANGED
@@ -1,15 +1,19 @@
1
- export { identify, initAnalytics, registerEvents, resetIdentity, trackEvent } from "./analytics.js";
1
+ export { group, identify, initAnalytics, registerEvents, resetIdentity, trackEvent } from "./analytics.js";
2
2
  export {
3
3
  CONSENT_CHANGE_EVENT,
4
4
  CONSENT_COOKIE_NAME,
5
5
  captureAttribution,
6
+ channels,
7
+ events,
6
8
  getAttribution,
7
9
  getConsent,
8
10
  initConsent,
9
11
  loadTags,
10
12
  onConsentChange,
11
13
  requiresOptIn,
14
+ resolveAiSource,
15
+ resolveChannel,
12
16
  saveConsent,
17
+ trackSiteEvent,
13
18
  } from "./site.js";
14
19
  export { track } from "./track-helpers.js";
15
- export { events } from "./vocabulary.js";
@@ -0,0 +1,35 @@
1
+ import { getAttribution } from "./attribution.js";
2
+ import { resolveChannel } from "./channels.js";
3
+ import { getConsent } from "./consent.js";
4
+ import { events } from "./vocabulary.js";
5
+
6
+ const CATALOGUE = new Set(Object.values(events));
7
+
8
+ /**
9
+ * Pushes a catalogue event (lead_submitted, whatsapp_click, calculator_use, phone_click...) to
10
+ * dataLayer for a Tag Manager trigger, carrying the visitor's first and last acquisition channel
11
+ * and ai_source. Nothing is pushed until the visitor has granted analytics consent, the same gate
12
+ * PostHog capture follows; a name outside the catalogue is dropped. Returns whether it was pushed.
13
+ */
14
+ export function trackSiteEvent(name, props = {}) {
15
+ if (!CATALOGUE.has(name) || !getConsent().analytics) {
16
+ return false;
17
+ }
18
+
19
+ const attribution = getAttribution();
20
+ const payload = { ...props, event: name };
21
+ for (const [prefix, touch] of [
22
+ ["first", attribution?.firstTouch],
23
+ ["last", attribution?.lastTouch],
24
+ ]) {
25
+ if (touch) {
26
+ payload[`${prefix}_touch_channel`] = resolveChannel(touch);
27
+ if (touch.aiSource) {
28
+ payload[`${prefix}_touch_ai_source`] = touch.aiSource;
29
+ }
30
+ }
31
+ }
32
+ window.dataLayer = window.dataLayer || [];
33
+ window.dataLayer.push(payload);
34
+ return true;
35
+ }
package/src/site.js CHANGED
@@ -1,6 +1,9 @@
1
1
  // Public websites: consent, marketing tags and attribution, without PostHog. The main entry
2
2
  // re-exports all of this for portals that also capture product events.
3
3
  export { captureAttribution, getAttribution } from "./attribution.js";
4
+ export { channels, resolveAiSource, resolveChannel } from "./channels.js";
4
5
  export { CONSENT_CHANGE_EVENT, CONSENT_COOKIE_NAME, getConsent, initConsent, onConsentChange, saveConsent } from "./consent.js";
5
6
  export { requiresOptIn } from "./consent-regions.js";
7
+ export { trackSiteEvent } from "./site-events.js";
6
8
  export { loadTags } from "./tags.js";
9
+ export { events } from "./vocabulary.js";
package/src/tags.js CHANGED
@@ -15,7 +15,8 @@ const tagState = {
15
15
  * Loads the marketing tags. Tag Manager waits for the first interaction, or 1.5s after load,
16
16
  * so its download never competes with the page's largest paint; Consent Mode (set by
17
17
  * initConsent) gates what its tags may do. Clarity records sessions, so it loads only with
18
- * analytics consent, and a revoke takes effect from the next page. Ahrefs is cookieless and
18
+ * analytics consent, and tells Clarity the visitor's choice (consentv2) when it loads and on every
19
+ * change after; a revoke stops its recording from the next page. Ahrefs is cookieless and
19
20
  * loads straight away. GA4 is deliberately absent: it runs inside the Tag Manager container,
20
21
  * and loading it here as well counts every page view twice.
21
22
  *
@@ -66,8 +67,10 @@ export function loadTags(options = {}) {
66
67
  scheduleClarity();
67
68
  }
68
69
  onConsentChange((consent) => {
69
- if (consent.analytics) {
70
+ if (consent.analytics && !tagState.clarityLoaded) {
70
71
  loadClarity();
72
+ } else if (tagState.clarityLoaded) {
73
+ sendClarityConsent(consent);
71
74
  }
72
75
  });
73
76
  }
@@ -131,9 +134,19 @@ function loadClarity() {
131
134
  // biome-ignore lint/complexity/noArguments: Clarity's stub queues the Arguments object, as its own snippet does.
132
135
  window.clarity.q.push(arguments);
133
136
  };
137
+ sendClarityConsent(getConsent());
134
138
  appendScript(`https://www.clarity.ms/tag/${encodeURIComponent(tagState.clarityId)}`, { crossorigin: "anonymous" });
135
139
  }
136
140
 
141
+ // Clarity's consent API v2 (required for EEA, UK and Switzerland visitors): the signal rides the
142
+ // stub's queue, so it is replayed in order once the script arrives.
143
+ function sendClarityConsent(consent) {
144
+ window.clarity("consentv2", {
145
+ ad_Storage: consent.marketing && isAdStorageEnabled() ? "granted" : "denied",
146
+ analytics_Storage: consent.analytics ? "granted" : "denied",
147
+ });
148
+ }
149
+
137
150
  function appendScript(src, attributes = {}) {
138
151
  const script = document.createElement("script");
139
152
  script.async = true;
package/src/vocabulary.js CHANGED
@@ -10,6 +10,14 @@ export const events = Object.freeze({
10
10
  PAYMENT_SUCCEEDED: "payment_succeeded",
11
11
  PAYMENT_FAILED: "payment_failed",
12
12
  FEEDBACK_SUBMITTED: "feedback_submitted",
13
+ LEAD_SUBMITTED: "lead_submitted",
14
+ WHATSAPP_CLICK: "whatsapp_click",
15
+ CALCULATOR_USE: "calculator_use",
16
+ PHONE_CLICK: "phone_click",
17
+ DEMO_BOOKED: "demo_booked",
18
+ CONTACT_SUBMITTED: "contact_submitted",
19
+ TRIAL_START_CLICK: "trial_start_click",
20
+ SAMPLE_REPORT_DOWNLOAD: "sample_report_download",
13
21
  });
14
22
 
15
23
  export const trackHelperNames = Object.freeze({
@@ -24,6 +32,14 @@ export const trackHelperNames = Object.freeze({
24
32
  paymentSucceeded: events.PAYMENT_SUCCEEDED,
25
33
  paymentFailed: events.PAYMENT_FAILED,
26
34
  feedbackSubmitted: events.FEEDBACK_SUBMITTED,
35
+ leadSubmitted: events.LEAD_SUBMITTED,
36
+ whatsappClick: events.WHATSAPP_CLICK,
37
+ calculatorUse: events.CALCULATOR_USE,
38
+ phoneClick: events.PHONE_CLICK,
39
+ demoBooked: events.DEMO_BOOKED,
40
+ contactSubmitted: events.CONTACT_SUBMITTED,
41
+ trialStartClick: events.TRIAL_START_CLICK,
42
+ sampleReportDownload: events.SAMPLE_REPORT_DOWNLOAD,
27
43
  });
28
44
 
29
45
  export const EVENT_NAME_PATTERN = /^[a-z][a-z0-9]*(_[a-z0-9]+)*$/;