@webority/analytics 0.11.0 → 0.11.1

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.
@@ -0,0 +1 @@
1
+ var rt=["google","googleads","adwords","youtube"],it=["meta","facebook","fb","instagram","ig","messenger"],at=["linkedin","lnkd"],st=["bing","microsoft","microsoftads","msn"],P=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"]]]),ct=["bing.com","duckduckgo.com","search.yahoo.com","ecosia.org","search.brave.com"],ut=["google","yandex"],dt=["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 g(t){if(!t)return s.DIRECT;let e=c(t.utmSource);if(e){let r=C(e);if(rt.includes(r))return s.GOOGLE;if(it.includes(r))return s.META;if(at.includes(r))return s.LINKEDIN;if(st.includes(r))return s.MICROSOFT}if(c(t.fbclid))return s.META;if(c(t.gclid)||c(t.gbraid)||c(t.wbraid))return s.GOOGLE;if(c(t.liFatId))return s.LINKEDIN;if(c(t.msclkid))return s.MICROSOFT;if(G(e))return s.AI_ASSISTANT;if(e||c(t.utmMedium)||c(t.utmCampaign)||c(t.utmContent)||c(t.utmTerm))return s.OTHER;let o=B(t.referrer);return o?F(o)?s.AI_ASSISTANT:lt(o)?s.ORGANIC_SEARCH:dt.some(r=>z(o,r))?s.ORGANIC_SOCIAL:s.REFERRAL:s.DIRECT}function I(t){if(!t)return"";let e=G(c(t.utmSource));if(e)return e;let n=B(t.referrer);return n?F(n):""}function G(t){if(!t)return"";let e=C(t);for(let[n,o]of P)if(C(n)===e||o.some(r=>C(r)===e))return n;return""}function F(t){for(let[e,n]of P)if(n.some(o=>z(t,o)))return e;return""}function lt(t){return ct.includes(t)?!0:ut.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 B(t){let e=c(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 c(t){return String(t??"").trim()}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 ft(t){let e=typeof t=="string"?t.trim().toUpperCase():"";return e.length!==2||e==="XX"||e==="T1"?!0:w.includes(e)}var S="webority_cookie_consent",K="webority-consent-change",$=365,i={initialized:!1,requiresOptIn:!0,strictOptIn:!1,adStorage:!0,cookieDays:$,cookieDomain:null,saved:null},O=new Set;function mt(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}=St();i.saved=n,i.cookieDomain&&o>1&&n&&W(n),_t(),Ct(),i.saved&&Y(i.saved)}function E(){return i.initialized}function q(){return i.strictOptIn}function A(){return i.adStorage}function d(){j("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 pt(t){j("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,Y(e),gt(d())}function p(t){return O.add(t),()=>O.delete(t)}function gt(t){for(let e of O)try{e(t)}catch(n){queueMicrotask(()=>{throw n})}typeof window?.dispatchEvent=="function"&&typeof CustomEvent=="function"&&window.dispatchEvent(new CustomEvent(K,{detail:t}))}function j(t){if(!i.initialized)throw new Error(`@webority/analytics: call initConsent before ${t}.`)}function m(){window.dataLayer=window.dataLayer||[],window.dataLayer.push(arguments)}function _t(){if(i.strictOptIn){m("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}m("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]}),m("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 Ct(){m("set","ads_data_redaction",!0),m("set","url_passthrough",!0)}function Y(t){let e=o=>o?"granted":"denied",n=t.marketing&&i.adStorage;m("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 St(){let t=`${S}=`,e=(document.cookie||"").split(";").map(o=>o.trim()).filter(o=>o.startsWith(t)),n=e.map(o=>Et(o.slice(t.length))).filter(Boolean);return{saved:yt(n),entryCount:e.length}}function Et(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 yt(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&&H(`${S}=;expires=Thu, 01 Jan 1970 00:00:00 GMT;max-age=0;path=/;SameSite=Lax${o}`);let r=i.cookieDomain?`;domain=${i.cookieDomain}`:"";H(`${S}=${encodeURIComponent(JSON.stringify(t))};expires=${new Date(n).toUTCString()};path=/${r};SameSite=Lax${o}`)}function H(t){document.cookie=t}var T="webority_attribution",Tt=2160*60*60*1e3,It=200,h=500,wt=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 Ot(){Q("captureAttribution"),Nt();let t=ht();if(!t)return;let e=R(D());X(D(),{firstTouch:e?.firstTouch??t,lastTouch:t}),At(t)}function At(t){let e={event:"acquisition_touch",acquisition_channel:g(t)};t.aiSource&&(e.ai_source=t.aiSource),window.dataLayer=window.dataLayer||[],window.dataLayer.push(e)}function b(){return Q("getAttribution"),R(D())}function ht(){let t=new URLSearchParams(window.location.search),e={};for(let[r,f]of Object.entries(wt)){let l=y(t.get(r),It);l&&(e[f]=l)}let n=y(t.get("referrer"),h)||Dt();if(n&&(e.referrer=n),Object.keys(e).length===0)return null;let o=I(e);return o&&(e.aiSource=o),e.landingPage=y(window.location.pathname,h),e.capturedDateTimeUtc=new Date().toISOString(),e}function Dt(){let t=document.referrer;if(!t)return"";try{return new URL(t).hostname===window.location.hostname?"":y(t,h)}catch{return""}}function Nt(){V||(V=!0,p(t=>{let e=t.marketing?L():N(),n=t.marketing?N():L(),o=R(e);Lt(e),o&&X(n,o)}))}function D(){return d().marketing?N():L()}function N(){try{return window.localStorage}catch{return null}}function L(){try{return window.sessionStorage}catch{return null}}function R(t){try{let e=t?.getItem(T);if(!e)return null;let n=JSON.parse(e),o=J(n?.firstTouch),r=J(n?.lastTouch);return!o&&!r?(t.removeItem(T),null):{firstTouch:o??r,lastTouch:r??o}}catch{return null}}function X(t,e){try{t?.setItem(T,JSON.stringify(e))}catch{}}function Lt(t){try{t?.removeItem(T)}catch{}}function J(t){if(!t||typeof t!="object")return null;let e=Date.parse(t.capturedDateTimeUtc);return Number.isFinite(e)&&Date.now()-e<=Tt?t:null}function y(t,e){let n=String(t??"").trim();return n?n.slice(0,e):""}function Q(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",LEAD_QUALIFIED:"lead_qualified",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"}),Yt=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,leadQualified:a.LEAD_QUALIFIED,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}),Wt=/^[a-z][a-z0-9]*(_[a-z0-9]+)*$/;var bt=new Set(Object.values(a));function Rt(t,e={}){if(!bt.has(t)||!d().analytics)return!1;let n=b(),o={...e,event:t};for(let[r,f]of[["first",n?.firstTouch],["last",n?.lastTouch]])f&&(o[`${r}_touch_channel`]=g(f),f.aiSource&&(o[`${r}_touch_ai_source`]=f.aiSource));return window.dataLayer=window.dataLayer||[],window.dataLayer.push(o),!0}var kt=["scroll","mousemove","touchstart","keydown","pointerdown"],vt="https://www.googletagmanager.com/gtm.js",Z=1500,Ut=3e3,Mt=100,u={loaded:!1,clarityId:null,clarityLoaded:!1,cancelClaritySchedule:null};function xt(t={}){if(!E())throw new Error("@webority/analytics: call initConsent before loadTags.");if(u.loaded)return;u.loaded=!0;let{gtmId:e,gtmUrl:n=vt,clarityId:o,ahrefsKey:r}=t;if(typeof n!="string"||n.trim()==="")throw new Error("@webority/analytics: gtmUrl must be a non-empty string.");let f=q();if(r){let l=()=>v("https://analytics.ahrefs.com/analytics.js",{"data-key":r});f?tt(_=>_.analytics,l):l()}if(e){let l=A(),_=x=>x.analytics||x.marketing&&l,nt=()=>{window.dataLayer=window.dataLayer||[],window.dataLayer.push({"gtm.start":Date.now(),event:"gtm.js"}),v(`${n}${n.includes("?")?"&":"?"}id=${encodeURIComponent(e)}`)},U=d(),ot=U.decided&&_(U)?Pt:Gt,M=()=>ot(nt);f?tt(_,M):M()}o&&(u.clarityId=o,d().analytics&&Ft(),p(l=>{l.analytics&&!u.clarityLoaded?k():u.clarityLoaded&&et(l)}))}function tt(t,e){if(t(d())){e();return}let n=p(o=>{t(o)&&(n(),e())})}function Pt(t){document.readyState==="loading"?document.addEventListener("DOMContentLoaded",t,{once:!0}):t()}function Gt(t){let e=!1,n=()=>{e||(e=!0,t())};for(let o of kt)window.addEventListener(o,n,{once:!0,passive:!0});document.readyState==="complete"?setTimeout(n,Z):window.addEventListener("load",()=>setTimeout(n,Z),{once:!0})}function Ft(){let t=()=>{window.scrollY>Mt&&k()};window.addEventListener("scroll",t,{passive:!0});let e=setTimeout(k,Ut);u.cancelClaritySchedule=()=>{window.removeEventListener("scroll",t),clearTimeout(e)}}function k(){u.cancelClaritySchedule?.(),u.cancelClaritySchedule=null,!(u.clarityLoaded||!u.clarityId)&&(u.clarityLoaded=!0,window.clarity=window.clarity||function(){window.clarity.q=window.clarity.q||[],window.clarity.q.push(arguments)},et(d()),v(`https://www.clarity.ms/tag/${encodeURIComponent(u.clarityId)}`,{crossorigin:"anonymous"}))}function et(t){window.clarity("consentv2",{ad_Storage:t.marketing&&A()?"granted":"denied",analytics_Storage:t.analytics?"granted":"denied"})}function v(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)}export{ft as a,S as b,K as c,mt as d,E as e,d as f,pt as g,p as h,a as i,Yt as j,Wt as k,s as l,g as m,I as n,Ot as o,b as p,Rt as q,xt as r};
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ import{a as b,b as y,c as C,d as x,e as c,f as u,g as w,h as l,i as f,j as m,k as h,l as k,m as A,n as T,o as N,p as S,q as O,r as P}from"./chunk-FDHICTXR.js";var n={config:null,posthogInstance:null,posthogInitPromise:null,registeredEvents:new Set(Object.values(f)),warnedUnregisteredEvents:new Set},I=50;function E(e){n.config=e,n.posthogInstance=null,n.posthogInitPromise=null}function p(){let e=n.config;return e?!!(e.enabled&&e.clientKey&&e.environment!=="Local"):!1}function r(){return c()&&u().analytics}function s(){return p()&&r()}async function a(){if(n.posthogInstance)return n.posthogInstance;if(n.posthogInitPromise)return n.posthogInitPromise;let e=n.config;return n.posthogInitPromise=import("posthog-js").then(t=>{let o=t.default??t,i=R(U(e),r());return o.init(e.clientKey,i),r()||o.opt_out_capturing(),o.register({product:e.product,surface:e.surface}),n.posthogInstance=o,o}).catch(t=>{throw n.posthogInitPromise=null,t}),n.posthogInitPromise}function R(e,t){return t?e:{...e,opt_out_capturing_by_default:!0}}function U(e){let t=e.sessionRecording??{enabled:!1},o={api_host:e.host,autocapture:!1,capture_pageview:!0,persistence:"localStorage",disable_session_recording:!t.enabled,disable_surveys:!0,disable_product_tours:!0,disable_conversations:!0,advanced_disable_feature_flags:!t.enabled};return t.enabled&&(o.session_recording={maskAllInputs:t.maskAllInputs??!0,...t.maskTextSelector?{maskTextSelector:t.maskTextSelector}:{}}),o}var v=null;function z(e){if(!c())throw new Error("@webority/analytics: call initConsent before initAnalytics.");v?.(),v=l(t=>j(t.analytics)),E({host:e.host,clientKey:e.clientKey,product:e.product,surface:e.surface,environment:e.environment,enabled:e.enabled??!0,logger:e.logger??null,sessionRecording:{enabled:e.sessionRecording?.enabled??!1,maskAllInputs:e.sessionRecording?.maskAllInputs??!0,maskTextSelector:e.sessionRecording?.maskTextSelector??null}}),s()&&K()}function K(){let e=()=>{s()&&_()};typeof requestIdleCallback=="function"?requestIdleCallback(e,{timeout:3e3}):setTimeout(e,0)}function _(){a().catch(()=>{})}function j(e){if(e&&p()){n.posthogInstance?n.posthogInstance.opt_in_capturing():_();return}e||L()}function L(){if(n.posthogInstance){n.posthogInstance.opt_out_capturing();return}n.posthogInitPromise&&n.posthogInitPromise.then(e=>{r()||e?.opt_out_capturing()}).catch(()=>{})}function M(e,t={}){if(!s()){d("identify",{publicId:e,...t});return}a().then(o=>{!o||!r()||o.identify(e,t)}).catch(()=>{})}function V(e,t,o={}){if(!s()){d("group",{type:e,key:t,...o});return}a().then(i=>{!i||!r()||i.group(e,t,o)}).catch(()=>{})}function W(){n.posthogInstance&&n.posthogInstance.reset()}function q(e){for(let t of e){if(!h.test(t))throw new Error(`@webority/analytics: invalid event name "${t}". Expected snake_case.`);n.registeredEvents.add(t)}}function g(e,t={}){if(!n.registeredEvents.has(e)){D(e);return}H(e,t)}function D(e){if(n.warnedUnregisteredEvents.has(e))return;n.warnedUnregisteredEvents.size<I&&n.warnedUnregisteredEvents.add(e);let t=n.config?.logger,o=`@webority/analytics: event "${e}" is not registered, so it was dropped. Call registerEvents() first.`;if(typeof t?.warn=="function"){t.warn(o);return}if(typeof t?.info=="function"){t.info(o);return}console.warn(o)}function H(e,t){if(!s()){d(e,t);return}let o={...t,product:n.config.product,surface:n.config.surface,environment:n.config.environment};a().then(i=>{!i||!r()||i.capture(e,o)}).catch(()=>{})}function d(e,t){n.config?.environment==="Local"&&n.config.logger?.info(e,t)}var $=Object.freeze(Object.fromEntries(Object.entries(m).map(([e,t])=>[e,(o={})=>g(t,o)])));export{C as CONSENT_CHANGE_EVENT,y as CONSENT_COOKIE_NAME,N as captureAttribution,k as channels,f as events,S as getAttribution,u as getConsent,V as group,M as identify,z as initAnalytics,x as initConsent,P as loadTags,l as onConsentChange,q as registerEvents,b as requiresOptIn,W as resetIdentity,T as resolveAiSource,A as resolveChannel,w as saveConsent,$ as track,g as trackEvent,O as trackSiteEvent};
package/dist/site.js ADDED
@@ -0,0 +1 @@
1
+ import{a,b,c,d,f as e,g as f,h as g,i as h,l as i,m as j,n as k,o as l,p as m,q as n,r as o}from"./chunk-FDHICTXR.js";export{c as CONSENT_CHANGE_EVENT,b as CONSENT_COOKIE_NAME,l as captureAttribution,i as channels,h as events,m as getAttribution,e as getConsent,d as initConsent,o as loadTags,g as onConsentChange,a as requiresOptIn,k as resolveAiSource,j as resolveChannel,f as saveConsent,n as trackSiteEvent};
package/package.json CHANGED
@@ -1,23 +1,22 @@
1
1
  {
2
2
  "name": "@webority/analytics",
3
- "version": "0.11.0",
3
+ "version": "0.11.1",
4
4
  "license": "SEE LICENSE IN LICENSE",
5
5
  "type": "module",
6
- "main": "src/index.js",
6
+ "main": "dist/index.js",
7
7
  "exports": {
8
- ".": "./src/index.js",
9
- "./site": "./src/site.js",
8
+ ".": "./dist/index.js",
9
+ "./site": "./dist/site.js",
10
10
  "./dist/webority-analytics-site.global.js": "./dist/webority-analytics-site.global.js"
11
11
  },
12
12
  "sideEffects": [
13
13
  "./dist/*.global.js"
14
14
  ],
15
15
  "files": [
16
- "src",
17
16
  "dist"
18
17
  ],
19
18
  "scripts": {
20
- "build": "esbuild src/site.js --bundle --format=iife --global-name=WeborityAnalytics --target=es2020 --minify --outfile=dist/webority-analytics-site.global.js",
19
+ "build": "node -e \"require(\\\"node:fs\\\").rmSync(\\\"dist\\\",{recursive:true,force:true})\" && esbuild src/index.js src/site.js --bundle --splitting --format=esm --external:posthog-js --target=es2020 --minify --outdir=dist && esbuild src/site.js --bundle --format=iife --global-name=WeborityAnalytics --target=es2020 --minify --outfile=dist/webority-analytics-site.global.js",
21
20
  "prepack": "npm run build",
22
21
  "lint": "biome lint ./src ./test",
23
22
  "format": "biome format --write ./src ./test",
@@ -36,5 +35,6 @@
36
35
  "@biomejs/biome": "2.5.8",
37
36
  "esbuild": "0.28.2",
38
37
  "posthog-js": "1.417.1"
39
- }
38
+ },
39
+ "module": "dist/index.js"
40
40
  }
package/src/analytics.js DELETED
@@ -1,225 +0,0 @@
1
- import { isConsentInitialized, onConsentChange } from "./consent.js";
2
- import { ensurePosthogInitialized } from "./posthog-bridge.js";
3
- import { hasConsent, isActive, isEligible, resetState, state, WARNED_EVENT_LIMIT } from "./state.js";
4
- import { EVENT_NAME_PATTERN } from "./vocabulary.js";
5
-
6
- let stopFollowingConsent = null;
7
-
8
- /**
9
- * Starts PostHog capture. initConsent must run first: capture follows the visitor's analytics
10
- * consent and nothing else, so a surface with no consent source would capture under a choice
11
- * nobody was offered.
12
- */
13
- export function initAnalytics(config) {
14
- if (!isConsentInitialized()) {
15
- throw new Error("@webority/analytics: call initConsent before initAnalytics.");
16
- }
17
-
18
- stopFollowingConsent?.();
19
- stopFollowingConsent = onConsentChange((consent) => followConsent(consent.analytics));
20
-
21
- resetState({
22
- host: config.host,
23
- clientKey: config.clientKey,
24
- product: config.product,
25
- surface: config.surface,
26
- environment: config.environment,
27
- enabled: config.enabled ?? true,
28
- logger: config.logger ?? null,
29
- // Off unless the product asks: replay records page content, so turning it on is the
30
- // product's disclosed decision, never a package default.
31
- sessionRecording: {
32
- enabled: config.sessionRecording?.enabled ?? false,
33
- maskAllInputs: config.sessionRecording?.maskAllInputs ?? true,
34
- maskTextSelector: config.sessionRecording?.maskTextSelector ?? null,
35
- },
36
- });
37
-
38
- if (isActive()) {
39
- warmUpWhenIdle();
40
- }
41
- }
42
-
43
- // Keeps the PostHog download off the portal's start-up path. Events tracked before then start
44
- // the load themselves, so nothing is lost.
45
- function warmUpWhenIdle() {
46
- const start = () => {
47
- if (isActive()) {
48
- warmUp();
49
- }
50
- };
51
- if (typeof requestIdleCallback === "function") {
52
- requestIdleCallback(start, { timeout: 3000 });
53
- } else {
54
- setTimeout(start, 0);
55
- }
56
- }
57
-
58
- // Starts the loader without waiting for it. The rejection handler is what keeps a failed
59
- // load inside the package: the bridge clears its cached promise before rethrowing, so the
60
- // next event retries, and the product never sees an unhandled rejection.
61
- function warmUp() {
62
- ensurePosthogInitialized().catch(() => {
63
- // Retried on the next call. Analytics must stay invisible to the product.
64
- });
65
- }
66
-
67
- function followConsent(granted) {
68
- if (granted && isEligible()) {
69
- // A prior revoke leaves an already-initialized instance opted out, so re-granting must undo it.
70
- if (state.posthogInstance) {
71
- state.posthogInstance.opt_in_capturing();
72
- } else {
73
- warmUp();
74
- }
75
- return;
76
- }
77
-
78
- if (!granted) {
79
- // Revoking before init finishes must still opt out: the instance does not exist yet,
80
- // and init would otherwise start capturing under the consent that has just been withdrawn.
81
- optOutWhenReady();
82
- }
83
- }
84
-
85
- function optOutWhenReady() {
86
- if (state.posthogInstance) {
87
- state.posthogInstance.opt_out_capturing();
88
- return;
89
- }
90
-
91
- if (state.posthogInitPromise) {
92
- state.posthogInitPromise
93
- .then((posthog) => {
94
- if (!hasConsent()) {
95
- posthog?.opt_out_capturing();
96
- }
97
- })
98
- .catch(() => {
99
- // Init failed on its own; nothing was capturing.
100
- });
101
- }
102
- }
103
-
104
- export function identify(publicId, props = {}) {
105
- if (!isActive()) {
106
- logInactive("identify", { publicId, ...props });
107
- return;
108
- }
109
-
110
- ensurePosthogInitialized()
111
- .then((posthog) => {
112
- if (!posthog || !hasConsent()) {
113
- return;
114
- }
115
- posthog.identify(publicId, props);
116
- })
117
- .catch(() => {
118
- // Load failure is retried on the next call. Do not surface it as an unhandled rejection.
119
- });
120
- }
121
-
122
- // PostHog group analytics (a paid add-on). Same gate as identify: nothing is sent without consent.
123
- export function group(type, key, props = {}) {
124
- if (!isActive()) {
125
- logInactive("group", { type, key, ...props });
126
- return;
127
- }
128
-
129
- ensurePosthogInitialized()
130
- .then((posthog) => {
131
- if (!posthog || !hasConsent()) {
132
- return;
133
- }
134
- posthog.group(type, key, props);
135
- })
136
- .catch(() => {
137
- // Load failure is retried on the next call. Do not surface it as an unhandled rejection.
138
- });
139
- }
140
-
141
- export function resetIdentity() {
142
- if (!state.posthogInstance) {
143
- return;
144
- }
145
- state.posthogInstance.reset();
146
- }
147
-
148
- export function registerEvents(names) {
149
- for (const name of names) {
150
- if (!EVENT_NAME_PATTERN.test(name)) {
151
- throw new Error(`@webority/analytics: invalid event name "${name}". Expected snake_case.`);
152
- }
153
- state.registeredEvents.add(name);
154
- }
155
- }
156
-
157
- export function trackEvent(name, props = {}) {
158
- if (!state.registeredEvents.has(name)) {
159
- warnUnregisteredOnce(name);
160
- return;
161
- }
162
- capture(name, props);
163
- }
164
-
165
- // Parity with IAnalyticsClient.Track on the server half: the same developer mistake never
166
- // throws there, it is logged and the event is dropped. A throw here runs inside the user's
167
- // browser and can take out the handler that raised it, so the browser half must not be the
168
- // harsher one. Once per name, because a misregistered event usually sits in a hot path.
169
- function warnUnregisteredOnce(name) {
170
- if (state.warnedUnregisteredEvents.has(name)) {
171
- return;
172
- }
173
- if (state.warnedUnregisteredEvents.size < WARNED_EVENT_LIMIT) {
174
- state.warnedUnregisteredEvents.add(name);
175
- }
176
-
177
- const logger = state.config?.logger;
178
- const message = `@webority/analytics: event "${name}" is not registered, so it was dropped. Call registerEvents() first.`;
179
- if (typeof logger?.warn === "function") {
180
- logger.warn(message);
181
- return;
182
- }
183
- if (typeof logger?.info === "function") {
184
- logger.info(message);
185
- return;
186
- }
187
-
188
- // The no-console rule exists to stop debug logging, and passing no logger is the default.
189
- // Routing the one diagnostic this package emits to a logger nobody passed would hide exactly
190
- // the mistake the warning exists to surface.
191
- // biome-ignore lint/suspicious/noConsole: the package's one diagnostic must be visible by default
192
- console.warn(message);
193
- }
194
-
195
- function capture(name, props) {
196
- if (!isActive()) {
197
- logInactive(name, props);
198
- return;
199
- }
200
-
201
- // environment rides on every event: products may share one PostHog project across
202
- // deployed environments (capnix does), so it is the only reliable env discriminator.
203
- const merged = {
204
- ...props,
205
- product: state.config.product,
206
- surface: state.config.surface,
207
- environment: state.config.environment,
208
- };
209
- ensurePosthogInitialized()
210
- .then((posthog) => {
211
- if (!posthog || !hasConsent()) {
212
- return;
213
- }
214
- posthog.capture(name, merged);
215
- })
216
- .catch(() => {
217
- // Load failure is retried on the next call. Do not surface it as an unhandled rejection.
218
- });
219
- }
220
-
221
- function logInactive(name, props) {
222
- if (state.config?.environment === "Local") {
223
- state.config.logger?.info(name, props);
224
- }
225
- }
@@ -1,205 +0,0 @@
1
- import { resolveAiSource, resolveChannel } from "./channels.js";
2
- import { getConsent, isConsentInitialized, onConsentChange } from "./consent.js";
3
-
4
- export const ATTRIBUTION_STORAGE_KEY = "webority_attribution";
5
- const RETENTION_MS = 90 * 24 * 60 * 60 * 1000;
6
- const MAX_LENGTH = 200;
7
- const MAX_URL_LENGTH = 500;
8
-
9
- // Field names match Webority.Analytics' MarketingTouch, so the server binds this JSON as it is.
10
- const PARAM_TO_FIELD = Object.freeze({
11
- utm_source: "utmSource",
12
- utm_medium: "utmMedium",
13
- utm_campaign: "utmCampaign",
14
- utm_content: "utmContent",
15
- utm_term: "utmTerm",
16
- gclid: "gclid",
17
- gbraid: "gbraid",
18
- wbraid: "wbraid",
19
- fbclid: "fbclid",
20
- msclkid: "msclkid",
21
- li_fat_id: "liFatId",
22
- });
23
-
24
- let followingConsent = false;
25
-
26
- /**
27
- * Records how the visitor arrived: campaign parameters, click ids and an outside referrer.
28
- * Call on every page load, before a client-side router strips the query string. The first
29
- * touch is kept for 90 days; the last touch is replaced by each later campaign or referral
30
- * visit. A direct visit changes nothing. Stored in localStorage only with marketing consent,
31
- * otherwise for this tab alone.
32
- */
33
- export function captureAttribution() {
34
- ensureConsentInitialized("captureAttribution");
35
- followConsent();
36
-
37
- const touch = touchFromLocation();
38
- if (!touch) {
39
- return;
40
- }
41
-
42
- const current = readFrom(activeStore());
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);
56
- }
57
-
58
- /** { firstTouch, lastTouch }, or null when no campaign or referral visit is on record. */
59
- export function getAttribution() {
60
- ensureConsentInitialized("getAttribution");
61
- return readFrom(activeStore());
62
- }
63
-
64
- function touchFromLocation() {
65
- const params = new URLSearchParams(window.location.search);
66
- const touch = {};
67
- for (const [param, field] of Object.entries(PARAM_TO_FIELD)) {
68
- const value = clip(params.get(param), MAX_LENGTH);
69
- if (value) {
70
- touch[field] = value;
71
- }
72
- }
73
-
74
- // A referrer query parameter carries the original referrer across a hop between our own
75
- // domains (marketing site to portal), where document.referrer would name our own site.
76
- const referrer = clip(params.get("referrer"), MAX_URL_LENGTH) || outsideReferrer();
77
- if (referrer) {
78
- touch.referrer = referrer;
79
- }
80
-
81
- if (Object.keys(touch).length === 0) {
82
- return null;
83
- }
84
-
85
- const aiSource = resolveAiSource(touch);
86
- if (aiSource) {
87
- touch.aiSource = aiSource;
88
- }
89
-
90
- // The path only: a query string can carry personal data a visitor typed into a form.
91
- touch.landingPage = clip(window.location.pathname, MAX_URL_LENGTH);
92
- touch.capturedDateTimeUtc = new Date().toISOString();
93
- return touch;
94
- }
95
-
96
- function outsideReferrer() {
97
- const referrer = document.referrer;
98
- if (!referrer) {
99
- return "";
100
- }
101
- try {
102
- return new URL(referrer).hostname === window.location.hostname ? "" : clip(referrer, MAX_URL_LENGTH);
103
- } catch {
104
- return "";
105
- }
106
- }
107
-
108
- // Moves the record when consent changes: into localStorage when marketing is granted, back to
109
- // this tab's storage when it is withdrawn, so a revoke leaves nothing behind for 90 days.
110
- function followConsent() {
111
- if (followingConsent) {
112
- return;
113
- }
114
- followingConsent = true;
115
-
116
- onConsentChange((consent) => {
117
- const from = consent.marketing ? sessionStore() : localStore();
118
- const to = consent.marketing ? localStore() : sessionStore();
119
- const record = readFrom(from);
120
- removeFrom(from);
121
- if (record) {
122
- writeTo(to, record);
123
- }
124
- });
125
- }
126
-
127
- function activeStore() {
128
- return getConsent().marketing ? localStore() : sessionStore();
129
- }
130
-
131
- // Storage access throws in some private modes and sandboxed frames. Attribution is then lost for
132
- // this visit, which is the whole cost: the page and the product carry on.
133
- function localStore() {
134
- try {
135
- return window.localStorage;
136
- } catch {
137
- return null;
138
- }
139
- }
140
-
141
- function sessionStore() {
142
- try {
143
- return window.sessionStorage;
144
- } catch {
145
- return null;
146
- }
147
- }
148
-
149
- function readFrom(store) {
150
- try {
151
- const raw = store?.getItem(ATTRIBUTION_STORAGE_KEY);
152
- if (!raw) {
153
- return null;
154
- }
155
- const parsed = JSON.parse(raw);
156
- const firstTouch = fresh(parsed?.firstTouch);
157
- const lastTouch = fresh(parsed?.lastTouch);
158
- if (!firstTouch && !lastTouch) {
159
- store.removeItem(ATTRIBUTION_STORAGE_KEY);
160
- return null;
161
- }
162
- return { firstTouch: firstTouch ?? lastTouch, lastTouch: lastTouch ?? firstTouch };
163
- } catch {
164
- return null;
165
- }
166
- }
167
-
168
- function writeTo(store, record) {
169
- try {
170
- store?.setItem(ATTRIBUTION_STORAGE_KEY, JSON.stringify(record));
171
- } catch {
172
- // Quota or a blocked store: see localStore.
173
- }
174
- }
175
-
176
- function removeFrom(store) {
177
- try {
178
- store?.removeItem(ATTRIBUTION_STORAGE_KEY);
179
- } catch {
180
- // See localStore.
181
- }
182
- }
183
-
184
- function fresh(touch) {
185
- if (!touch || typeof touch !== "object") {
186
- return null;
187
- }
188
- const captured = Date.parse(touch.capturedDateTimeUtc);
189
- return Number.isFinite(captured) && Date.now() - captured <= RETENTION_MS ? touch : null;
190
- }
191
-
192
- function clip(value, maxLength) {
193
- const trimmed = String(value ?? "").trim();
194
- return trimmed ? trimmed.slice(0, maxLength) : "";
195
- }
196
-
197
- function ensureConsentInitialized(caller) {
198
- if (!isConsentInitialized()) {
199
- throw new Error(`@webority/analytics: call initConsent before ${caller}.`);
200
- }
201
- }
202
-
203
- export function resetAttributionForTests() {
204
- followingConsent = false;
205
- }