Skip to content

Commit 38a3a65

Browse files
authored
feat: introduce plugin system to offset non-essenial logic
Introduce a plugin system to offset some of the non-essential logic and reduce the core library's overall file size. At a high level, we want: - a minimalistic probing logic that checks the markup or page context for some "markers" that indicate additional optional logic is needed - a loader script that pulls in the required script for the particular plugin - the plugin should have a default export that gets the required context passed as input
2 parents 266b93b + 8eb271a commit 38a3a65

12 files changed

+779
-249
lines changed

modules/index.js

+64-95
Original file line numberDiff line numberDiff line change
@@ -14,26 +14,65 @@
1414
import { KNOWN_PROPERTIES, DEFAULT_TRACKING_EVENTS } from './defaults.js';
1515
import { urlSanitizers } from './utils.js';
1616
import { targetSelector, sourceSelector } from './dom.js';
17-
import {
18-
addAdsParametersTracking,
19-
addCookieConsentTracking,
20-
addEmailParameterTracking,
21-
addUTMParametersTracking,
22-
} from './martech.js';
2317
import { fflags } from './fflags.js';
2418

2519
const { sampleRUM, queue, isSelected } = (window.hlx && window.hlx.rum) ? window.hlx.rum
2620
/* c8 ignore next */ : {};
2721

28-
// blocks mutation observer
29-
// eslint-disable-next-line no-use-before-define, max-len
30-
const blocksMO = window.MutationObserver ? new MutationObserver(blocksMCB)
31-
/* c8 ignore next */ : {};
22+
const createMO = (cb) => (window.MutationObserver ? new MutationObserver(cb)
23+
/* c8 ignore next */ : {});
3224

33-
// media mutation observer
34-
// eslint-disable-next-line no-use-before-define, max-len
35-
const mediaMO = window.MutationObserver ? new MutationObserver(mediaMCB)
36-
/* c8 ignore next */ : {};
25+
// blocks & media mutation observer
26+
// eslint-disable-next-line no-use-before-define
27+
const [blocksMO, mediaMO] = [blocksMCB, mediaMCB].map(createMO);
28+
29+
// Check for the presence of a given cookie
30+
const hasCookieKey = (key) => () => document.cookie.split(';').some((c) => c.trim().startsWith(`${key}=`));
31+
32+
// Set the base path for the plugins
33+
const pluginBasePath = new URL(document.currentScript.src).href.replace(/index(\.map)?\.js/, 'plugins');
34+
35+
const PLUGINS = {
36+
cwv: `${pluginBasePath}/cwv.js`,
37+
// Interactive elements
38+
form: { url: `${pluginBasePath}/form.js`, when: () => document.querySelector('form'), isBlockDependent: true },
39+
video: { url: `${pluginBasePath}/video.js`, when: () => document.querySelector('video'), isBlockDependent: true },
40+
// Martech
41+
martech: { url: `${pluginBasePath}/martech.js`, when: ({ urlParameters }) => urlParameters.size > 0 },
42+
onetrust: { url: `${pluginBasePath}/onetrust.js`, when: () => (document.querySelector('#onetrust-consent-sdk') || hasCookieKey('OptanonAlertBoxClosed')), isBlockDependent: true },
43+
// test: broken-plugin
44+
};
45+
46+
const PLUGIN_PARAMETERS = {
47+
context: document.body,
48+
fflags,
49+
sampleRUM,
50+
sourceSelector,
51+
targetSelector,
52+
};
53+
54+
const pluginCache = new Map();
55+
56+
function loadPlugin(key, params) {
57+
const plugin = PLUGINS[key];
58+
const usp = new URLSearchParams(window.location.search);
59+
if (!pluginCache.has(key) && plugin.when && !plugin.when({ urlParameters: usp })) {
60+
return null;
61+
}
62+
if (!pluginCache.has(key)) {
63+
pluginCache.set(key, import(`${plugin.url || plugin}`));
64+
}
65+
const pluginLoadPromise = pluginCache.get(key);
66+
return pluginLoadPromise
67+
.then((p) => (p.default && p.default(params)) || (typeof p === 'function' && p(params)))
68+
.catch(() => { /* silent plugin error catching */ });
69+
}
70+
71+
function loadPlugins(filter = () => true, params = PLUGIN_PARAMETERS) {
72+
Object.entries(PLUGINS)
73+
.filter(([, plugin]) => filter(plugin))
74+
.map(([key]) => loadPlugin(key, params));
75+
}
3776
/**
3877
* Maximum number of events. The first call will be made by rum-js,
3978
* leaving 1023 events for the enhancer to track
@@ -70,55 +109,9 @@ function processQueue() {
70109
}
71110
}
72111

73-
function addCWVTracking() {
74-
setTimeout(() => {
75-
try {
76-
const cwvScript = new URL('.rum/web-vitals/dist/web-vitals.iife.js', sampleRUM.baseURL).href;
77-
if (document.querySelector(`script[src="${cwvScript}"]`)) {
78-
// web vitals script has been loaded already
79-
return;
80-
}
81-
const script = document.createElement('script');
82-
script.src = cwvScript;
83-
script.onload = () => {
84-
const storeCWV = (measurement) => {
85-
const data = { cwv: {} };
86-
data.cwv[measurement.name] = measurement.value;
87-
if (measurement.name === 'LCP' && measurement.entries.length > 0) {
88-
const { element } = measurement.entries.pop();
89-
data.target = targetSelector(element);
90-
data.source = sourceSelector(element) || (element && element.outerHTML.slice(0, 30));
91-
}
92-
sampleRUM('cwv', data);
93-
};
94-
95-
const isEager = (metric) => ['CLS', 'LCP'].includes(metric);
96-
97-
// When loading `web-vitals` using a classic script, all the public
98-
// methods can be found on the `webVitals` global namespace.
99-
['INP', 'TTFB', 'CLS', 'LCP'].forEach((metric) => {
100-
const metricFn = window.webVitals[`on${metric}`];
101-
if (typeof metricFn === 'function') {
102-
let opts = {};
103-
fflags.enabled('eagercwv', () => {
104-
opts = { reportAllChanges: isEager(metric) };
105-
});
106-
metricFn(storeCWV, opts);
107-
}
108-
});
109-
};
110-
document.head.appendChild(script);
111-
/* c8 ignore next 3 */
112-
} catch (error) {
113-
// something went wrong
114-
}
115-
}, 2000); // wait for delayed
116-
}
117-
118112
function addNavigationTracking() {
119113
// enter checkpoint when referrer is not the current page url
120114
const navigate = (source, type, redirectCount) => {
121-
// target can be 'visible', 'hidden' (background tab) or 'prerendered' (speculation rules)
122115
const payload = { source, target: document.visibilityState };
123116
/* c8 ignore next 13 */
124117
// prerendering cannot be tested yet with headless browsers
@@ -218,8 +211,6 @@ function getIntersectionObsever(checkpoint) {
218211
if (!window.IntersectionObserver) {
219212
return null;
220213
}
221-
activateBlocksMO();
222-
activateMediaMO();
223214
const observer = new IntersectionObserver((entries) => {
224215
try {
225216
entries
@@ -258,30 +249,6 @@ function addViewMediaTracking(parent) {
258249
}
259250
}
260251

261-
function addFormTracking(parent) {
262-
activateBlocksMO();
263-
activateMediaMO();
264-
parent.querySelectorAll('form').forEach((form) => {
265-
form.addEventListener('submit', (e) => sampleRUM('formsubmit', { target: targetSelector(e.target), source: sourceSelector(e.target) }), { once: true });
266-
let lastSource;
267-
form.addEventListener('change', (e) => {
268-
if (e.target.checkVisibility()) {
269-
const source = sourceSelector(e.target);
270-
if (source !== lastSource) {
271-
sampleRUM('fill', { source });
272-
lastSource = source;
273-
}
274-
}
275-
});
276-
form.addEventListener('focusin', (e) => {
277-
if (['INPUT', 'TEXTAREA', 'SELECT', 'BUTTON'].includes(e.target.tagName)
278-
|| e.target.getAttribute('contenteditable') === 'true') {
279-
sampleRUM('click', { source: sourceSelector(e.target) });
280-
}
281-
});
282-
});
283-
}
284-
285252
function addObserver(ck, fn, block) {
286253
return DEFAULT_TRACKING_EVENTS.includes(ck) && fn(block);
287254
}
@@ -293,7 +260,7 @@ function blocksMCB(mutations) {
293260
.filter((m) => m.type === 'attributes' && m.attributeName === 'data-block-status')
294261
.filter((m) => m.target.dataset.blockStatus === 'loaded')
295262
.forEach((m) => {
296-
addObserver('form', addFormTracking, m.target);
263+
addObserver('form', (el) => loadPlugins((p) => p.isBlockDependent, { ...PLUGIN_PARAMETERS, context: el }), m.target);
297264
addObserver('viewblock', addViewBlockTracking, m.target);
298265
});
299266
}
@@ -308,20 +275,22 @@ function mediaMCB(mutations) {
308275
}
309276

310277
function addTrackingFromConfig() {
278+
activateBlocksMO();
279+
activateMediaMO();
280+
311281
document.addEventListener('click', (event) => {
312282
sampleRUM('click', { target: targetSelector(event.target), source: sourceSelector(event.target) });
313283
});
314284

315-
addCWVTracking();
316-
addFormTracking(window.document.body);
285+
// Core tracking
317286
addNavigationTracking();
318287
addLoadResourceTracking();
319-
addUTMParametersTracking(sampleRUM);
320-
addViewBlockTracking(window.document.body);
321-
addViewMediaTracking(window.document.body);
322-
addCookieConsentTracking(sampleRUM);
323-
addAdsParametersTracking(sampleRUM);
324-
addEmailParameterTracking(sampleRUM);
288+
addViewBlockTracking(document.body);
289+
addViewMediaTracking(document.body);
290+
291+
// Tracking extensions
292+
loadPlugins();
293+
325294
fflags.enabled('language', () => {
326295
const target = navigator.language;
327296
const source = document.documentElement.lang;

0 commit comments

Comments
 (0)