@webority/analytics 0.5.2 → 0.7.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 m=Object.defineProperty;var j=Object.getOwnPropertyDescriptor;var G=Object.getOwnPropertyNames;var K=Object.prototype.hasOwnProperty;var P=(t,e)=>{for(var n in e)m(t,n,{get:e[n],enumerable:!0})},H=(t,e,n,o)=>{if(e&&typeof e=="object"||typeof e=="function")for(let r of G(e))!K.call(t,r)&&r!==n&&m(t,r,{get:()=>e[r],enumerable:!(o=j(e,r))||o.enumerable});return t};var B=t=>H(m({},"__esModule",{value:!0}),t);var dt={};P(dt,{CONSENT_CHANGE_EVENT:()=>y,CONSENT_COOKIE_NAME:()=>u,captureAttribution:()=>z,getAttribution:()=>U,getConsent:()=>a,initConsent:()=>k,loadTags:()=>M,onConsentChange:()=>c,requiresOptIn:()=>O,saveConsent:()=>L});var p=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 O(t){let e=typeof t=="string"?t.trim().toUpperCase():"";return e.length!==2||e==="XX"||e==="T1"?!0:p.includes(e)}var u="webority_cookie_consent",y="webority-consent-change",Y=365,i={initialized:!1,requiresOptIn:!0,cookieDomain:null,saved:null},g=new Set;function k(t){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.');i.initialized=!0,i.requiresOptIn=t.requiresOptIn,i.cookieDomain=t.cookieDomain?.trim()??null;let{saved:e,entryCount:n}=V();i.saved=e,i.cookieDomain&&n>1&&e&&x(e),X(),i.saved&&D(i.saved)}function l(){return i.initialized}function a(){A("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 L(t){A("saveConsent");let e={essential:!0,analytics:t?.analytics===!0,functional:t?.functional===!0,marketing:t?.marketing===!0,timestamp:new Date().toISOString()};x(e),i.saved=e,D(e),J(a())}function c(t){return g.add(t),()=>g.delete(t)}function J(t){for(let e of g)try{e(t)}catch(n){queueMicrotask(()=>{throw n})}typeof window?.dispatchEvent=="function"&&typeof CustomEvent=="function"&&window.dispatchEvent(new CustomEvent(y,{detail:t}))}function A(t){if(!i.initialized)throw new Error(`@webority/analytics: call initConsent before ${t}.`)}function w(){window.dataLayer=window.dataLayer||[],window.dataLayer.push(arguments)}function X(){w("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:[...p]}),w("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 D(t){let e=n=>n?"granted":"denied";w("consent","update",{analytics_storage:e(t.analytics),functionality_storage:e(t.functional),personalization_storage:e(t.functional),ad_storage:e(t.marketing),ad_user_data:e(t.marketing),ad_personalization:e(t.marketing)})}function V(){let t=`${u}=`,e=(document.cookie||"").split(";").map(o=>o.trim()).filter(o=>o.startsWith(t)),n=e.map(o=>Q(o.slice(t.length))).filter(Boolean);return{saved:W(n),entryCount:e.length}}function Q(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 W(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 x(t){let e=typeof t.timestamp=="string"?Date.parse(t.timestamp):Number.NaN,n=(Number.isNaN(e)?Date.now():e)+Y*24*60*60*1e3,o=window.location?.protocol==="https:"?";Secure":"";i.cookieDomain&&b(`${u}=;expires=Thu, 01 Jan 1970 00:00:00 GMT;max-age=0;path=/;SameSite=Lax${o}`);let r=i.cookieDomain?`;domain=${i.cookieDomain}`:"";b(`${u}=${encodeURIComponent(JSON.stringify(t))};expires=${new Date(n).toUTCString()};path=/${r};SameSite=Lax${o}`)}function b(t){document.cookie=t}var f="webority_attribution",Z=2160*60*60*1e3,tt=200,C=500,et=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"}),v=!1;function z(){q("captureAttribution"),rt();let t=nt();if(!t)return;let e=I(_());F(_(),{firstTouch:e?.firstTouch??t,lastTouch:t})}function U(){return q("getAttribution"),I(_())}function nt(){let t=new URLSearchParams(window.location.search),e={};for(let[o,r]of Object.entries(et)){let S=d(t.get(o),tt);S&&(e[r]=S)}let n=d(t.get("referrer"),C)||ot();return n&&(e.referrer=n),Object.keys(e).length===0?null:(e.landingPage=d(window.location.pathname,C),e.capturedDateTimeUtc=new Date().toISOString(),e)}function ot(){let t=document.referrer;if(!t)return"";try{return new URL(t).hostname===window.location.hostname?"":d(t,C)}catch{return""}}function rt(){v||(v=!0,c(t=>{let e=t.marketing?T():h(),n=t.marketing?h():T(),o=I(e);it(e),o&&F(n,o)}))}function _(){return a().marketing?h():T()}function h(){try{return window.localStorage}catch{return null}}function T(){try{return window.sessionStorage}catch{return null}}function I(t){try{let e=t?.getItem(f);if(!e)return null;let n=JSON.parse(e),o=R(n?.firstTouch),r=R(n?.lastTouch);return!o&&!r?(t.removeItem(f),null):{firstTouch:o??r,lastTouch:r??o}}catch{return null}}function F(t,e){try{t?.setItem(f,JSON.stringify(e))}catch{}}function it(t){try{t?.removeItem(f)}catch{}}function R(t){if(!t||typeof t!="object")return null;let e=Date.parse(t.capturedDateTimeUtc);return Number.isFinite(e)&&Date.now()-e<=Z?t:null}function d(t,e){let n=String(t??"").trim();return n?n.slice(0,e):""}function q(t){if(!l())throw new Error(`@webority/analytics: call initConsent before ${t}.`)}var at=["scroll","mousemove","touchstart","keydown","pointerdown"],$=1500,st=3e3,ct=100,s={loaded:!1,clarityId:null,clarityLoaded:!1};function M(t={}){if(!l())throw new Error("@webority/analytics: call initConsent before loadTags.");if(s.loaded)return;s.loaded=!0;let{gtmId:e,clarityId:n,ahrefsKey:o}=t;o&&N("https://analytics.ahrefs.com/analytics.js",{"data-key":o}),e&&ut(()=>{window.dataLayer=window.dataLayer||[],window.dataLayer.push({"gtm.start":Date.now(),event:"gtm.js"}),N(`https://www.googletagmanager.com/gtm.js?id=${encodeURIComponent(e)}`)}),n&&(s.clarityId=n,a().analytics&&lt(),c(r=>{r.analytics&&E()}))}function ut(t){let e=!1,n=()=>{e||(e=!0,t())};for(let o of at)window.addEventListener(o,n,{once:!0,passive:!0});document.readyState==="complete"?setTimeout(n,$):window.addEventListener("load",()=>setTimeout(n,$),{once:!0})}function lt(){let t=()=>{window.scrollY>ct&&(window.removeEventListener("scroll",t),E())};window.addEventListener("scroll",t,{passive:!0}),setTimeout(E,st)}function E(){s.clarityLoaded||!s.clarityId||(s.clarityLoaded=!0,window.clarity=window.clarity||function(){window.clarity.q=window.clarity.q||[],window.clarity.q.push(arguments)},N(`https://www.clarity.ms/tag/${encodeURIComponent(s.clarityId)}`,{crossorigin:"anonymous"}))}function N(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 B(dt);})();
1
+ var WeborityAnalytics=(()=>{var T=Object.defineProperty;var at=Object.getOwnPropertyDescriptor;var st=Object.getOwnPropertyNames;var ct=Object.prototype.hasOwnProperty;var ut=(t,e)=>{for(var n in e)T(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&&T(t,r,{get:()=>e[r],enumerable:!(o=at(e,r))||o.enumerable});return t};var lt=t=>dt(T({},"__esModule",{value:!0}),t);var Ft={};ut(Ft,{CONSENT_CHANGE_EVENT:()=>h,CONSENT_COOKIE_NAME:()=>_,captureAttribution:()=>Q,channels:()=>s,events:()=>a,getAttribution:()=>I,getConsent:()=>d,initConsent:()=>q,loadTags:()=>rt,onConsentChange:()=>f,requiresOptIn:()=>H,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 H(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",h="webority-consent-change",$=365,i={initialized:!1,requiresOptIn:!0,strictOptIn:!1,adStorage:!0,cookieDays:$,cookieDomain:null,saved:null},A=new Set;function q(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??$,i.cookieDomain=t.cookieDomain?.trim()??null;let{saved:n,entryCount:o}=Tt();i.saved=n,i.cookieDomain&&o>1&&n&&W(n),wt(),It(),i.saved&&V(i.saved)}function E(){return i.initialized}function K(){return i.strictOptIn}function N(){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()};W(e),i.saved=e,V(e),yt(d())}function f(t){return A.add(t),()=>A.delete(t)}function yt(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(h,{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 wt(){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 V(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 Tt(){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 W(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&&B(`${_}=;expires=Thu, 01 Jan 1970 00:00:00 GMT;max-age=0;path=/;SameSite=Lax${o}`);let r=i.cookieDomain?`;domain=${i.cookieDomain}`:"";B(`${_}=${encodeURIComponent(JSON.stringify(t))};expires=${new Date(n).toUTCString()};path=/${r};SameSite=Lax${o}`)}function B(t){document.cookie=t}var w="webority_attribution",ht=2160*60*60*1e3,Nt=200,b=500,bt=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"),Rt();let t=Lt();if(!t)return;let e=R(D());Z(D(),{firstTouch:e?.firstTouch??t,lastTouch:t}),Dt(t)}function Dt(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"),R(D())}function Lt(){let t=new URLSearchParams(window.location.search),e={};for(let[r,c]of Object.entries(bt)){let m=y(t.get(r),Nt);m&&(e[c]=m)}let n=y(t.get("referrer"),b)||kt();if(n&&(e.referrer=n),Object.keys(e).length===0)return null;let o=S(e);return o&&(e.aiSource=o),e.landingPage=y(window.location.pathname,b),e.capturedDateTimeUtc=new Date().toISOString(),e}function kt(){let t=document.referrer;if(!t)return"";try{return new URL(t).hostname===window.location.hostname?"":y(t,b)}catch{return""}}function Rt(){J||(J=!0,f(t=>{let e=t.marketing?k():L(),n=t.marketing?L():k(),o=R(e);vt(e),o&&Z(n,o)}))}function D(){return d().marketing?L():k()}function L(){try{return window.localStorage}catch{return null}}function k(){try{return window.sessionStorage}catch{return null}}function R(t){try{let e=t?.getItem(w);if(!e)return null;let n=JSON.parse(e),o=X(n?.firstTouch),r=X(n?.lastTouch);return!o&&!r?(t.removeItem(w),null):{firstTouch:o??r,lastTouch:r??o}}catch{return null}}function Z(t,e){try{t?.setItem(w,JSON.stringify(e))}catch{}}function vt(t){try{t?.removeItem(w)}catch{}}function X(t){if(!t||typeof t!="object")return null;let e=Date.parse(t.capturedDateTimeUtc);return Number.isFinite(e)&&Date.now()-e<=ht?t:null}function y(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"}),Wt=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});var xt=new Set(Object.values(a));function et(t,e={}){if(!xt.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 Ut=["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=K();if(o){let c=()=>x("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"}),x(`https://www.googletagmanager.com/gtm.js?id=${encodeURIComponent(e)}`)});if(r){let m=N();ot(U=>U.analytics||U.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 Ut)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()),x(`https://www.clarity.ms/tag/${encodeURIComponent(l.clarityId)}`,{crossorigin:"anonymous"}))}function it(t){window.clarity("consentv2",{ad_Storage:t.marketing&&N()?"granted":"denied",analytics_Storage:t.analytics?"granted":"denied"})}function x(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.5.2",
3
+ "version": "0.7.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -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
@@ -2,11 +2,14 @@ import { OPT_IN_COUNTRIES } from "./consent-regions.js";
2
2
 
3
3
  export const CONSENT_COOKIE_NAME = "webority_cookie_consent";
4
4
  export const CONSENT_CHANGE_EVENT = "webority-consent-change";
5
- const CONSENT_COOKIE_DAYS = 365;
5
+ const DEFAULT_COOKIE_DAYS = 365;
6
6
 
7
7
  const consentState = {
8
8
  initialized: false,
9
9
  requiresOptIn: true,
10
+ strictOptIn: false,
11
+ adStorage: true,
12
+ cookieDays: DEFAULT_COOKIE_DAYS,
10
13
  cookieDomain: null,
11
14
  saved: null,
12
15
  };
@@ -18,17 +21,41 @@ const listeners = new Set();
18
21
  * knows the visitor's country (requiresOptIn(countryCode)); guessing it in the browser misplaces
19
22
  * VPN users. cookieDomain (".capnix.ai") shares one choice between the website and every portal
20
23
  * subdomain; without it the choice stays on the current host.
24
+ *
25
+ * strictOptIn asks every visitor in every region: Consent Mode is denied everywhere until they
26
+ * choose, loadTags holds Tag Manager and Ahrefs until consent, and requiresOptIn may be left out.
27
+ * adStorage says whether marketing consent also grants Google's ad_* signals: true by default,
28
+ * false under strictOptIn. cookieDays is the saved choice's lifetime, 365 by default.
21
29
  */
22
30
  export function initConsent(options) {
23
- if (typeof options?.requiresOptIn !== "boolean") {
31
+ const strictOptIn = options?.strictOptIn ?? false;
32
+ if (typeof strictOptIn !== "boolean") {
33
+ throw new Error("@webority/analytics: initConsent strictOptIn must be true or false, or left out.");
34
+ }
35
+ if (strictOptIn) {
36
+ if (options.requiresOptIn !== undefined && options.requiresOptIn !== true) {
37
+ throw new Error(
38
+ "@webority/analytics: initConsent strictOptIn asks every visitor to opt in, so requiresOptIn must be true or left out.",
39
+ );
40
+ }
41
+ } else if (typeof options?.requiresOptIn !== "boolean") {
24
42
  throw new Error("@webority/analytics: initConsent needs requiresOptIn (true or false), decided from the visitor's country.");
25
43
  }
26
44
  if (options.cookieDomain !== undefined && (typeof options.cookieDomain !== "string" || !options.cookieDomain.trim())) {
27
45
  throw new Error('@webority/analytics: initConsent cookieDomain must be a domain such as ".example.com", or left out.');
28
46
  }
47
+ if (options.cookieDays !== undefined && !(Number.isInteger(options.cookieDays) && options.cookieDays > 0)) {
48
+ throw new Error("@webority/analytics: initConsent cookieDays must be a whole number of days above zero, or left out.");
49
+ }
50
+ if (options.adStorage !== undefined && typeof options.adStorage !== "boolean") {
51
+ throw new Error("@webority/analytics: initConsent adStorage must be true or false, or left out.");
52
+ }
29
53
 
30
54
  consentState.initialized = true;
31
- consentState.requiresOptIn = options.requiresOptIn;
55
+ consentState.strictOptIn = strictOptIn;
56
+ consentState.requiresOptIn = strictOptIn || options.requiresOptIn;
57
+ consentState.adStorage = options.adStorage ?? !strictOptIn;
58
+ consentState.cookieDays = options.cookieDays ?? DEFAULT_COOKIE_DAYS;
32
59
  consentState.cookieDomain = options.cookieDomain?.trim() ?? null;
33
60
  const { saved, entryCount } = readConsentCookie();
34
61
  consentState.saved = saved;
@@ -40,6 +67,7 @@ export function initConsent(options) {
40
67
  }
41
68
 
42
69
  pushConsentDefaults();
70
+ pushAdSignalSettings();
43
71
  if (consentState.saved) {
44
72
  pushConsentUpdate(consentState.saved);
45
73
  }
@@ -49,6 +77,14 @@ export function isConsentInitialized() {
49
77
  return consentState.initialized;
50
78
  }
51
79
 
80
+ export function isStrictOptIn() {
81
+ return consentState.strictOptIn;
82
+ }
83
+
84
+ export function isAdStorageEnabled() {
85
+ return consentState.adStorage;
86
+ }
87
+
52
88
  /**
53
89
  * The visitor's current consent. Before they choose (decided: false) every optional category
54
90
  * follows the region default: denied where opt-in is required, granted everywhere else.
@@ -124,6 +160,19 @@ function gtag() {
124
160
 
125
161
  // Defaults must reach dataLayer before the Tag Manager container loads, or its tags fire ungated.
126
162
  function pushConsentDefaults() {
163
+ if (consentState.strictOptIn) {
164
+ gtag("consent", "default", {
165
+ ad_storage: "denied",
166
+ ad_user_data: "denied",
167
+ ad_personalization: "denied",
168
+ analytics_storage: "denied",
169
+ functionality_storage: "denied",
170
+ personalization_storage: "denied",
171
+ security_storage: "granted",
172
+ wait_for_update: 500,
173
+ });
174
+ return;
175
+ }
127
176
  gtag("consent", "default", {
128
177
  ad_storage: "denied",
129
178
  ad_user_data: "denied",
@@ -146,15 +195,24 @@ function pushConsentDefaults() {
146
195
  });
147
196
  }
148
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
+
149
206
  function pushConsentUpdate(saved) {
150
207
  const state = (granted) => (granted ? "granted" : "denied");
208
+ const ads = saved.marketing && consentState.adStorage;
151
209
  gtag("consent", "update", {
152
210
  analytics_storage: state(saved.analytics),
153
211
  functionality_storage: state(saved.functional),
154
212
  personalization_storage: state(saved.functional),
155
- ad_storage: state(saved.marketing),
156
- ad_user_data: state(saved.marketing),
157
- ad_personalization: state(saved.marketing),
213
+ ad_storage: state(ads),
214
+ ad_user_data: state(ads),
215
+ ad_personalization: state(ads),
158
216
  });
159
217
  }
160
218
 
@@ -207,9 +265,9 @@ function newestChoice(choices) {
207
265
  }
208
266
 
209
267
  function writeConsentCookie(saved) {
210
- // The year runs from the choice, so folding an old choice onto the shared domain does not extend it.
268
+ // The lifetime runs from the choice, so folding an old choice onto the shared domain does not extend it.
211
269
  const chosenAt = typeof saved.timestamp === "string" ? Date.parse(saved.timestamp) : Number.NaN;
212
- const expiresAt = (Number.isNaN(chosenAt) ? Date.now() : chosenAt) + CONSENT_COOKIE_DAYS * 24 * 60 * 60 * 1000;
270
+ const expiresAt = (Number.isNaN(chosenAt) ? Date.now() : chosenAt) + consentState.cookieDays * 24 * 60 * 60 * 1000;
213
271
  const secure = window.location?.protocol === "https:" ? ";Secure" : "";
214
272
  if (consentState.cookieDomain) {
215
273
  // Expired before the shared write: a browser that treats the two as one cookie on the
@@ -230,6 +288,9 @@ function setCookie(text) {
230
288
  export function resetConsentForTests() {
231
289
  consentState.initialized = false;
232
290
  consentState.requiresOptIn = true;
291
+ consentState.strictOptIn = false;
292
+ consentState.adStorage = true;
293
+ consentState.cookieDays = DEFAULT_COOKIE_DAYS;
233
294
  consentState.cookieDomain = null;
234
295
  consentState.saved = null;
235
296
  listeners.clear();
package/src/index.js CHANGED
@@ -3,13 +3,17 @@ 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
@@ -1,4 +1,4 @@
1
- import { getConsent, isConsentInitialized, onConsentChange } from "./consent.js";
1
+ import { getConsent, isAdStorageEnabled, isConsentInitialized, isStrictOptIn, onConsentChange } from "./consent.js";
2
2
 
3
3
  const INTERACTION_EVENTS = ["scroll", "mousemove", "touchstart", "keydown", "pointerdown"];
4
4
  const DEFER_AFTER_LOAD_MS = 1500;
@@ -15,9 +15,15 @@ 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.
22
+ *
23
+ * Under initConsent's strictOptIn nothing loads before consent: Ahrefs waits for analytics, and
24
+ * Tag Manager for analytics, or for marketing when adStorage lets marketing grant ad signals,
25
+ * whether granted on this page or saved on an earlier one. Marketing alone without adStorage
26
+ * unlocks nothing in the container, and loading it would still let GA4 send cookieless pings.
21
27
  */
22
28
  export function loadTags(options = {}) {
23
29
  if (!isConsentInitialized()) {
@@ -29,17 +35,30 @@ export function loadTags(options = {}) {
29
35
  tagState.loaded = true;
30
36
 
31
37
  const { gtmId, clarityId, ahrefsKey } = options;
38
+ const strict = isStrictOptIn();
32
39
 
33
40
  if (ahrefsKey) {
34
- appendScript("https://analytics.ahrefs.com/analytics.js", { "data-key": ahrefsKey });
41
+ const loadAhrefs = () => appendScript("https://analytics.ahrefs.com/analytics.js", { "data-key": ahrefsKey });
42
+ if (strict) {
43
+ whenConsented((consent) => consent.analytics, loadAhrefs);
44
+ } else {
45
+ loadAhrefs();
46
+ }
35
47
  }
36
48
 
37
49
  if (gtmId) {
38
- runAfterFirstInteraction(() => {
39
- window.dataLayer = window.dataLayer || [];
40
- window.dataLayer.push({ "gtm.start": Date.now(), event: "gtm.js" });
41
- appendScript(`https://www.googletagmanager.com/gtm.js?id=${encodeURIComponent(gtmId)}`);
42
- });
50
+ const loadTagManager = () =>
51
+ runAfterFirstInteraction(() => {
52
+ window.dataLayer = window.dataLayer || [];
53
+ window.dataLayer.push({ "gtm.start": Date.now(), event: "gtm.js" });
54
+ appendScript(`https://www.googletagmanager.com/gtm.js?id=${encodeURIComponent(gtmId)}`);
55
+ });
56
+ if (strict) {
57
+ const ads = isAdStorageEnabled();
58
+ whenConsented((consent) => consent.analytics || (consent.marketing && ads), loadTagManager);
59
+ } else {
60
+ loadTagManager();
61
+ }
43
62
  }
44
63
 
45
64
  if (clarityId) {
@@ -48,13 +67,28 @@ export function loadTags(options = {}) {
48
67
  scheduleClarity();
49
68
  }
50
69
  onConsentChange((consent) => {
51
- if (consent.analytics) {
70
+ if (consent.analytics && !tagState.clarityLoaded) {
52
71
  loadClarity();
72
+ } else if (tagState.clarityLoaded) {
73
+ sendClarityConsent(consent);
53
74
  }
54
75
  });
55
76
  }
56
77
  }
57
78
 
79
+ function whenConsented(granted, callback) {
80
+ if (granted(getConsent())) {
81
+ callback();
82
+ return;
83
+ }
84
+ const unsubscribe = onConsentChange((consent) => {
85
+ if (granted(consent)) {
86
+ unsubscribe();
87
+ callback();
88
+ }
89
+ });
90
+ }
91
+
58
92
  function runAfterFirstInteraction(callback) {
59
93
  let ran = false;
60
94
  const run = () => {
@@ -100,9 +134,19 @@ function loadClarity() {
100
134
  // biome-ignore lint/complexity/noArguments: Clarity's stub queues the Arguments object, as its own snippet does.
101
135
  window.clarity.q.push(arguments);
102
136
  };
137
+ sendClarityConsent(getConsent());
103
138
  appendScript(`https://www.clarity.ms/tag/${encodeURIComponent(tagState.clarityId)}`, { crossorigin: "anonymous" });
104
139
  }
105
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
+
106
150
  function appendScript(src, attributes = {}) {
107
151
  const script = document.createElement("script");
108
152
  script.async = true;
package/src/vocabulary.js CHANGED
@@ -10,6 +10,10 @@ 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",
13
17
  });
14
18
 
15
19
  export const trackHelperNames = Object.freeze({
@@ -24,6 +28,10 @@ export const trackHelperNames = Object.freeze({
24
28
  paymentSucceeded: events.PAYMENT_SUCCEEDED,
25
29
  paymentFailed: events.PAYMENT_FAILED,
26
30
  feedbackSubmitted: events.FEEDBACK_SUBMITTED,
31
+ leadSubmitted: events.LEAD_SUBMITTED,
32
+ whatsappClick: events.WHATSAPP_CLICK,
33
+ calculatorUse: events.CALCULATOR_USE,
34
+ phoneClick: events.PHONE_CLICK,
27
35
  });
28
36
 
29
37
  export const EVENT_NAME_PATTERN = /^[a-z][a-z0-9]*(_[a-z0-9]+)*$/;