Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ module.exports = {
rules: {
'import/extensions': [2, 'ignorePackages'],
'import/prefer-default-export': 0,
'no-underscore-dangle': ['error', { allow: ['_reactRootContainer'] }],
},
globals: {
__rootdir: true,
Expand Down
1 change: 1 addition & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
npx lint-staged
npm test
34 changes: 32 additions & 2 deletions modules/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

import { KNOWN_PROPERTIES, DEFAULT_TRACKING_EVENTS } from './defaults.js';
import { fflags } from './fflags.js';
import { urlSanitizers } from './utils.js';
import { urlSanitizers, getReactContainers, isReactApp } from './utils.js';
import { targetSelector, sourceSelector } from './dom.js';

const { sampleRUM, queue, isSelected } = (window.hlx && window.hlx.rum) ? window.hlx.rum : {};
Expand Down Expand Up @@ -214,6 +214,33 @@ function addViewMediaTracking(parent) {
}
}

function addReactMediaTracking(parent) {
const reactDivs = getReactContainers(parent);
const mediaobserver = getIntersectionObsever('viewmedia');
const observer = new MutationObserver((mutationsList) => {
for (const mutation of mutationsList) {
if (mutation.type === 'childList') {
for (const node of mutation.addedNodes) {
// improve performance by poking with querySelector first
if (node.querySelector && node.querySelector('img, video, audio, iframe')) {
node?.querySelectorAll('img, video, audio, iframe')?.forEach((m) => {
const element = m;
if (!element.dataset.withObserver) {
// not so nice as we leave trace in the host's DOM
element.dataset.withObserver = 'true';
mediaobserver.observe(element);
}
});
}
}
}
}
});
reactDivs.forEach((div) => {
observer.observe(div, { childList: true, subtree: true });
});
}

function addUTMParametersTracking() {
const usp = new URLSearchParams(window.location.search);
[...usp.entries()]
Expand Down Expand Up @@ -320,7 +347,10 @@ function addTrackingFromConfig() {
loadresource: () => addLoadResourceTracking(),
utm: () => addUTMParametersTracking(),
viewblock: () => addViewBlockTracking(window.document.body),
viewmedia: () => addViewMediaTracking(window.document.body),
viewmedia: () => {
addViewMediaTracking(window.document.body);
if (isReactApp()) addReactMediaTracking(window.document.body);
},
consent: () => addCookieConsentTracking(),
paid: () => addAdsParametersTracking(),
email: () => addEmailParameterTracking(),
Expand Down
25 changes: 25 additions & 0 deletions modules/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,28 @@ export const urlSanitizers = {
return `${u.origin}${u.pathname}`;
},
};

/**
* getReactContainers
* @param {DOMElement} container The DOM element to search for React containers
* @returns {Array} Array of DOM elements (if more than one) used to bootstrap React
*/
export const getReactContainers = (container) => {
const reactElement = container.querySelector('[data-reactroot], [data-reactid]');
if (reactElement) return [reactElement];
return Array.from(container.querySelectorAll('*')).filter((e) => e._reactRootContainer !== undefined || Object.keys(e).some((k) => k.startsWith('__reactContainer')));
};

/**
* Determines if the current page is running a React application
* by inspecting React-related elements in the DOM.
* @returns {bool}
*/
export const isReactApp = () => {
// https://gist.github.com/rambabusaravanan/1d594bd8d1c3153bc8367753b17d074b
if (!!window.React
|| !!document.querySelector('[data-reactroot], [data-reactid]')
|| Array.from(document.querySelectorAll('*')).some((e) => e._reactRootContainer !== undefined || Object.keys(e).some((k) => k.startsWith('__reactContainer')))
) return true;
return false;
};
66 changes: 66 additions & 0 deletions test/it/img.react.test.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<html>

<head>
<title>Test Runner</title>
</head>

<body>
<script type="module">
window.React = {};
import { runTests } from '@web/test-runner-mocha';
import { expect } from '@esm-bundle/chai';

runTests(async () => {
describe('HTML IMG React Tests', () => {

it('rum enhancer reports image, even if added later', async () => {
let called = false;
const imagesSeen = [];
window.hlx = {
rum: {
sampleRUM: (checkpoint, data) => {
called = true;
if (checkpoint === 'viewmedia') {
imagesSeen.push(data.target);
console.log('viewmedia', data.target);
}
}
}
};

const script = document.createElement('script');
script.src = new URL('/modules/index.js', window.location).href;
script.type = 'module';
document.head.appendChild(script);

const reactDiv = document.getElementById('react');

await new Promise((resolve) => {
script.onload = resolve;
});

await new Promise((resolve) => {
setTimeout(resolve, 700); //webkit friendly delay
});
expect(called).to.be.true;
expect(imagesSeen).to.deep.equal(['https://www.example.com/loadstart.jpg']);

reactDiv.innerHTML = '<div><img src="https://www.example.com/loadlater.jpg" alt="example image"></div>';

await new Promise((resolve) => {
setTimeout(resolve, 700); //webkit friendly delay
});
expect(called).to.be.true;
expect(imagesSeen).to.deep.equal(['https://www.example.com/loadstart.jpg','https://www.example.com/loadlater.jpg']);
});

});
});
</script>
<div id="react" data-reactroot="yes">
<img src="https://www.example.com/loadstart.jpg" alt="example image">
</div>

</body>

</html>