@webority/analytics 0.5.2 → 0.6.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 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);})();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webority/analytics",
3
- "version": "0.5.2",
3
+ "version": "0.6.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
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;
@@ -49,6 +76,14 @@ export function isConsentInitialized() {
49
76
  return consentState.initialized;
50
77
  }
51
78
 
79
+ export function isStrictOptIn() {
80
+ return consentState.strictOptIn;
81
+ }
82
+
83
+ export function isAdStorageEnabled() {
84
+ return consentState.adStorage;
85
+ }
86
+
52
87
  /**
53
88
  * The visitor's current consent. Before they choose (decided: false) every optional category
54
89
  * follows the region default: denied where opt-in is required, granted everywhere else.
@@ -124,6 +159,19 @@ function gtag() {
124
159
 
125
160
  // Defaults must reach dataLayer before the Tag Manager container loads, or its tags fire ungated.
126
161
  function pushConsentDefaults() {
162
+ if (consentState.strictOptIn) {
163
+ gtag("consent", "default", {
164
+ ad_storage: "denied",
165
+ ad_user_data: "denied",
166
+ ad_personalization: "denied",
167
+ analytics_storage: "denied",
168
+ functionality_storage: "denied",
169
+ personalization_storage: "denied",
170
+ security_storage: "granted",
171
+ wait_for_update: 500,
172
+ });
173
+ return;
174
+ }
127
175
  gtag("consent", "default", {
128
176
  ad_storage: "denied",
129
177
  ad_user_data: "denied",
@@ -148,13 +196,14 @@ function pushConsentDefaults() {
148
196
 
149
197
  function pushConsentUpdate(saved) {
150
198
  const state = (granted) => (granted ? "granted" : "denied");
199
+ const ads = saved.marketing && consentState.adStorage;
151
200
  gtag("consent", "update", {
152
201
  analytics_storage: state(saved.analytics),
153
202
  functionality_storage: state(saved.functional),
154
203
  personalization_storage: state(saved.functional),
155
- ad_storage: state(saved.marketing),
156
- ad_user_data: state(saved.marketing),
157
- ad_personalization: state(saved.marketing),
204
+ ad_storage: state(ads),
205
+ ad_user_data: state(ads),
206
+ ad_personalization: state(ads),
158
207
  });
159
208
  }
160
209
 
@@ -207,9 +256,9 @@ function newestChoice(choices) {
207
256
  }
208
257
 
209
258
  function writeConsentCookie(saved) {
210
- // The year runs from the choice, so folding an old choice onto the shared domain does not extend it.
259
+ // The lifetime runs from the choice, so folding an old choice onto the shared domain does not extend it.
211
260
  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;
261
+ const expiresAt = (Number.isNaN(chosenAt) ? Date.now() : chosenAt) + consentState.cookieDays * 24 * 60 * 60 * 1000;
213
262
  const secure = window.location?.protocol === "https:" ? ";Secure" : "";
214
263
  if (consentState.cookieDomain) {
215
264
  // Expired before the shared write: a browser that treats the two as one cookie on the
@@ -230,6 +279,9 @@ function setCookie(text) {
230
279
  export function resetConsentForTests() {
231
280
  consentState.initialized = false;
232
281
  consentState.requiresOptIn = true;
282
+ consentState.strictOptIn = false;
283
+ consentState.adStorage = true;
284
+ consentState.cookieDays = DEFAULT_COOKIE_DAYS;
233
285
  consentState.cookieDomain = null;
234
286
  consentState.saved = null;
235
287
  listeners.clear();
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;
@@ -18,6 +18,11 @@ const tagState = {
18
18
  * analytics consent, and a revoke takes effect from the next page. Ahrefs is cookieless and
19
19
  * loads straight away. GA4 is deliberately absent: it runs inside the Tag Manager container,
20
20
  * and loading it here as well counts every page view twice.
21
+ *
22
+ * Under initConsent's strictOptIn nothing loads before consent: Ahrefs waits for analytics, and
23
+ * Tag Manager for analytics, or for marketing when adStorage lets marketing grant ad signals,
24
+ * whether granted on this page or saved on an earlier one. Marketing alone without adStorage
25
+ * unlocks nothing in the container, and loading it would still let GA4 send cookieless pings.
21
26
  */
22
27
  export function loadTags(options = {}) {
23
28
  if (!isConsentInitialized()) {
@@ -29,17 +34,30 @@ export function loadTags(options = {}) {
29
34
  tagState.loaded = true;
30
35
 
31
36
  const { gtmId, clarityId, ahrefsKey } = options;
37
+ const strict = isStrictOptIn();
32
38
 
33
39
  if (ahrefsKey) {
34
- appendScript("https://analytics.ahrefs.com/analytics.js", { "data-key": ahrefsKey });
40
+ const loadAhrefs = () => appendScript("https://analytics.ahrefs.com/analytics.js", { "data-key": ahrefsKey });
41
+ if (strict) {
42
+ whenConsented((consent) => consent.analytics, loadAhrefs);
43
+ } else {
44
+ loadAhrefs();
45
+ }
35
46
  }
36
47
 
37
48
  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
- });
49
+ const loadTagManager = () =>
50
+ runAfterFirstInteraction(() => {
51
+ window.dataLayer = window.dataLayer || [];
52
+ window.dataLayer.push({ "gtm.start": Date.now(), event: "gtm.js" });
53
+ appendScript(`https://www.googletagmanager.com/gtm.js?id=${encodeURIComponent(gtmId)}`);
54
+ });
55
+ if (strict) {
56
+ const ads = isAdStorageEnabled();
57
+ whenConsented((consent) => consent.analytics || (consent.marketing && ads), loadTagManager);
58
+ } else {
59
+ loadTagManager();
60
+ }
43
61
  }
44
62
 
45
63
  if (clarityId) {
@@ -55,6 +73,19 @@ export function loadTags(options = {}) {
55
73
  }
56
74
  }
57
75
 
76
+ function whenConsented(granted, callback) {
77
+ if (granted(getConsent())) {
78
+ callback();
79
+ return;
80
+ }
81
+ const unsubscribe = onConsentChange((consent) => {
82
+ if (granted(consent)) {
83
+ unsubscribe();
84
+ callback();
85
+ }
86
+ });
87
+ }
88
+
58
89
  function runAfterFirstInteraction(callback) {
59
90
  let ran = false;
60
91
  const run = () => {