@webority/analytics 0.11.0 → 0.11.2

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/src/channels.js DELETED
@@ -1,193 +0,0 @@
1
- // The browser half of Webority.Analytics' AcquisitionChannelResolver. Keep the two tables in step.
2
- const GOOGLE_SOURCES = ["google", "googleads", "adwords", "youtube"];
3
- const META_SOURCES = ["meta", "facebook", "fb", "instagram", "ig", "messenger"];
4
- const LINKEDIN_SOURCES = ["linkedin", "lnkd"];
5
- const MICROSOFT_SOURCES = ["bing", "microsoft", "microsoftads", "msn"];
6
-
7
- const AI_ASSISTANTS = Object.freeze([
8
- ["chatgpt", ["chatgpt.com", "chat.openai.com"]],
9
- ["perplexity", ["perplexity.ai"]],
10
- ["gemini", ["gemini.google.com"]],
11
- ["copilot", ["copilot.microsoft.com"]],
12
- ["claude", ["claude.ai"]],
13
- ["you", ["you.com"]],
14
- ["phind", ["phind.com"]],
15
- ["deepseek", ["deepseek.com"]],
16
- ["grok", ["grok.com"]],
17
- ["meta_ai", ["meta.ai"]],
18
- ["poe", ["poe.com"]],
19
- ]);
20
-
21
- const SEARCH_HOSTS = ["bing.com", "duckduckgo.com", "search.yahoo.com", "ecosia.org", "search.brave.com"];
22
- const SEARCH_BRANDS_WITH_COUNTRY_DOMAIN = ["google", "yandex"];
23
-
24
- const SOCIAL_HOSTS = [
25
- "facebook.com",
26
- "fb.com",
27
- "instagram.com",
28
- "linkedin.com",
29
- "lnkd.in",
30
- "twitter.com",
31
- "t.co",
32
- "x.com",
33
- "reddit.com",
34
- "pinterest.com",
35
- "youtube.com",
36
- "youtu.be",
37
- "tiktok.com",
38
- "whatsapp.com",
39
- "threads.net",
40
- "t.me",
41
- "telegram.org",
42
- ];
43
-
44
- export const channels = Object.freeze({
45
- DIRECT: "direct",
46
- GOOGLE: "google",
47
- META: "meta",
48
- LINKEDIN: "linkedin",
49
- MICROSOFT: "microsoft",
50
- OTHER: "other",
51
- AI_ASSISTANT: "ai_assistant",
52
- ORGANIC_SEARCH: "organic_search",
53
- ORGANIC_SOCIAL: "organic_social",
54
- REFERRAL: "referral",
55
- });
56
-
57
- /**
58
- * The channel of a touch, as captureAttribution stores it. utm_source wins, then click ids; an
59
- * untagged visit is classified by its referrer's host.
60
- */
61
- export function resolveChannel(touch) {
62
- if (!touch) {
63
- return channels.DIRECT;
64
- }
65
-
66
- const source = clean(touch.utmSource);
67
- if (source) {
68
- const key = lettersAndDigits(source);
69
- if (GOOGLE_SOURCES.includes(key)) {
70
- return channels.GOOGLE;
71
- }
72
- if (META_SOURCES.includes(key)) {
73
- return channels.META;
74
- }
75
- if (LINKEDIN_SOURCES.includes(key)) {
76
- return channels.LINKEDIN;
77
- }
78
- if (MICROSOFT_SOURCES.includes(key)) {
79
- return channels.MICROSOFT;
80
- }
81
- }
82
-
83
- if (clean(touch.fbclid)) {
84
- return channels.META;
85
- }
86
- if (clean(touch.gclid) || clean(touch.gbraid) || clean(touch.wbraid)) {
87
- return channels.GOOGLE;
88
- }
89
- if (clean(touch.liFatId)) {
90
- return channels.LINKEDIN;
91
- }
92
- if (clean(touch.msclkid)) {
93
- return channels.MICROSOFT;
94
- }
95
-
96
- if (assistantFromSource(source)) {
97
- return channels.AI_ASSISTANT;
98
- }
99
-
100
- const tagged = source || clean(touch.utmMedium) || clean(touch.utmCampaign) || clean(touch.utmContent) || clean(touch.utmTerm);
101
- if (tagged) {
102
- return channels.OTHER;
103
- }
104
-
105
- const host = referrerHost(touch.referrer);
106
- if (!host) {
107
- return channels.DIRECT;
108
- }
109
- if (assistantFromHost(host)) {
110
- return channels.AI_ASSISTANT;
111
- }
112
- if (isSearchHost(host)) {
113
- return channels.ORGANIC_SEARCH;
114
- }
115
- return SOCIAL_HOSTS.some((social) => hostIs(host, social)) ? channels.ORGANIC_SOCIAL : channels.REFERRAL;
116
- }
117
-
118
- /** The AI assistant ("chatgpt", "perplexity", "gemini", "copilot", "claude"...) behind a touch, or "". */
119
- export function resolveAiSource(touch) {
120
- if (!touch) {
121
- return "";
122
- }
123
- const fromSource = assistantFromSource(clean(touch.utmSource));
124
- if (fromSource) {
125
- return fromSource;
126
- }
127
- const host = referrerHost(touch.referrer);
128
- return host ? assistantFromHost(host) : "";
129
- }
130
-
131
- function assistantFromSource(source) {
132
- if (!source) {
133
- return "";
134
- }
135
- const key = lettersAndDigits(source);
136
- for (const [name, hosts] of AI_ASSISTANTS) {
137
- if (lettersAndDigits(name) === key || hosts.some((host) => lettersAndDigits(host) === key)) {
138
- return name;
139
- }
140
- }
141
- return "";
142
- }
143
-
144
- function assistantFromHost(host) {
145
- for (const [name, hosts] of AI_ASSISTANTS) {
146
- if (hosts.some((assistantHost) => hostIs(host, assistantHost))) {
147
- return name;
148
- }
149
- }
150
- return "";
151
- }
152
-
153
- // google.com, google.co.in, yandex.ru, bing.com, search.yahoo.com: the search front end itself, never
154
- // another product of the brand (docs.google.com, mail.google.com). The caller has stripped "www.".
155
- function isSearchHost(host) {
156
- if (SEARCH_HOSTS.includes(host)) {
157
- return true;
158
- }
159
- return SEARCH_BRANDS_WITH_COUNTRY_DOMAIN.some((brand) => {
160
- if (!host.startsWith(`${brand}.`)) {
161
- return false;
162
- }
163
- const domain = host.slice(brand.length + 1).split(".");
164
- return domain.length === 1 || (domain.length === 2 && (domain[0] === "co" || domain[0] === "com"));
165
- });
166
- }
167
-
168
- function hostIs(host, domain) {
169
- return host === domain || host.endsWith(`.${domain}`);
170
- }
171
-
172
- function referrerHost(referrer) {
173
- const value = clean(referrer);
174
- if (!value) {
175
- return "";
176
- }
177
- try {
178
- const host = new URL(/^[a-z][a-z0-9+.-]*:\/\//i.test(value) ? value : `https://${value}`).hostname.toLowerCase();
179
- return host.startsWith("www.") ? host.slice(4) : host;
180
- } catch {
181
- return "";
182
- }
183
- }
184
-
185
- function lettersAndDigits(value) {
186
- return String(value)
187
- .toLowerCase()
188
- .replace(/[^\p{L}\p{N}]/gu, "");
189
- }
190
-
191
- function clean(value) {
192
- return String(value ?? "").trim();
193
- }
@@ -1,47 +0,0 @@
1
- // The EEA, the UK and Switzerland: visitors here opt in before any non-essential cookie.
2
- // Webority.Analytics' ConsentRegion carries the same list for server-rendered pages, and a test
3
- // on that side holds the two in step.
4
- export const OPT_IN_COUNTRIES = Object.freeze([
5
- "AT",
6
- "BE",
7
- "BG",
8
- "HR",
9
- "CY",
10
- "CZ",
11
- "DK",
12
- "EE",
13
- "FI",
14
- "FR",
15
- "DE",
16
- "GR",
17
- "HU",
18
- "IE",
19
- "IT",
20
- "LV",
21
- "LT",
22
- "LU",
23
- "MT",
24
- "NL",
25
- "PL",
26
- "PT",
27
- "RO",
28
- "SK",
29
- "SI",
30
- "ES",
31
- "SE",
32
- "IS",
33
- "LI",
34
- "NO",
35
- "GB",
36
- "CH",
37
- ]);
38
-
39
- // An unknown country opts in: guessing "granted" for a visitor who is really in the EEA is the
40
- // failure with a legal cost. XX and T1 are Cloudflare's markers for an unplaced country and a Tor exit.
41
- export function requiresOptIn(countryCode) {
42
- const code = typeof countryCode === "string" ? countryCode.trim().toUpperCase() : "";
43
- if (code.length !== 2 || code === "XX" || code === "T1") {
44
- return true;
45
- }
46
- return OPT_IN_COUNTRIES.includes(code);
47
- }
package/src/consent.js DELETED
@@ -1,297 +0,0 @@
1
- import { OPT_IN_COUNTRIES } from "./consent-regions.js";
2
-
3
- export const CONSENT_COOKIE_NAME = "webority_cookie_consent";
4
- export const CONSENT_CHANGE_EVENT = "webority-consent-change";
5
- const DEFAULT_COOKIE_DAYS = 365;
6
-
7
- const consentState = {
8
- initialized: false,
9
- requiresOptIn: true,
10
- strictOptIn: false,
11
- adStorage: true,
12
- cookieDays: DEFAULT_COOKIE_DAYS,
13
- cookieDomain: null,
14
- saved: null,
15
- };
16
- const listeners = new Set();
17
-
18
- /**
19
- * Sets Google Consent Mode's defaults and applies the visitor's saved choice. Call once, first,
20
- * before initAnalytics, loadTags or captureAttribution. requiresOptIn comes from the host, which
21
- * knows the visitor's country (requiresOptIn(countryCode)); guessing it in the browser misplaces
22
- * VPN users. cookieDomain (".capnix.ai") shares one choice between the website and every portal
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.
29
- */
30
- export function initConsent(options) {
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") {
42
- throw new Error("@webority/analytics: initConsent needs requiresOptIn (true or false), decided from the visitor's country.");
43
- }
44
- if (options.cookieDomain !== undefined && (typeof options.cookieDomain !== "string" || !options.cookieDomain.trim())) {
45
- throw new Error('@webority/analytics: initConsent cookieDomain must be a domain such as ".example.com", or left out.');
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
- }
53
-
54
- consentState.initialized = true;
55
- consentState.strictOptIn = strictOptIn;
56
- consentState.requiresOptIn = strictOptIn || options.requiresOptIn;
57
- consentState.adStorage = options.adStorage ?? !strictOptIn;
58
- consentState.cookieDays = options.cookieDays ?? DEFAULT_COOKIE_DAYS;
59
- consentState.cookieDomain = options.cookieDomain?.trim() ?? null;
60
- const { saved, entryCount } = readConsentCookie();
61
- consentState.saved = saved;
62
-
63
- // A host-only cookie from before the domain was set sits beside the shared one, and
64
- // browsers list the older first. Folding them into one stops an old choice resurfacing.
65
- if (consentState.cookieDomain && entryCount > 1 && saved) {
66
- writeConsentCookie(saved);
67
- }
68
-
69
- pushConsentDefaults();
70
- pushAdSignalSettings();
71
- if (consentState.saved) {
72
- pushConsentUpdate(consentState.saved);
73
- }
74
- }
75
-
76
- export function isConsentInitialized() {
77
- return consentState.initialized;
78
- }
79
-
80
- export function isStrictOptIn() {
81
- return consentState.strictOptIn;
82
- }
83
-
84
- export function isAdStorageEnabled() {
85
- return consentState.adStorage;
86
- }
87
-
88
- /**
89
- * The visitor's current consent. Before they choose (decided: false) every optional category
90
- * follows the region default: denied where opt-in is required, granted everywhere else.
91
- */
92
- export function getConsent() {
93
- ensureInitialized("getConsent");
94
-
95
- const saved = consentState.saved;
96
- if (saved) {
97
- return { essential: true, analytics: saved.analytics, functional: saved.functional, marketing: saved.marketing, decided: true };
98
- }
99
-
100
- const granted = !consentState.requiresOptIn;
101
- return { essential: true, analytics: granted, functional: granted, marketing: granted, decided: false };
102
- }
103
-
104
- /** Records the visitor's choice, tells Google, and notifies every listener. */
105
- export function saveConsent(preferences) {
106
- ensureInitialized("saveConsent");
107
-
108
- const saved = {
109
- essential: true,
110
- analytics: preferences?.analytics === true,
111
- functional: preferences?.functional === true,
112
- marketing: preferences?.marketing === true,
113
- // The cookie's existing field name: visitors already hold cookies in this shape.
114
- timestamp: new Date().toISOString(),
115
- };
116
- writeConsentCookie(saved);
117
- consentState.saved = saved;
118
-
119
- pushConsentUpdate(saved);
120
- notify(getConsent());
121
- }
122
-
123
- /** Calls listener with the new consent after every saveConsent. Returns an unsubscribe function. */
124
- export function onConsentChange(listener) {
125
- listeners.add(listener);
126
- return () => listeners.delete(listener);
127
- }
128
-
129
- function notify(consent) {
130
- for (const listener of listeners) {
131
- try {
132
- listener(consent);
133
- } catch (error) {
134
- // One broken listener must not stop the others or the save. Rethrown on its own
135
- // turn so it still reaches the page's error reporting instead of vanishing.
136
- queueMicrotask(() => {
137
- throw error;
138
- });
139
- }
140
- }
141
-
142
- // Pages without a module loader (plain Razor scripts) listen on window instead.
143
- if (typeof window?.dispatchEvent === "function" && typeof CustomEvent === "function") {
144
- window.dispatchEvent(new CustomEvent(CONSENT_CHANGE_EVENT, { detail: consent }));
145
- }
146
- }
147
-
148
- function ensureInitialized(caller) {
149
- if (!consentState.initialized) {
150
- throw new Error(`@webority/analytics: call initConsent before ${caller}.`);
151
- }
152
- }
153
-
154
- // Consent Mode reads the real Arguments object; a plain array is ignored.
155
- function gtag() {
156
- window.dataLayer = window.dataLayer || [];
157
- // biome-ignore lint/complexity/noArguments: Consent Mode reads the Arguments object, not an array.
158
- window.dataLayer.push(arguments);
159
- }
160
-
161
- // Defaults must reach dataLayer before the Tag Manager container loads, or its tags fire ungated.
162
- function pushConsentDefaults() {
163
- if (consentState.strictOptIn) {
164
- gtag("consent", "default", {
165
- ad_storage: "denied",
166
- ad_user_data: "denied",
167
- ad_personalization: "denied",
168
- analytics_storage: "denied",
169
- functionality_storage: "denied",
170
- personalization_storage: "denied",
171
- security_storage: "granted",
172
- wait_for_update: 500,
173
- });
174
- return;
175
- }
176
- gtag("consent", "default", {
177
- ad_storage: "denied",
178
- ad_user_data: "denied",
179
- ad_personalization: "denied",
180
- analytics_storage: "denied",
181
- functionality_storage: "denied",
182
- personalization_storage: "denied",
183
- security_storage: "granted",
184
- wait_for_update: 500,
185
- region: [...OPT_IN_COUNTRIES],
186
- });
187
- gtag("consent", "default", {
188
- ad_storage: "granted",
189
- ad_user_data: "granted",
190
- ad_personalization: "granted",
191
- analytics_storage: "granted",
192
- functionality_storage: "granted",
193
- personalization_storage: "granted",
194
- security_storage: "granted",
195
- });
196
- }
197
-
198
- // Both apply only while ad_storage is denied, so they are safe to set for every visitor: Google
199
- // redacts ad click identifiers in requests, and url_passthrough keeps gclid and friends on the
200
- // URLs we link to, so a denied visitor's click is not lost between pages.
201
- function pushAdSignalSettings() {
202
- gtag("set", "ads_data_redaction", true);
203
- gtag("set", "url_passthrough", true);
204
- }
205
-
206
- function pushConsentUpdate(saved) {
207
- const state = (granted) => (granted ? "granted" : "denied");
208
- const ads = saved.marketing && consentState.adStorage;
209
- gtag("consent", "update", {
210
- analytics_storage: state(saved.analytics),
211
- functionality_storage: state(saved.functional),
212
- personalization_storage: state(saved.functional),
213
- ad_storage: state(ads),
214
- ad_user_data: state(ads),
215
- ad_personalization: state(ads),
216
- });
217
- }
218
-
219
- function readConsentCookie() {
220
- const prefix = `${CONSENT_COOKIE_NAME}=`;
221
- const entries = (document.cookie || "")
222
- .split(";")
223
- .map((part) => part.trim())
224
- .filter((part) => part.startsWith(prefix));
225
- const choices = entries.map((entry) => parseConsentEntry(entry.slice(prefix.length))).filter(Boolean);
226
- return { saved: newestChoice(choices), entryCount: entries.length };
227
- }
228
-
229
- function parseConsentEntry(value) {
230
- try {
231
- const parsed = JSON.parse(decodeURIComponent(value));
232
- if (!parsed || typeof parsed !== "object") {
233
- return null;
234
- }
235
- return {
236
- essential: true,
237
- analytics: parsed.analytics === true,
238
- functional: parsed.functional === true,
239
- marketing: parsed.marketing === true,
240
- timestamp: parsed.timestamp ?? null,
241
- };
242
- } catch {
243
- // A cookie we cannot read is treated as no choice made: the banner asks again.
244
- return null;
245
- }
246
- }
247
-
248
- // Without a readable timestamp on every choice, the last one wins: browsers list equal-path
249
- // cookies oldest first.
250
- function newestChoice(choices) {
251
- if (choices.length === 0) {
252
- return null;
253
- }
254
- const times = choices.map((choice) => (typeof choice.timestamp === "string" ? Date.parse(choice.timestamp) : Number.NaN));
255
- if (times.some(Number.isNaN)) {
256
- return choices.at(-1);
257
- }
258
- let newest = 0;
259
- for (let index = 1; index < choices.length; index++) {
260
- if (times[index] >= times[newest]) {
261
- newest = index;
262
- }
263
- }
264
- return choices[newest];
265
- }
266
-
267
- function writeConsentCookie(saved) {
268
- // The lifetime runs from the choice, so folding an old choice onto the shared domain does not extend it.
269
- const chosenAt = typeof saved.timestamp === "string" ? Date.parse(saved.timestamp) : Number.NaN;
270
- const expiresAt = (Number.isNaN(chosenAt) ? Date.now() : chosenAt) + consentState.cookieDays * 24 * 60 * 60 * 1000;
271
- const secure = window.location?.protocol === "https:" ? ";Secure" : "";
272
- if (consentState.cookieDomain) {
273
- // Expired before the shared write: a browser that treats the two as one cookie on the
274
- // apex host then still ends up holding the shared one.
275
- setCookie(`${CONSENT_COOKIE_NAME}=;expires=Thu, 01 Jan 1970 00:00:00 GMT;max-age=0;path=/;SameSite=Lax${secure}`);
276
- }
277
- const domain = consentState.cookieDomain ? `;domain=${consentState.cookieDomain}` : "";
278
- setCookie(
279
- `${CONSENT_COOKIE_NAME}=${encodeURIComponent(JSON.stringify(saved))};expires=${new Date(expiresAt).toUTCString()};path=/${domain};SameSite=Lax${secure}`,
280
- );
281
- }
282
-
283
- function setCookie(text) {
284
- // biome-ignore lint/suspicious/noDocumentCookie: the Cookie Store API is missing from browsers our visitors use.
285
- document.cookie = text;
286
- }
287
-
288
- export function resetConsentForTests() {
289
- consentState.initialized = false;
290
- consentState.requiresOptIn = true;
291
- consentState.strictOptIn = false;
292
- consentState.adStorage = true;
293
- consentState.cookieDays = DEFAULT_COOKIE_DAYS;
294
- consentState.cookieDomain = null;
295
- consentState.saved = null;
296
- listeners.clear();
297
- }
package/src/index.js DELETED
@@ -1,19 +0,0 @@
1
- export { group, identify, initAnalytics, registerEvents, resetIdentity, trackEvent } from "./analytics.js";
2
- export {
3
- CONSENT_CHANGE_EVENT,
4
- CONSENT_COOKIE_NAME,
5
- captureAttribution,
6
- channels,
7
- events,
8
- getAttribution,
9
- getConsent,
10
- initConsent,
11
- loadTags,
12
- onConsentChange,
13
- requiresOptIn,
14
- resolveAiSource,
15
- resolveChannel,
16
- saveConsent,
17
- trackSiteEvent,
18
- } from "./site.js";
19
- export { track } from "./track-helpers.js";
@@ -1,76 +0,0 @@
1
- import { hasConsent, state } from "./state.js";
2
-
3
- // Dynamic import keeps posthog-js out of the module graph entirely for callers that
4
- // never go active (e.g. Local environment), because it assumes browser globals at init time.
5
- export async function ensurePosthogInitialized() {
6
- if (state.posthogInstance) {
7
- return state.posthogInstance;
8
- }
9
- if (state.posthogInitPromise) {
10
- return state.posthogInitPromise;
11
- }
12
-
13
- const config = state.config;
14
- state.posthogInitPromise = import("posthog-js")
15
- .then((module) => {
16
- const posthog = module.default ?? module;
17
- // Consent can flip while posthog-js is still downloading. Opting out in the init
18
- // options stops the pageview init would send before a later opt_out_capturing() call.
19
- const options = applyConsentToInitOptions(buildInitOptions(config), hasConsent());
20
- posthog.init(config.clientKey, options);
21
- if (!hasConsent()) {
22
- posthog.opt_out_capturing();
23
- }
24
- posthog.register({ product: config.product, surface: config.surface });
25
- state.posthogInstance = posthog;
26
- return posthog;
27
- })
28
- .catch((error) => {
29
- // A rejected init left in place would fail every later event without retrying.
30
- state.posthogInitPromise = null;
31
- throw error;
32
- });
33
-
34
- return state.posthogInitPromise;
35
- }
36
-
37
- /**
38
- * Copies init options and, when consent is absent, sets the flag that keeps init itself
39
- * from capturing. Pure so the smoke test can lock the consent decision without a browser.
40
- */
41
- export function applyConsentToInitOptions(options, consentGranted) {
42
- if (consentGranted) {
43
- return options;
44
- }
45
-
46
- return { ...options, opt_out_capturing_by_default: true };
47
- }
48
-
49
- /**
50
- * The posthog-js init options for a config. Pure, so the smoke test can assert the mapping
51
- * without a browser: replay stays off unless the product enabled it, and the masking choices
52
- * ride through only when it is on.
53
- */
54
- export function buildInitOptions(config) {
55
- const recording = config.sessionRecording ?? { enabled: false };
56
- const options = {
57
- api_host: config.host,
58
- autocapture: false,
59
- capture_pageview: true,
60
- persistence: "localStorage",
61
- disable_session_recording: !recording.enabled,
62
- // The package exposes none of these, so each is only extra scripts and requests per page.
63
- disable_surveys: true,
64
- disable_product_tours: true,
65
- disable_conversations: true,
66
- // Replay can be gated on a linked flag, so flags stay on whenever replay is.
67
- advanced_disable_feature_flags: !recording.enabled,
68
- };
69
- if (recording.enabled) {
70
- options.session_recording = {
71
- maskAllInputs: recording.maskAllInputs ?? true,
72
- ...(recording.maskTextSelector ? { maskTextSelector: recording.maskTextSelector } : {}),
73
- };
74
- }
75
- return options;
76
- }
@@ -1,35 +0,0 @@
1
- import { getAttribution } from "./attribution.js";
2
- import { resolveChannel } from "./channels.js";
3
- import { getConsent } from "./consent.js";
4
- import { events } from "./vocabulary.js";
5
-
6
- const CATALOGUE = new Set(Object.values(events));
7
-
8
- /**
9
- * Pushes a catalogue event (lead_submitted, whatsapp_click, calculator_use, phone_click...) to
10
- * dataLayer for a Tag Manager trigger, carrying the visitor's first and last acquisition channel
11
- * and ai_source. Nothing is pushed until the visitor has granted analytics consent, the same gate
12
- * PostHog capture follows; a name outside the catalogue is dropped. Returns whether it was pushed.
13
- */
14
- export function trackSiteEvent(name, props = {}) {
15
- if (!CATALOGUE.has(name) || !getConsent().analytics) {
16
- return false;
17
- }
18
-
19
- const attribution = getAttribution();
20
- const payload = { ...props, event: name };
21
- for (const [prefix, touch] of [
22
- ["first", attribution?.firstTouch],
23
- ["last", attribution?.lastTouch],
24
- ]) {
25
- if (touch) {
26
- payload[`${prefix}_touch_channel`] = resolveChannel(touch);
27
- if (touch.aiSource) {
28
- payload[`${prefix}_touch_ai_source`] = touch.aiSource;
29
- }
30
- }
31
- }
32
- window.dataLayer = window.dataLayer || [];
33
- window.dataLayer.push(payload);
34
- return true;
35
- }
package/src/site.js DELETED
@@ -1,9 +0,0 @@
1
- // Public websites: consent, marketing tags and attribution, without PostHog. The main entry
2
- // re-exports all of this for portals that also capture product events.
3
- export { captureAttribution, getAttribution } from "./attribution.js";
4
- export { channels, resolveAiSource, resolveChannel } from "./channels.js";
5
- export { CONSENT_CHANGE_EVENT, CONSENT_COOKIE_NAME, getConsent, initConsent, onConsentChange, saveConsent } from "./consent.js";
6
- export { requiresOptIn } from "./consent-regions.js";
7
- export { trackSiteEvent } from "./site-events.js";
8
- export { loadTags } from "./tags.js";
9
- export { events } from "./vocabulary.js";