@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.
- package/dist/chunk-FDHICTXR.js +1 -0
- package/dist/index.js +1 -0
- package/dist/site.js +1 -0
- package/package.json +7 -7
- package/src/analytics.js +0 -225
- package/src/attribution.js +0 -205
- package/src/channels.js +0 -193
- package/src/consent-regions.js +0 -47
- package/src/consent.js +0 -297
- package/src/index.js +0 -19
- package/src/posthog-bridge.js +0 -76
- package/src/site-events.js +0 -35
- package/src/site.js +0 -9
- package/src/state.js +0 -42
- package/src/tags.js +0 -191
- package/src/track-helpers.js +0 -8
- package/src/vocabulary.js +0 -47
package/src/state.js
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { getConsent as getSiteConsent, isConsentInitialized } from "./consent.js";
|
|
2
|
-
import { events } from "./vocabulary.js";
|
|
3
|
-
|
|
4
|
-
export const state = {
|
|
5
|
-
config: null,
|
|
6
|
-
posthogInstance: null,
|
|
7
|
-
posthogInitPromise: null,
|
|
8
|
-
registeredEvents: new Set(Object.values(events)),
|
|
9
|
-
warnedUnregisteredEvents: new Set(),
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
// The warning exists to reach a developer once. After this many distinct bad names the message
|
|
13
|
-
// has landed, and a product building names from data must not grow the set for the life of the tab.
|
|
14
|
-
export const WARNED_EVENT_LIMIT = 50;
|
|
15
|
-
|
|
16
|
-
// registeredEvents is deliberately not reset: an event name is part of the product's
|
|
17
|
-
// vocabulary, not of one analytics session, so a product that registers at module scope and
|
|
18
|
-
// starts analytics later in a bootstrap effect keeps its registrations. warnedUnregisteredEvents
|
|
19
|
-
// follows it for the same reason: the warning is about the code, not about one session, so
|
|
20
|
-
// re-initializing must not make the same mistake shout again.
|
|
21
|
-
export function resetState(config) {
|
|
22
|
-
state.config = config;
|
|
23
|
-
state.posthogInstance = null;
|
|
24
|
-
state.posthogInitPromise = null;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function isEligible() {
|
|
28
|
-
const config = state.config;
|
|
29
|
-
if (!config) {
|
|
30
|
-
return false;
|
|
31
|
-
}
|
|
32
|
-
return Boolean(config.enabled && config.clientKey && config.environment !== "Local");
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// The visitor's analytics consent is the only thing that gates capture.
|
|
36
|
-
export function hasConsent() {
|
|
37
|
-
return isConsentInitialized() && getSiteConsent().analytics;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export function isActive() {
|
|
41
|
-
return isEligible() && hasConsent();
|
|
42
|
-
}
|
package/src/tags.js
DELETED
|
@@ -1,191 +0,0 @@
|
|
|
1
|
-
import { getConsent, isAdStorageEnabled, isConsentInitialized, isStrictOptIn, onConsentChange } from "./consent.js";
|
|
2
|
-
|
|
3
|
-
const INTERACTION_EVENTS = ["scroll", "mousemove", "touchstart", "keydown", "pointerdown"];
|
|
4
|
-
const GOOGLE_GTM_URL = "https://www.googletagmanager.com/gtm.js";
|
|
5
|
-
const DEFER_AFTER_LOAD_MS = 1500;
|
|
6
|
-
const CLARITY_FALLBACK_MS = 3000;
|
|
7
|
-
const CLARITY_SCROLL_PX = 100;
|
|
8
|
-
|
|
9
|
-
const tagState = {
|
|
10
|
-
loaded: false,
|
|
11
|
-
clarityId: null,
|
|
12
|
-
clarityLoaded: false,
|
|
13
|
-
cancelClaritySchedule: null,
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Loads the marketing tags. Tag Manager waits for the first interaction, or 1.5s after load,
|
|
18
|
-
* so its download never competes with the page's largest paint; Consent Mode (set by
|
|
19
|
-
* initConsent) gates what its tags may do. Clarity records sessions, so it loads only with
|
|
20
|
-
* analytics consent, and tells Clarity the visitor's choice (consentv2) when it loads and on every
|
|
21
|
-
* change after; a revoke stops its recording from the next page. Ahrefs is cookieless and
|
|
22
|
-
* loads straight away. GA4 is deliberately absent: it runs inside the Tag Manager container,
|
|
23
|
-
* and loading it here as well counts every page view twice.
|
|
24
|
-
*
|
|
25
|
-
* gtmUrl replaces Google's loader address with the site's own (a proxy path or domain serving
|
|
26
|
-
* gtm.js); the container id is appended as the id query parameter. A visitor whose saved choice
|
|
27
|
-
* already grants Tag Manager gets it at DOM-ready, without the interaction wait.
|
|
28
|
-
*
|
|
29
|
-
* Under initConsent's strictOptIn nothing loads before consent: Ahrefs waits for analytics, and
|
|
30
|
-
* Tag Manager for analytics, or for marketing when adStorage lets marketing grant ad signals,
|
|
31
|
-
* whether granted on this page or saved on an earlier one. Marketing alone without adStorage
|
|
32
|
-
* unlocks nothing in the container, and loading it would still let GA4 send cookieless pings.
|
|
33
|
-
*/
|
|
34
|
-
export function loadTags(options = {}) {
|
|
35
|
-
if (!isConsentInitialized()) {
|
|
36
|
-
throw new Error("@webority/analytics: call initConsent before loadTags.");
|
|
37
|
-
}
|
|
38
|
-
if (tagState.loaded) {
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
tagState.loaded = true;
|
|
42
|
-
|
|
43
|
-
const { gtmId, gtmUrl = GOOGLE_GTM_URL, clarityId, ahrefsKey } = options;
|
|
44
|
-
if (typeof gtmUrl !== "string" || gtmUrl.trim() === "") {
|
|
45
|
-
throw new Error("@webority/analytics: gtmUrl must be a non-empty string.");
|
|
46
|
-
}
|
|
47
|
-
const strict = isStrictOptIn();
|
|
48
|
-
|
|
49
|
-
if (ahrefsKey) {
|
|
50
|
-
const loadAhrefs = () => appendScript("https://analytics.ahrefs.com/analytics.js", { "data-key": ahrefsKey });
|
|
51
|
-
if (strict) {
|
|
52
|
-
whenConsented((consent) => consent.analytics, loadAhrefs);
|
|
53
|
-
} else {
|
|
54
|
-
loadAhrefs();
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
if (gtmId) {
|
|
59
|
-
const ads = isAdStorageEnabled();
|
|
60
|
-
const tagManagerGranted = (consent) => consent.analytics || (consent.marketing && ads);
|
|
61
|
-
const injectTagManager = () => {
|
|
62
|
-
window.dataLayer = window.dataLayer || [];
|
|
63
|
-
window.dataLayer.push({ "gtm.start": Date.now(), event: "gtm.js" });
|
|
64
|
-
appendScript(`${gtmUrl}${gtmUrl.includes("?") ? "&" : "?"}id=${encodeURIComponent(gtmId)}`);
|
|
65
|
-
};
|
|
66
|
-
const saved = getConsent();
|
|
67
|
-
const schedule = saved.decided && tagManagerGranted(saved) ? runAtDomReady : runAfterFirstInteraction;
|
|
68
|
-
const loadTagManager = () => schedule(injectTagManager);
|
|
69
|
-
if (strict) {
|
|
70
|
-
whenConsented(tagManagerGranted, loadTagManager);
|
|
71
|
-
} else {
|
|
72
|
-
loadTagManager();
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (clarityId) {
|
|
77
|
-
tagState.clarityId = clarityId;
|
|
78
|
-
if (getConsent().analytics) {
|
|
79
|
-
scheduleClarity();
|
|
80
|
-
}
|
|
81
|
-
onConsentChange((consent) => {
|
|
82
|
-
if (consent.analytics && !tagState.clarityLoaded) {
|
|
83
|
-
loadClarity();
|
|
84
|
-
} else if (tagState.clarityLoaded) {
|
|
85
|
-
sendClarityConsent(consent);
|
|
86
|
-
}
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function whenConsented(granted, callback) {
|
|
92
|
-
if (granted(getConsent())) {
|
|
93
|
-
callback();
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
const unsubscribe = onConsentChange((consent) => {
|
|
97
|
-
if (granted(consent)) {
|
|
98
|
-
unsubscribe();
|
|
99
|
-
callback();
|
|
100
|
-
}
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function runAtDomReady(callback) {
|
|
105
|
-
if (document.readyState === "loading") {
|
|
106
|
-
document.addEventListener("DOMContentLoaded", callback, { once: true });
|
|
107
|
-
} else {
|
|
108
|
-
callback();
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function runAfterFirstInteraction(callback) {
|
|
113
|
-
let ran = false;
|
|
114
|
-
const run = () => {
|
|
115
|
-
if (ran) {
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
ran = true;
|
|
119
|
-
callback();
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
for (const eventName of INTERACTION_EVENTS) {
|
|
123
|
-
window.addEventListener(eventName, run, { once: true, passive: true });
|
|
124
|
-
}
|
|
125
|
-
if (document.readyState === "complete") {
|
|
126
|
-
setTimeout(run, DEFER_AFTER_LOAD_MS);
|
|
127
|
-
} else {
|
|
128
|
-
window.addEventListener("load", () => setTimeout(run, DEFER_AFTER_LOAD_MS), { once: true });
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function scheduleClarity() {
|
|
133
|
-
const onScroll = () => {
|
|
134
|
-
if (window.scrollY > CLARITY_SCROLL_PX) {
|
|
135
|
-
loadClarity();
|
|
136
|
-
}
|
|
137
|
-
};
|
|
138
|
-
window.addEventListener("scroll", onScroll, { passive: true });
|
|
139
|
-
const timer = setTimeout(loadClarity, CLARITY_FALLBACK_MS);
|
|
140
|
-
tagState.cancelClaritySchedule = () => {
|
|
141
|
-
window.removeEventListener("scroll", onScroll);
|
|
142
|
-
clearTimeout(timer);
|
|
143
|
-
};
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function loadClarity() {
|
|
147
|
-
// Whichever path loads Clarity first (scroll, timer or a consent grant) retires the others.
|
|
148
|
-
tagState.cancelClaritySchedule?.();
|
|
149
|
-
tagState.cancelClaritySchedule = null;
|
|
150
|
-
if (tagState.clarityLoaded || !tagState.clarityId) {
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
tagState.clarityLoaded = true;
|
|
154
|
-
|
|
155
|
-
// Clarity's own queue: calls made before its script arrives are replayed once it does.
|
|
156
|
-
window.clarity =
|
|
157
|
-
window.clarity ||
|
|
158
|
-
function clarityQueue() {
|
|
159
|
-
window.clarity.q = window.clarity.q || [];
|
|
160
|
-
// biome-ignore lint/complexity/noArguments: Clarity's stub queues the Arguments object, as its own snippet does.
|
|
161
|
-
window.clarity.q.push(arguments);
|
|
162
|
-
};
|
|
163
|
-
sendClarityConsent(getConsent());
|
|
164
|
-
appendScript(`https://www.clarity.ms/tag/${encodeURIComponent(tagState.clarityId)}`, { crossorigin: "anonymous" });
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// Clarity's consent API v2 (required for EEA, UK and Switzerland visitors): the signal rides the
|
|
168
|
-
// stub's queue, so it is replayed in order once the script arrives.
|
|
169
|
-
function sendClarityConsent(consent) {
|
|
170
|
-
window.clarity("consentv2", {
|
|
171
|
-
ad_Storage: consent.marketing && isAdStorageEnabled() ? "granted" : "denied",
|
|
172
|
-
analytics_Storage: consent.analytics ? "granted" : "denied",
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function appendScript(src, attributes = {}) {
|
|
177
|
-
const script = document.createElement("script");
|
|
178
|
-
script.async = true;
|
|
179
|
-
script.src = src;
|
|
180
|
-
for (const [name, value] of Object.entries(attributes)) {
|
|
181
|
-
script.setAttribute(name, value);
|
|
182
|
-
}
|
|
183
|
-
document.head.appendChild(script);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
export function resetTagsForTests() {
|
|
187
|
-
tagState.loaded = false;
|
|
188
|
-
tagState.clarityId = null;
|
|
189
|
-
tagState.clarityLoaded = false;
|
|
190
|
-
tagState.cancelClaritySchedule = null;
|
|
191
|
-
}
|
package/src/track-helpers.js
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import { trackEvent } from "./analytics.js";
|
|
2
|
-
import { trackHelperNames } from "./vocabulary.js";
|
|
3
|
-
|
|
4
|
-
export const track = Object.freeze(
|
|
5
|
-
Object.fromEntries(
|
|
6
|
-
Object.entries(trackHelperNames).map(([camelName, eventName]) => [camelName, (props = {}) => trackEvent(eventName, props)]),
|
|
7
|
-
),
|
|
8
|
-
);
|
package/src/vocabulary.js
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
export const events = Object.freeze({
|
|
2
|
-
SIGNUP_STARTED: "signup_started",
|
|
3
|
-
SIGNUP_COMPLETED: "signup_completed",
|
|
4
|
-
LOGIN_SUCCEEDED: "login_succeeded",
|
|
5
|
-
ONBOARDING_STARTED: "onboarding_started",
|
|
6
|
-
ONBOARDING_STEP_COMPLETED: "onboarding_step_completed",
|
|
7
|
-
ONBOARDING_COMPLETED: "onboarding_completed",
|
|
8
|
-
SUBSCRIPTION_ACTIVATED: "subscription_activated",
|
|
9
|
-
SUBSCRIPTION_CANCELLED: "subscription_cancelled",
|
|
10
|
-
PAYMENT_SUCCEEDED: "payment_succeeded",
|
|
11
|
-
PAYMENT_FAILED: "payment_failed",
|
|
12
|
-
FEEDBACK_SUBMITTED: "feedback_submitted",
|
|
13
|
-
LEAD_SUBMITTED: "lead_submitted",
|
|
14
|
-
LEAD_QUALIFIED: "lead_qualified",
|
|
15
|
-
WHATSAPP_CLICK: "whatsapp_click",
|
|
16
|
-
CALCULATOR_USE: "calculator_use",
|
|
17
|
-
PHONE_CLICK: "phone_click",
|
|
18
|
-
DEMO_BOOKED: "demo_booked",
|
|
19
|
-
CONTACT_SUBMITTED: "contact_submitted",
|
|
20
|
-
TRIAL_START_CLICK: "trial_start_click",
|
|
21
|
-
SAMPLE_REPORT_DOWNLOAD: "sample_report_download",
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
export const trackHelperNames = Object.freeze({
|
|
25
|
-
signupStarted: events.SIGNUP_STARTED,
|
|
26
|
-
signupCompleted: events.SIGNUP_COMPLETED,
|
|
27
|
-
loginSucceeded: events.LOGIN_SUCCEEDED,
|
|
28
|
-
onboardingStarted: events.ONBOARDING_STARTED,
|
|
29
|
-
onboardingStepCompleted: events.ONBOARDING_STEP_COMPLETED,
|
|
30
|
-
onboardingCompleted: events.ONBOARDING_COMPLETED,
|
|
31
|
-
subscriptionActivated: events.SUBSCRIPTION_ACTIVATED,
|
|
32
|
-
subscriptionCancelled: events.SUBSCRIPTION_CANCELLED,
|
|
33
|
-
paymentSucceeded: events.PAYMENT_SUCCEEDED,
|
|
34
|
-
paymentFailed: events.PAYMENT_FAILED,
|
|
35
|
-
feedbackSubmitted: events.FEEDBACK_SUBMITTED,
|
|
36
|
-
leadSubmitted: events.LEAD_SUBMITTED,
|
|
37
|
-
leadQualified: events.LEAD_QUALIFIED,
|
|
38
|
-
whatsappClick: events.WHATSAPP_CLICK,
|
|
39
|
-
calculatorUse: events.CALCULATOR_USE,
|
|
40
|
-
phoneClick: events.PHONE_CLICK,
|
|
41
|
-
demoBooked: events.DEMO_BOOKED,
|
|
42
|
-
contactSubmitted: events.CONTACT_SUBMITTED,
|
|
43
|
-
trialStartClick: events.TRIAL_START_CLICK,
|
|
44
|
-
sampleReportDownload: events.SAMPLE_REPORT_DOWNLOAD,
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
export const EVENT_NAME_PATTERN = /^[a-z][a-z0-9]*(_[a-z0-9]+)*$/;
|