-
Notifications
You must be signed in to change notification settings - Fork 14
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: introduce plugin system to offset non-essenial logic #348
base: main
Are you sure you want to change the base?
Changes from 5 commits
6066f0b
8642557
d5aa373
4613b00
9726dc8
068c5c5
95a653e
5c9adbc
1c21afd
d88b010
7d89e9f
b3f32eb
93b4537
c73eb31
f77d736
8964f80
8ee7b04
29e7138
3181698
9a9608b
08d9933
b2e0b5d
ab954d7
351876c
1f1fa8f
aacaf8d
be228fa
a8fa936
f531f09
e3ad482
8bf19f4
f1bf66a
b030130
9603927
179dd62
bbae3e7
65ee6dc
0a643d5
361afee
dfc1eea
325e9be
10decef
f3a3385
2bb5f45
fa1ac2b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,26 +14,73 @@ | |
import { KNOWN_PROPERTIES, DEFAULT_TRACKING_EVENTS } from './defaults.js'; | ||
import { urlSanitizers } from './utils.js'; | ||
import { targetSelector, sourceSelector } from './dom.js'; | ||
import { | ||
addAdsParametersTracking, | ||
addCookieConsentTracking, | ||
addEmailParameterTracking, | ||
addUTMParametersTracking, | ||
} from './martech.js'; | ||
import { fflags } from './fflags.js'; | ||
|
||
const { sampleRUM, queue, isSelected } = (window.hlx && window.hlx.rum) ? window.hlx.rum | ||
/* c8 ignore next */ : {}; | ||
|
||
const createMO = (cb) => (window.MutationObserver ? new MutationObserver(cb) | ||
/* c8 ignore next */ : {}); | ||
|
||
// blocks mutation observer | ||
// eslint-disable-next-line no-use-before-define, max-len | ||
const blocksMO = window.MutationObserver ? new MutationObserver(blocksMCB) | ||
/* c8 ignore next */ : {}; | ||
// eslint-disable-next-line no-use-before-define | ||
const blocksMO = createMO(blocksMCB); | ||
|
||
// media mutation observer | ||
// eslint-disable-next-line no-use-before-define, max-len | ||
const mediaMO = window.MutationObserver ? new MutationObserver(mediaMCB) | ||
/* c8 ignore next */ : {}; | ||
// eslint-disable-next-line no-use-before-define | ||
const mediaMO = createMO(mediaMCB); | ||
|
||
// Check for the presence of URL parameters | ||
const hasUrlParameters = ({ urlParameters }) => urlParameters.keys().length > 0; | ||
// Check for the presence of a given cookie | ||
const hasCookieKey = (key) => () => document.cookie.split(';').map((c) => c.trim()).some((cookie) => cookie.startsWith(`${key}=`)); | ||
|
||
const pluginBasePath = new URL('.rum/@adobe/helix-rum-enhancer@^2/src/plugins', sampleRUM.baseURL).href; | ||
|
||
const PLUGINS = { | ||
cwv: `${pluginBasePath}/cwv.js`, | ||
navigation: `${pluginBasePath}/navigation.js`, | ||
ramboz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// Interactive elements | ||
form: { url: `${pluginBasePath}/form.js`, condition: () => document.body.querySelector('form') }, | ||
ramboz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
video: { url: `${pluginBasePath}/video.js`, condition: () => document.body.querySelector('video') }, | ||
// Martech | ||
ads: { url: `${pluginBasePath}/ads.js`, condition: hasUrlParameters }, | ||
email: { url: `${pluginBasePath}/email.js`, condition: hasUrlParameters }, | ||
onetrust: { url: `${pluginBasePath}/onetrust.js`, condition: () => hasCookieKey('OptanonAlertBoxClosed') }, | ||
ramboz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
utm: { url: `${pluginBasePath}/utm.js`, condition: hasUrlParameters }, | ||
ramboz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}; | ||
|
||
const allPlugins = { | ||
...(window.RUM_PLUGINS || {}), | ||
ramboz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
PLUGINS, | ||
}; | ||
|
||
const PLUGIN_PARAMETERS = { | ||
context: document.body, | ||
fflags, | ||
sampleRUM, | ||
sourceSelector, | ||
targetSelector, | ||
}; | ||
|
||
const pluginCache = new Map(); | ||
|
||
async function loadPlugin(key, params) { | ||
const plugin = allPlugins[key]; | ||
if (!plugin) return Promise.reject(new Error(`Plugin ${key} not found`)); | ||
const usp = new URLSearchParams(window.location.search); | ||
if (!pluginCache.has(key) && plugin.condition && !plugin.condition({ urlParameters: usp })) { | ||
return Promise.reject(new Error(`Condition for plugin ${key} not met`)); | ||
ramboz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
if (!pluginCache.has(key)) { | ||
try { | ||
pluginCache.set(key, import(`${plugin.url || plugin}`)); | ||
} catch (e) { | ||
return Promise.reject(new Error(`Error loading plugin ${key}: ${e.message}`)); | ||
} | ||
} | ||
return pluginCache.get(key).then((p) => p.default && p.default(params)); | ||
} | ||
|
||
function trackCheckpoint(checkpoint, data, t) { | ||
const { weight, id } = window.hlx.rum; | ||
|
@@ -64,99 +111,6 @@ function processQueue() { | |
} | ||
} | ||
|
||
function addCWVTracking() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Moved to |
||
setTimeout(() => { | ||
try { | ||
const cwvScript = new URL('.rum/web-vitals/dist/web-vitals.iife.js', sampleRUM.baseURL).href; | ||
if (document.querySelector(`script[src="${cwvScript}"]`)) { | ||
// web vitals script has been loaded already | ||
return; | ||
} | ||
const script = document.createElement('script'); | ||
script.src = cwvScript; | ||
script.onload = () => { | ||
const storeCWV = (measurement) => { | ||
const data = { cwv: {} }; | ||
data.cwv[measurement.name] = measurement.value; | ||
if (measurement.name === 'LCP' && measurement.entries.length > 0) { | ||
const { element } = measurement.entries.pop(); | ||
data.target = targetSelector(element); | ||
data.source = sourceSelector(element) || (element && element.outerHTML.slice(0, 30)); | ||
} | ||
sampleRUM('cwv', data); | ||
}; | ||
|
||
const isEager = (metric) => ['CLS', 'LCP'].includes(metric); | ||
|
||
// When loading `web-vitals` using a classic script, all the public | ||
// methods can be found on the `webVitals` global namespace. | ||
['INP', 'TTFB', 'CLS', 'LCP'].forEach((metric) => { | ||
const metricFn = window.webVitals[`on${metric}`]; | ||
if (typeof metricFn === 'function') { | ||
let opts = {}; | ||
fflags.enabled('eagercwv', () => { | ||
opts = { reportAllChanges: isEager(metric) }; | ||
}); | ||
metricFn(storeCWV, opts); | ||
} | ||
}); | ||
}; | ||
document.head.appendChild(script); | ||
/* c8 ignore next 3 */ | ||
} catch (error) { | ||
// something went wrong | ||
} | ||
}, 2000); // wait for delayed | ||
} | ||
|
||
function addNavigationTracking() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Moved to |
||
// enter checkpoint when referrer is not the current page url | ||
const navigate = (source, type, redirectCount) => { | ||
// target can be 'visible', 'hidden' (background tab) or 'prerendered' (speculation rules) | ||
const payload = { source, target: document.visibilityState }; | ||
/* c8 ignore next 13 */ | ||
// prerendering cannot be tested yet with headless browsers | ||
if (document.prerendering) { | ||
// listen for "activation" of the current pre-rendered page | ||
document.addEventListener('prerenderingchange', () => { | ||
// pre-rendered page is now "activated" | ||
payload.target = 'prerendered'; | ||
sampleRUM('navigate', payload); // prerendered navigation | ||
}, { | ||
once: true, | ||
}); | ||
if (type === 'navigate') { | ||
sampleRUM('prerender', payload); // prerendering page | ||
} | ||
} else if (type === 'reload' || source === window.location.href) { | ||
sampleRUM('reload', payload); | ||
} else if (type && type !== 'navigate') { | ||
sampleRUM(type, payload); // back, forward, prerender, etc. | ||
} else if (source && window.location.origin === new URL(source).origin) { | ||
sampleRUM('navigate', payload); // internal navigation | ||
} else { | ||
sampleRUM('enter', payload); // enter site | ||
} | ||
fflags.enabled('redirect', () => { | ||
const from = new URLSearchParams(window.location.search).get('redirect_from'); | ||
if (redirectCount || from) { | ||
sampleRUM('redirect', { source: from, target: redirectCount || 1 }); | ||
} | ||
}); | ||
}; | ||
|
||
const processed = new Set(); // avoid processing duplicate types | ||
new PerformanceObserver((list) => list | ||
.getEntries() | ||
.filter(({ type }) => !processed.has(type)) | ||
.map((e) => [e, processed.add(e.type)]) | ||
.map(([e]) => navigate( | ||
window.hlx.referrer || document.referrer, | ||
e.type, | ||
e.redirectCount, | ||
))).observe({ type: 'navigation', buffered: true }); | ||
} | ||
|
||
function addLoadResourceTracking() { | ||
const observer = new PerformanceObserver((list) => { | ||
try { | ||
|
@@ -211,8 +165,6 @@ function getIntersectionObsever(checkpoint) { | |
if (!window.IntersectionObserver) { | ||
return null; | ||
} | ||
activateBlocksMO(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Moved down |
||
activateMediaMO(); | ||
const observer = new IntersectionObserver((entries) => { | ||
try { | ||
entries | ||
|
@@ -251,28 +203,6 @@ function addViewMediaTracking(parent) { | |
} | ||
} | ||
|
||
function addFormTracking(parent) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Moved to |
||
activateBlocksMO(); | ||
activateMediaMO(); | ||
parent.querySelectorAll('form').forEach((form) => { | ||
form.addEventListener('submit', (e) => sampleRUM('formsubmit', { target: targetSelector(e.target), source: sourceSelector(e.target) }), { once: true }); | ||
let lastSource; | ||
form.addEventListener('change', (e) => { | ||
const source = sourceSelector(e.target); | ||
if (source !== lastSource) { | ||
sampleRUM('fill', { source }); | ||
lastSource = source; | ||
} | ||
}); | ||
form.addEventListener('focusin', (e) => { | ||
if (['INPUT', 'TEXTAREA', 'SELECT', 'BUTTON'].includes(e.target.tagName) | ||
|| e.target.getAttribute('contenteditable') === 'true') { | ||
sampleRUM('click', { source: sourceSelector(e.target) }); | ||
} | ||
}); | ||
}); | ||
} | ||
|
||
function addObserver(ck, fn, block) { | ||
return DEFAULT_TRACKING_EVENTS.includes(ck) && fn(block); | ||
} | ||
|
@@ -284,7 +214,7 @@ function blocksMCB(mutations) { | |
.filter((m) => m.type === 'attributes' && m.attributeName === 'data-block-status') | ||
.filter((m) => m.target.dataset.blockStatus === 'loaded') | ||
.forEach((m) => { | ||
addObserver('form', addFormTracking, m.target); | ||
addObserver('form', (el) => loadPlugin('form', { ...PLUGIN_PARAMETERS, context: el }), m.target); | ||
ramboz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
addObserver('viewblock', addViewBlockTracking, m.target); | ||
}); | ||
} | ||
|
@@ -299,6 +229,9 @@ function mediaMCB(mutations) { | |
} | ||
|
||
function addTrackingFromConfig() { | ||
activateBlocksMO(); | ||
activateMediaMO(); | ||
|
||
let lastSource; | ||
let lastTarget; | ||
document.addEventListener('click', (event) => { | ||
|
@@ -310,16 +243,16 @@ function addTrackingFromConfig() { | |
lastTarget = target; | ||
} | ||
}); | ||
addCWVTracking(); | ||
addFormTracking(window.document.body); | ||
addNavigationTracking(); | ||
|
||
// Core tracking | ||
addLoadResourceTracking(); | ||
addUTMParametersTracking(sampleRUM); | ||
addViewBlockTracking(window.document.body); | ||
addViewMediaTracking(window.document.body); | ||
addCookieConsentTracking(sampleRUM); | ||
addAdsParametersTracking(sampleRUM); | ||
addEmailParameterTracking(sampleRUM); | ||
addViewBlockTracking(document.body); | ||
addViewMediaTracking(document.body); | ||
|
||
// Tracking extensions | ||
Object.keys(allPlugins) | ||
.forEach((key) => loadPlugin(key, PLUGIN_PARAMETERS)); | ||
|
||
fflags.enabled('language', () => { | ||
const target = navigator.language; | ||
const source = document.documentElement.lang; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
/* | ||
* Copyright 2025 Adobe. All rights reserved. | ||
* This file is licensed to you under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. You may obtain a copy | ||
* of the License at http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software distributed under | ||
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS | ||
* OF ANY KIND, either express or implied. See the License for the specific language | ||
* governing permissions and limitations under the License. | ||
*/ | ||
export function addAdsParametersTracking({ sampleRUM }) { | ||
const networks = { | ||
google: /gclid|gclsrc|wbraid|gbraid/, | ||
doubleclick: /dclid/, | ||
microsoft: /msclkid/, | ||
facebook: /fb(cl|ad_|pxl_)id/, | ||
twitter: /tw(clid|src|term)/, | ||
linkedin: /li_fat_id/, | ||
pinterest: /epik/, | ||
tiktok: /ttclid/, | ||
}; | ||
const params = Array.from(new URLSearchParams(window.location.search).keys()); | ||
Object.entries(networks).forEach(([network, regex]) => { | ||
params.filter((param) => regex.test(param)).forEach((param) => sampleRUM('paid', { source: network, target: param })); | ||
}); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
/* | ||
* Copyright 2025 Adobe. All rights reserved. | ||
* This file is licensed to you under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. You may obtain a copy | ||
* of the License at http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software distributed under | ||
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS | ||
* OF ANY KIND, either express or implied. See the License for the specific language | ||
* governing permissions and limitations under the License. | ||
*/ | ||
export default function addCWVTracking({ | ||
sampleRUM, sourceSelector, targetSelector, fflags, | ||
}) { | ||
setTimeout(() => { | ||
try { | ||
const cwvScript = new URL('.rum/web-vitals/dist/web-vitals.iife.js', sampleRUM.baseURL).href; | ||
if (document.querySelector(`script[src="${cwvScript}"]`)) { | ||
// web vitals script has been loaded already | ||
return; | ||
} | ||
const script = document.createElement('script'); | ||
script.src = cwvScript; | ||
script.onload = () => { | ||
const storeCWV = (measurement) => { | ||
const data = { cwv: {} }; | ||
data.cwv[measurement.name] = measurement.value; | ||
if (measurement.name === 'LCP' && measurement.entries.length > 0) { | ||
const { element } = measurement.entries.pop(); | ||
data.target = targetSelector(element); | ||
data.source = sourceSelector(element) || (element && element.outerHTML.slice(0, 30)); | ||
} | ||
sampleRUM('cwv', data); | ||
}; | ||
|
||
const isEager = (metric) => ['CLS', 'LCP'].includes(metric); | ||
|
||
// When loading `web-vitals` using a classic script, all the public | ||
// methods can be found on the `webVitals` global namespace. | ||
['INP', 'TTFB', 'CLS', 'LCP'].forEach((metric) => { | ||
const metricFn = window.webVitals[`on${metric}`]; | ||
if (typeof metricFn === 'function') { | ||
let opts = {}; | ||
fflags.enabled('eagercwv', () => { | ||
opts = { reportAllChanges: isEager(metric) }; | ||
}); | ||
metricFn(storeCWV, opts); | ||
} | ||
}); | ||
}; | ||
document.head.appendChild(script); | ||
/* c8 ignore next 3 */ | ||
} catch (error) { | ||
// something went wrong | ||
} | ||
}, 2000); // wait for delayed | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
/* | ||
* Copyright 2025 Adobe. All rights reserved. | ||
* This file is licensed to you under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. You may obtain a copy | ||
* of the License at http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software distributed under | ||
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS | ||
* OF ANY KIND, either express or implied. See the License for the specific language | ||
* governing permissions and limitations under the License. | ||
*/ | ||
export function addEmailParameterTracking({ sampleRUM }) { | ||
const networks = { | ||
mailchimp: /mc_(c|e)id/, | ||
marketo: /mkt_tok/, | ||
}; | ||
const params = Array.from(new URLSearchParams(window.location.search).keys()); | ||
Object.entries(networks).forEach(([network, regex]) => { | ||
params.filter((param) => regex.test(param)).forEach((param) => sampleRUM('email', { source: network, target: param })); | ||
}); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Moved to
martech.js
&onetrust.js