@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.
- package/dist/webority-analytics-site.global.js +1 -1
- package/package.json +1 -1
- package/src/consent.js +60 -8
- package/src/tags.js +38 -7
|
@@ -1 +1 @@
|
|
|
1
|
-
var WeborityAnalytics=(()=>{var
|
|
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&>(),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
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
|
|
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
|
-
|
|
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.
|
|
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(
|
|
156
|
-
ad_user_data: state(
|
|
157
|
-
ad_personalization: state(
|
|
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
|
|
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) +
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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 = () => {
|