@adobe/helix-rum-enhancer 2.25.0 → 2.26.0

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/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ # [2.26.0](https://github.com/adobe/helix-rum-enhancer/compare/v2.25.0...v2.26.0) (2024-10-16)
2
+
3
+
4
+ ### Features
5
+
6
+ * handle pre-speculation rules and rendering ([#301](https://github.com/adobe/helix-rum-enhancer/issues/301)) ([196d404](https://github.com/adobe/helix-rum-enhancer/commit/196d404f3f90a247a223e8ddc7c667592a87319f))
7
+
1
8
  # [2.25.0](https://github.com/adobe/helix-rum-enhancer/compare/v2.24.0...v2.25.0) (2024-10-03)
2
9
 
3
10
 
package/modules/dom.js CHANGED
@@ -9,99 +9,99 @@
9
9
  * OF ANY KIND, either express or implied. See the License for the specific language
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
- export const getTargetValue = (element) => element.getAttribute('data-rum-target') || element.getAttribute('href')
13
- || element.currentSrc || element.getAttribute('src') || element.dataset.action || element.action;
12
+ export const getTargetValue = (el) => el.getAttribute('data-rum-target') || el.getAttribute('href')
13
+ || el.currentSrc || el.getAttribute('src') || el.dataset.action || el.action;
14
14
 
15
- export const targetSelector = (element) => {
15
+ export const targetSelector = (el) => {
16
16
  try {
17
- if (!element) return undefined;
18
- let value = getTargetValue(element);
19
- if (!value && element.tagName !== 'A' && element.closest('a')) {
20
- value = getTargetValue(element.closest('a'));
17
+ if (!el) return undefined;
18
+ let v = getTargetValue(el);
19
+ if (!v && el.tagName !== 'A' && el.closest('a')) {
20
+ v = getTargetValue(el.closest('a'));
21
21
  }
22
- if (value && !value.startsWith('https://')) {
22
+ if (v && !v.startsWith('https://')) {
23
23
  // resolve relative links
24
- value = new URL(value, window.location).href;
24
+ v = new URL(v, window.location).href;
25
25
  }
26
- return value;
26
+ return v;
27
27
  /* c8 ignore next 3 */
28
28
  } catch (error) {
29
29
  return null;
30
30
  }
31
31
  };
32
32
 
33
- function walk(element, checkFn) {
34
- if (!element || element === document.body || element === document.documentElement) {
33
+ function walk(el, checkFn) {
34
+ if (!el || el === document.body || el === document.documentElement) {
35
35
  return undefined;
36
36
  }
37
- const checkValue = checkFn(element);
38
- return checkValue || walk(element.parentElement, checkFn);
37
+ return checkFn(el) || walk(el.parentElement, checkFn);
39
38
  }
40
39
 
41
- function isDialog(element) {
40
+ function isDialog(el) {
42
41
  // doing it well
43
- if (element.tagName === 'DIALOG') return true;
42
+ if (el.tagName === 'DIALOG') return true;
44
43
  // making the best of it
45
- if (element.getAttribute('role') === 'dialog') return true;
46
- if (element.getAttribute('role') === 'alertdialog') return true;
47
- if (element.getAttribute('aria-modal') === 'true') return true;
44
+ if (el.getAttribute('role') === 'dialog') return true;
45
+ if (el.getAttribute('role') === 'alertdialog') return true;
46
+ if (el.getAttribute('aria-modal') === 'true') return true;
48
47
  // doing it wrong
49
- const computedStyle = window.getComputedStyle(element);
50
- return (computedStyle && computedStyle.position === 'fixed' && computedStyle.zIndex > 100);
48
+ const cs = window.getComputedStyle(el);
49
+ return (cs && cs.position === 'fixed' && cs.zIndex > 100);
51
50
  }
52
51
 
53
- function isButton(element) {
54
- if (element.tagName === 'BUTTON') return true;
55
- if (element.tagName === 'INPUT' && element.getAttribute('type') === 'button') return true;
56
- if (element.tagName === 'A') {
57
- const classes = Array.from(element.classList);
52
+ function isButton(el) {
53
+ if (el.tagName === 'BUTTON') return true;
54
+ if (el.tagName === 'INPUT' && el.getAttribute('type') === 'button') return true;
55
+ if (el.tagName === 'A') {
56
+ const classes = Array.from(el.classList);
58
57
  return classes.some((className) => className.match(/button|cta/));
59
58
  }
60
- return element.getAttribute('role') === 'button';
59
+ return el.getAttribute('role') === 'button';
61
60
  }
62
61
 
63
- function getSourceContext(element) {
64
- if (element.closest('form')) {
65
- return `form${element.id ? `#${element.id}` : ''}`;
62
+ function getSourceContext(el) {
63
+ if (el.closest('form')) {
64
+ return `form${el.id ? `#${el.id}` : ''}`;
66
65
  }
67
- const block = element.closest('.block[data-block-name]');
66
+ const block = el.closest('.block[data-block-name]');
68
67
  if (block) return `.${block.getAttribute('data-block-name')}`;
69
- if (walk(element, isDialog)) return 'dialog';
70
- if (element.closest('nav')) return 'nav';
71
- if (element.closest('header')) return 'header';
72
- if (element.closest('footer')) return 'footer';
73
- if (element.closest('aside')) return 'aside';
74
- return (walk(element, (e) => e.id && `#${e.id}`));
68
+ if (walk(el, isDialog)) return 'dialog';
69
+ if (el.closest('nav')) return 'nav';
70
+ if (el.closest('header')) return 'header';
71
+ if (el.closest('footer')) return 'footer';
72
+ if (el.closest('aside')) return 'aside';
73
+ return (walk(el, (e) => e.id && `#${e.id}`));
75
74
  }
76
75
 
77
- function getSourceElement(element) {
78
- if (element.closest('form') && Array.from(element.closest('form').elements).includes(element)) {
79
- return (element.tagName.toLowerCase()
80
- + (['INPUT', 'BUTTON'].includes(element.tagName)
81
- ? `[type='${element.getAttribute('type') || ''}']`
76
+ function getSourceElement(el) {
77
+ const f = el.closest('form');
78
+ if (f && Array.from(f.elements).includes(el)) {
79
+ return (el.tagName.toLowerCase()
80
+ + (['INPUT', 'BUTTON'].includes(el.tagName)
81
+ ? `[type='${el.getAttribute('type') || ''}']`
82
82
  : ''));
83
83
  }
84
- if (walk(element, isButton)) return 'button';
85
- return element.tagName.toLowerCase().match(/^(a|img|video)$/) && element.tagName.toLowerCase();
84
+ if (walk(el, isButton)) return 'button';
85
+ return el.tagName.toLowerCase().match(/^(a|img|video)$/) && el.tagName.toLowerCase();
86
86
  }
87
87
 
88
- function getSourceIdentifier(element) {
89
- if (element.id) return `#${element.id}`;
90
- if (element.getAttribute('data-block-name')) return `.${element.getAttribute('data-block-name')}`;
91
- return (element.classList.length > 0 && `.${element.classList[0]}`);
88
+ function getSourceIdentifier(el) {
89
+ if (el.id) return `#${el.id}`;
90
+ if (el.getAttribute('data-block-name')) return `.${el.getAttribute('data-block-name')}`;
91
+ return (el.classList.length > 0 && `.${el.classList[0]}`);
92
92
  }
93
- export const sourceSelector = (element) => {
93
+ export const sourceSelector = (el) => {
94
94
  try {
95
- if (!element || element === document.body || element === document.documentElement) {
95
+ if (!el || el === document.body || el === document.documentElement) {
96
96
  return undefined;
97
97
  }
98
- if (element.getAttribute('data-rum-source')) {
99
- return element.getAttribute('data-rum-source');
98
+ if (el.getAttribute('data-rum-source')) {
99
+ return el.getAttribute('data-rum-source');
100
100
  }
101
- const context = getSourceContext(element.parentElement) || '';
102
- const elementName = getSourceElement(element) || '';
103
- const identifier = getSourceIdentifier(element) || '';
104
- return `${context} ${elementName}${identifier}`.trim() || `"${element.textContent.substring(0, 10)}"`;
101
+ const ctx = getSourceContext(el.parentElement) || '';
102
+ const name = getSourceElement(el) || '';
103
+ const id = getSourceIdentifier(el) || '';
104
+ return `${ctx} ${name}${id}`.trim() || `"${el.textContent.substring(0, 10)}"`;
105
105
  /* c8 ignore next 3 */
106
106
  } catch (error) {
107
107
  return null;
package/modules/index.js CHANGED
@@ -25,14 +25,14 @@ import { fflags } from './fflags.js';
25
25
  const { sampleRUM, queue, isSelected } = (window.hlx && window.hlx.rum) ? window.hlx.rum
26
26
  /* c8 ignore next */ : {};
27
27
 
28
- const formSubmitListener = (e) => sampleRUM('formsubmit', { target: targetSelector(e.target), source: sourceSelector(e.target) });
29
-
28
+ // blocks mutation observer
30
29
  // eslint-disable-next-line no-use-before-define, max-len
31
- const blocksMutationObserver = window.MutationObserver ? new MutationObserver(blocksMutationsCallback)
30
+ const blocksMO = window.MutationObserver ? new MutationObserver(blocksMCB)
32
31
  /* c8 ignore next */ : {};
33
32
 
33
+ // media mutation observer
34
34
  // eslint-disable-next-line no-use-before-define, max-len
35
- const mediaMutationObserver = window.MutationObserver ? new MutationObserver(mediaMutationsCallback)
35
+ const mediaMO = window.MutationObserver ? new MutationObserver(mediaMCB)
36
36
  /* c8 ignore next */ : {};
37
37
 
38
38
  function trackCheckpoint(checkpoint, data, t) {
@@ -112,9 +112,23 @@ function addCWVTracking() {
112
112
  function addNavigationTracking() {
113
113
  // enter checkpoint when referrer is not the current page url
114
114
  const navigate = (source, type, redirectCount) => {
115
+ // target can be 'visible', 'hidden' (background tab) or 'prerendered' (speculation rules)
115
116
  const payload = { source, target: document.visibilityState };
116
- // reload: same page, navigate: same origin, enter: everything else
117
- if (type === 'reload' || source === window.location.href) {
117
+ /* c8 ignore next 13 */
118
+ // prerendering cannot be tested yet with headless browsers
119
+ if (document.prerendering) {
120
+ // listen for "activation" of the current pre-rendered page
121
+ document.addEventListener('prerenderingchange', () => {
122
+ // pre-rendered page is now "activated"
123
+ payload.target = 'prerendered';
124
+ sampleRUM('navigate', payload); // prerendered navigation
125
+ }, {
126
+ once: true,
127
+ });
128
+ if (type === 'navigate') {
129
+ sampleRUM('prerender', payload); // prerendering page
130
+ }
131
+ } else if (type === 'reload' || source === window.location.href) {
118
132
  sampleRUM('reload', payload);
119
133
  } else if (type && type !== 'navigate') {
120
134
  sampleRUM(type, payload); // back, forward, prerender, etc.
@@ -132,10 +146,10 @@ function addNavigationTracking() {
132
146
  };
133
147
 
134
148
  new PerformanceObserver((list) => list
135
- .getEntries().map((entry) => navigate(
149
+ .getEntries().map((e) => navigate(
136
150
  window.hlx.referrer || document.referrer,
137
- entry.type,
138
- entry.redirectCount,
151
+ e.type,
152
+ e.redirectCount,
139
153
  )))
140
154
  .observe({ type: 'navigation', buffered: true });
141
155
  }
@@ -144,16 +158,16 @@ function addLoadResourceTracking() {
144
158
  const observer = new PerformanceObserver((list) => {
145
159
  try {
146
160
  list.getEntries()
147
- .filter((entry) => !entry.responseStatus || entry.responseStatus < 400)
148
- .filter((entry) => window.location.hostname === new URL(entry.name).hostname)
149
- .filter((entry) => new URL(entry.name).pathname.match('.*(\\.plain\\.html$|\\.json|graphql|api)'))
150
- .forEach((entry) => {
151
- sampleRUM('loadresource', { source: entry.name, target: Math.round(entry.duration) });
161
+ .filter((e) => !e.responseStatus || e.responseStatus < 400)
162
+ .filter((e) => window.location.hostname === new URL(e.name).hostname)
163
+ .filter((e) => new URL(e.name).pathname.match('.*(\\.plain\\.html$|\\.json|graphql|api)'))
164
+ .forEach((e) => {
165
+ sampleRUM('loadresource', { source: e.name, target: Math.round(e.duration) });
152
166
  });
153
167
  list.getEntries()
154
- .filter((entry) => entry.responseStatus === 404)
155
- .forEach((entry) => {
156
- sampleRUM('missingresource', { source: entry.name, target: entry.hostname });
168
+ .filter((e) => e.responseStatus === 404)
169
+ .forEach((e) => {
170
+ sampleRUM('missingresource', { source: e.name, target: e.hostname });
157
171
  });
158
172
  /* c8 ignore next 3 */
159
173
  } catch (error) {
@@ -163,24 +177,26 @@ function addLoadResourceTracking() {
163
177
  observer.observe({ type: 'resource', buffered: true });
164
178
  }
165
179
 
166
- function activateBlocksMutationObserver() {
167
- if (!blocksMutationObserver || blocksMutationObserver.active) {
180
+ // activate blocks mutation observer
181
+ function activateBlocksMO() {
182
+ if (!blocksMO || blocksMO.active) {
168
183
  return;
169
184
  }
170
- blocksMutationObserver.active = true;
171
- blocksMutationObserver.observe(
185
+ blocksMO.active = true;
186
+ blocksMO.observe(
172
187
  document.body,
173
188
  // eslint-disable-next-line object-curly-newline
174
189
  { subtree: true, attributes: true, attributeFilter: ['data-block-status'] },
175
190
  );
176
191
  }
177
192
 
178
- function activateMediaMutationObserver() {
179
- if (!mediaMutationObserver || mediaMutationObserver.active) {
193
+ // activate media mutation observer
194
+ function activateMediaMO() {
195
+ if (!mediaMO || mediaMO.active) {
180
196
  return;
181
197
  }
182
- mediaMutationObserver.active = true;
183
- mediaMutationObserver.observe(
198
+ mediaMO.active = true;
199
+ mediaMO.observe(
184
200
  document.body,
185
201
  // eslint-disable-next-line object-curly-newline
186
202
  { subtree: true, attributes: false, childList: true },
@@ -192,16 +208,16 @@ function getIntersectionObsever(checkpoint) {
192
208
  if (!window.IntersectionObserver) {
193
209
  return null;
194
210
  }
195
- activateBlocksMutationObserver();
196
- activateMediaMutationObserver();
211
+ activateBlocksMO();
212
+ activateMediaMO();
197
213
  const observer = new IntersectionObserver((entries) => {
198
214
  try {
199
215
  entries
200
- .filter((entry) => entry.isIntersecting)
201
- .forEach((entry) => {
202
- observer.unobserve(entry.target); // observe only once
203
- const target = targetSelector(entry.target);
204
- const source = sourceSelector(entry.target);
216
+ .filter((e) => e.isIntersecting)
217
+ .forEach((e) => {
218
+ observer.unobserve(e.target); // observe only once
219
+ const target = targetSelector(e.target);
220
+ const source = sourceSelector(e.target);
205
221
  sampleRUM(checkpoint, { target, source });
206
222
  });
207
223
  /* c8 ignore next 3 */
@@ -233,11 +249,10 @@ function addViewMediaTracking(parent) {
233
249
  }
234
250
 
235
251
  function addFormTracking(parent) {
236
- activateBlocksMutationObserver();
237
- activateMediaMutationObserver();
252
+ activateBlocksMO();
253
+ activateMediaMO();
238
254
  parent.querySelectorAll('form').forEach((form) => {
239
- form.removeEventListener('submit', formSubmitListener); // listen only once
240
- form.addEventListener('submit', formSubmitListener);
255
+ form.addEventListener('submit', (e) => sampleRUM('formsubmit', { target: targetSelector(e.target), source: sourceSelector(e.target) }), { once: true });
241
256
  });
242
257
  }
243
258
 
@@ -245,7 +260,8 @@ function addObserver(ck, fn, block) {
245
260
  return DEFAULT_TRACKING_EVENTS.includes(ck) && fn(block);
246
261
  }
247
262
 
248
- function blocksMutationsCallback(mutations) {
263
+ // blocks mutation observer callback
264
+ function blocksMCB(mutations) {
249
265
  // block specific mutations
250
266
  mutations
251
267
  .filter((m) => m.type === 'attributes' && m.attributeName === 'data-block-status')
@@ -256,7 +272,8 @@ function blocksMutationsCallback(mutations) {
256
272
  });
257
273
  }
258
274
 
259
- function mediaMutationsCallback(mutations) {
275
+ // media mutation observer callback
276
+ function mediaMCB(mutations) {
260
277
  // media mutations
261
278
  mutations
262
279
  .forEach((m) => {
@@ -19,7 +19,7 @@ export function addCookieConsentTracking(sampleRUM) {
19
19
  return;
20
20
  }
21
21
 
22
- let consentMutationObserver;
22
+ let consentMO; // consent mutation observer
23
23
  const trackShowConsent = () => {
24
24
  const otsdk = document.querySelector('body > div#onetrust-consent-sdk');
25
25
  if (otsdk) {
@@ -28,8 +28,8 @@ export function addCookieConsentTracking(sampleRUM) {
28
28
  } else {
29
29
  sampleRUM('consent', { source: 'onetrust', target: 'show' });
30
30
  }
31
- if (consentMutationObserver) {
32
- consentMutationObserver.disconnect();
31
+ if (consentMO) {
32
+ consentMO.disconnect();
33
33
  }
34
34
  return true;
35
35
  }
@@ -38,11 +38,11 @@ export function addCookieConsentTracking(sampleRUM) {
38
38
 
39
39
  if (!trackShowConsent()) {
40
40
  // eslint-disable-next-line max-len
41
- consentMutationObserver = window.MutationObserver
41
+ consentMO = window.MutationObserver
42
42
  ? new MutationObserver(trackShowConsent)
43
43
  : /* c8 ignore next */ null;
44
- if (consentMutationObserver) {
45
- consentMutationObserver.observe(
44
+ if (consentMO) {
45
+ consentMO.observe(
46
46
  document.body,
47
47
  // eslint-disable-next-line object-curly-newline
48
48
  { attributes: false, childList: true, subtree: false },
package/modules/utils.js CHANGED
@@ -10,6 +10,8 @@
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
12
 
13
+ const { href } = window.location;
14
+
13
15
  export const urlSanitizers = {
14
16
  /**
15
17
  * Returns the full url.
@@ -17,21 +19,21 @@ export const urlSanitizers = {
17
19
  * @param {string} url (default: window.location.href) The url to sanitize
18
20
  * @returns {string} The sanitized url
19
21
  */
20
- full: (url = window.location.href) => new URL(url).toString(),
22
+ full: (url = href) => new URL(url).toString(),
21
23
  /**
22
24
  * Returns the origin of the provided url.
23
25
  * If no url is provided, it defaults to window.location.href.
24
26
  * @param {string} url (default: window.location.href) The url to sanitize
25
27
  * @returns {string} The sanitized url
26
28
  */
27
- origin: (url = window.location.href) => new URL(url).origin,
29
+ origin: (url = href) => new URL(url).origin,
28
30
  /**
29
31
  * Returns the sanitized url: the origin and the path (no query params or hash)
30
32
  * If no url is provided, it defaults to window.location.href.
31
33
  * @param {string} url (default: window.location.href) The url to sanitize
32
34
  * @returns {string} The sanitized url
33
35
  */
34
- path: (url = window.location.href) => {
36
+ path: (url = href) => {
35
37
  const u = new URL(url);
36
38
  return `${u.origin}${u.pathname}`;
37
39
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/helix-rum-enhancer",
3
- "version": "2.25.0",
3
+ "version": "2.26.0",
4
4
  "description": "Helix RUM Enhancer",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -33,7 +33,7 @@
33
33
  "reporter-options": "configFile=.mocha-multi.json"
34
34
  },
35
35
  "devDependencies": {
36
- "@adobe/eslint-config-helix": "2.0.7",
36
+ "@adobe/eslint-config-helix": "2.0.8",
37
37
  "@adobe/helix-rum-js": "2.6.0",
38
38
  "@esm-bundle/chai": "4.3.4-fix.0",
39
39
  "@semantic-release/changelog": "6.0.3",
@@ -47,7 +47,7 @@
47
47
  "codecov": "3.8.3",
48
48
  "eslint": "8.57.1",
49
49
  "eslint-plugin-header": "3.1.1",
50
- "eslint-plugin-import": "2.30.0",
50
+ "eslint-plugin-import": "2.31.0",
51
51
  "husky": "9.1.6",
52
52
  "jsdoc-to-markdown": "9.0.2",
53
53
  "junit-report-builder": "5.1.1",
package/src/index.js CHANGED
@@ -29,96 +29,97 @@
29
29
  const DEFAULT_TRACKING_EVENTS = ['click', 'cwv', 'form', 'viewblock', 'viewmedia', 'loadresource', 'utm', 'paid', 'email', 'consent'];
30
30
  fflags.enabled('example', () => DEFAULT_TRACKING_EVENTS.push('example'));
31
31
 
32
+ const { href } = window.location;
32
33
  const urlSanitizers = {
33
- full: (url = window.location.href) => new URL(url).toString(),
34
- origin: (url = window.location.href) => new URL(url).origin,
35
- path: (url = window.location.href) => {
34
+ full: (url = href) => new URL(url).toString(),
35
+ origin: (url = href) => new URL(url).origin,
36
+ path: (url = href) => {
36
37
  const u = new URL(url);
37
38
  return `${u.origin}${u.pathname}`;
38
39
  },
39
40
  };
40
41
 
41
- const getTargetValue = (element) => element.getAttribute('data-rum-target') || element.getAttribute('href')
42
- || element.currentSrc || element.getAttribute('src') || element.dataset.action || element.action;
43
- const targetSelector = (element) => {
42
+ const getTargetValue = (el) => el.getAttribute('data-rum-target') || el.getAttribute('href')
43
+ || el.currentSrc || el.getAttribute('src') || el.dataset.action || el.action;
44
+ const targetSelector = (el) => {
44
45
  try {
45
- if (!element) return undefined;
46
- let value = getTargetValue(element);
47
- if (!value && element.tagName !== 'A' && element.closest('a')) {
48
- value = getTargetValue(element.closest('a'));
46
+ if (!el) return undefined;
47
+ let v = getTargetValue(el);
48
+ if (!v && el.tagName !== 'A' && el.closest('a')) {
49
+ v = getTargetValue(el.closest('a'));
49
50
  }
50
- if (value && !value.startsWith('https://')) {
51
- value = new URL(value, window.location).href;
51
+ if (v && !v.startsWith('https://')) {
52
+ v = new URL(v, window.location).href;
52
53
  }
53
- return value;
54
+ return v;
54
55
  } catch (error) {
55
56
  return null;
56
57
  }
57
58
  };
58
- function walk(element, checkFn) {
59
- if (!element || element === document.body || element === document.documentElement) {
59
+ function walk(el, checkFn) {
60
+ if (!el || el === document.body || el === document.documentElement) {
60
61
  return undefined;
61
62
  }
62
- const checkValue = checkFn(element);
63
- return checkValue || walk(element.parentElement, checkFn);
63
+ return checkFn(el) || walk(el.parentElement, checkFn);
64
64
  }
65
- function isDialog(element) {
66
- if (element.tagName === 'DIALOG') return true;
67
- if (element.getAttribute('role') === 'dialog') return true;
68
- if (element.getAttribute('role') === 'alertdialog') return true;
69
- if (element.getAttribute('aria-modal') === 'true') return true;
70
- const computedStyle = window.getComputedStyle(element);
71
- return (computedStyle && computedStyle.position === 'fixed' && computedStyle.zIndex > 100);
65
+ function isDialog(el) {
66
+ if (el.tagName === 'DIALOG') return true;
67
+ if (el.getAttribute('role') === 'dialog') return true;
68
+ if (el.getAttribute('role') === 'alertdialog') return true;
69
+ if (el.getAttribute('aria-modal') === 'true') return true;
70
+ const cs = window.getComputedStyle(el);
71
+ return (cs && cs.position === 'fixed' && cs.zIndex > 100);
72
72
  }
73
- function isButton(element) {
74
- if (element.tagName === 'BUTTON') return true;
75
- if (element.tagName === 'INPUT' && element.getAttribute('type') === 'button') return true;
76
- if (element.tagName === 'A') {
77
- const classes = Array.from(element.classList);
73
+ function isButton(el) {
74
+ if (el.tagName === 'BUTTON') return true;
75
+ if (el.tagName === 'INPUT' && el.getAttribute('type') === 'button') return true;
76
+ if (el.tagName === 'A') {
77
+ const classes = Array.from(el.classList);
78
78
  return classes.some((className) => className.match(/button|cta/));
79
79
  }
80
- return element.getAttribute('role') === 'button';
80
+ return el.getAttribute('role') === 'button';
81
81
  }
82
- function getSourceContext(element) {
83
- if (element.closest('form')) {
84
- return `form${element.id ? `#${element.id}` : ''}`;
82
+ function getSourceContext(el) {
83
+ if (el.closest('form')) {
84
+ return `form${el.id ? `#${el.id}` : ''}`;
85
85
  }
86
- const block = element.closest('.block[data-block-name]');
86
+ const block = el.closest('.block[data-block-name]');
87
87
  if (block) return `.${block.getAttribute('data-block-name')}`;
88
- if (walk(element, isDialog)) return 'dialog';
89
- if (element.closest('nav')) return 'nav';
90
- if (element.closest('header')) return 'header';
91
- if (element.closest('footer')) return 'footer';
92
- if (element.closest('aside')) return 'aside';
93
- return (walk(element, (e) => e.id && `#${e.id}`));
88
+ if (walk(el, isDialog)) return 'dialog';
89
+ if (el.closest('nav')) return 'nav';
90
+ if (el.closest('header')) return 'header';
91
+ if (el.closest('footer')) return 'footer';
92
+ if (el.closest('aside')) return 'aside';
93
+ return (walk(el, (e) => e.id && `#${e.id}`));
94
94
  }
95
- function getSourceElement(element) {
96
- if (element.closest('form') && Array.from(element.closest('form').elements).includes(element)) {
97
- return (element.tagName.toLowerCase()
98
- + (['INPUT', 'BUTTON'].includes(element.tagName)
99
- ? `[type='${element.getAttribute('type') || ''}']`
95
+ function getSourceElement(el) {
96
+ const f = el.closest('form');
97
+ if (f && Array.from(f.elements).includes(el)) {
98
+ return (el.tagName.toLowerCase()
99
+ + (['INPUT', 'BUTTON'].includes(el.tagName)
100
+ ? `[type='${el.getAttribute('type') || ''}']`
100
101
  : ''));
101
102
  }
102
- if (walk(element, isButton)) return 'button';
103
- return element.tagName.toLowerCase().match(/^(a|img|video)$/) && element.tagName.toLowerCase();
103
+ if (walk(el, isButton)) return 'button';
104
+ return el.tagName.toLowerCase().match(/^(a|img|video)$/) && el.tagName.toLowerCase();
104
105
  }
105
- function getSourceIdentifier(element) {
106
- if (element.id) return `#${element.id}`;
107
- if (element.getAttribute('data-block-name')) return `.${element.getAttribute('data-block-name')}`;
108
- return (element.classList.length > 0 && `.${element.classList[0]}`);
106
+ function getSourceIdentifier(el) {
107
+ if (el.id) return `#${el.id}`;
108
+ if (el.getAttribute('data-block-name')) return `.${el.getAttribute('data-block-name')}`;
109
+ return (el.classList.length > 0 && `.${el.classList[0]}`);
109
110
  }
110
- const sourceSelector = (element) => {
111
+ const sourceSelector = (el) => {
111
112
  try {
112
- if (!element || element === document.body || element === document.documentElement) {
113
+ if (!el || el === document.body || el === document.documentElement) {
113
114
  return undefined;
114
115
  }
115
- if (element.getAttribute('data-rum-source')) {
116
- return element.getAttribute('data-rum-source');
116
+ if (el.getAttribute('data-rum-source')) {
117
+ return el.getAttribute('data-rum-source');
117
118
  }
118
- const context = getSourceContext(element.parentElement) || '';
119
- const elementName = getSourceElement(element) || '';
120
- const identifier = getSourceIdentifier(element) || '';
121
- return `${context} ${elementName}${identifier}`.trim() || `"${element.textContent.substring(0, 10)}"`;
119
+ const ctx = getSourceContext(el.parentElement) || '';
120
+ const name = getSourceElement(el) || '';
121
+ const id = getSourceIdentifier(el) || '';
122
+ return `${ctx} ${name}${id}`.trim() || `"${el.textContent.substring(0, 10)}"`;
122
123
  } catch (error) {
123
124
  return null;
124
125
  }
@@ -132,7 +133,7 @@
132
133
  sampleRUM('consent', { source: 'onetrust', target: 'hidden' });
133
134
  return;
134
135
  }
135
- let consentMutationObserver;
136
+ let consentMO;
136
137
  const trackShowConsent = () => {
137
138
  const otsdk = document.querySelector('body > div#onetrust-consent-sdk');
138
139
  if (otsdk) {
@@ -141,19 +142,19 @@
141
142
  } else {
142
143
  sampleRUM('consent', { source: 'onetrust', target: 'show' });
143
144
  }
144
- if (consentMutationObserver) {
145
- consentMutationObserver.disconnect();
145
+ if (consentMO) {
146
+ consentMO.disconnect();
146
147
  }
147
148
  return true;
148
149
  }
149
150
  return false;
150
151
  };
151
152
  if (!trackShowConsent()) {
152
- consentMutationObserver = window.MutationObserver
153
+ consentMO = window.MutationObserver
153
154
  ? new MutationObserver(trackShowConsent)
154
155
  : null;
155
- if (consentMutationObserver) {
156
- consentMutationObserver.observe(
156
+ if (consentMO) {
157
+ consentMO.observe(
157
158
  document.body,
158
159
  { attributes: false, childList: true, subtree: false },
159
160
  );
@@ -197,10 +198,9 @@
197
198
 
198
199
  const { sampleRUM, queue, isSelected } = (window.hlx && window.hlx.rum) ? window.hlx.rum
199
200
  : {};
200
- const formSubmitListener = (e) => sampleRUM('formsubmit', { target: targetSelector(e.target), source: sourceSelector(e.target) });
201
- const blocksMutationObserver = window.MutationObserver ? new MutationObserver(blocksMutationsCallback)
201
+ const blocksMO = window.MutationObserver ? new MutationObserver(blocksMCB)
202
202
  : {};
203
- const mediaMutationObserver = window.MutationObserver ? new MutationObserver(mediaMutationsCallback)
203
+ const mediaMO = window.MutationObserver ? new MutationObserver(mediaMCB)
204
204
  : {};
205
205
  function trackCheckpoint(checkpoint, data, t) {
206
206
  const { weight, id } = window.hlx.rum;
@@ -266,7 +266,17 @@
266
266
  function addNavigationTracking() {
267
267
  const navigate = (source, type, redirectCount) => {
268
268
  const payload = { source, target: document.visibilityState };
269
- if (type === 'reload' || source === window.location.href) {
269
+ if (document.prerendering) {
270
+ document.addEventListener('prerenderingchange', () => {
271
+ payload.target = 'prerendered';
272
+ sampleRUM('navigate', payload);
273
+ }, {
274
+ once: true,
275
+ });
276
+ if (type === 'navigate') {
277
+ sampleRUM('prerender', payload);
278
+ }
279
+ } else if (type === 'reload' || source === window.location.href) {
270
280
  sampleRUM('reload', payload);
271
281
  } else if (type && type !== 'navigate') {
272
282
  sampleRUM(type, payload);
@@ -283,10 +293,10 @@
283
293
  });
284
294
  };
285
295
  new PerformanceObserver((list) => list
286
- .getEntries().map((entry) => navigate(
296
+ .getEntries().map((e) => navigate(
287
297
  window.hlx.referrer || document.referrer,
288
- entry.type,
289
- entry.redirectCount,
298
+ e.type,
299
+ e.redirectCount,
290
300
  )))
291
301
  .observe({ type: 'navigation', buffered: true });
292
302
  }
@@ -294,38 +304,38 @@
294
304
  const observer = new PerformanceObserver((list) => {
295
305
  try {
296
306
  list.getEntries()
297
- .filter((entry) => !entry.responseStatus || entry.responseStatus < 400)
298
- .filter((entry) => window.location.hostname === new URL(entry.name).hostname)
299
- .filter((entry) => new URL(entry.name).pathname.match('.*(\\.plain\\.html$|\\.json|graphql|api)'))
300
- .forEach((entry) => {
301
- sampleRUM('loadresource', { source: entry.name, target: Math.round(entry.duration) });
307
+ .filter((e) => !e.responseStatus || e.responseStatus < 400)
308
+ .filter((e) => window.location.hostname === new URL(e.name).hostname)
309
+ .filter((e) => new URL(e.name).pathname.match('.*(\\.plain\\.html$|\\.json|graphql|api)'))
310
+ .forEach((e) => {
311
+ sampleRUM('loadresource', { source: e.name, target: Math.round(e.duration) });
302
312
  });
303
313
  list.getEntries()
304
- .filter((entry) => entry.responseStatus === 404)
305
- .forEach((entry) => {
306
- sampleRUM('missingresource', { source: entry.name, target: entry.hostname });
314
+ .filter((e) => e.responseStatus === 404)
315
+ .forEach((e) => {
316
+ sampleRUM('missingresource', { source: e.name, target: e.hostname });
307
317
  });
308
318
  } catch (error) {
309
319
  }
310
320
  });
311
321
  observer.observe({ type: 'resource', buffered: true });
312
322
  }
313
- function activateBlocksMutationObserver() {
314
- if (!blocksMutationObserver || blocksMutationObserver.active) {
323
+ function activateBlocksMO() {
324
+ if (!blocksMO || blocksMO.active) {
315
325
  return;
316
326
  }
317
- blocksMutationObserver.active = true;
318
- blocksMutationObserver.observe(
327
+ blocksMO.active = true;
328
+ blocksMO.observe(
319
329
  document.body,
320
330
  { subtree: true, attributes: true, attributeFilter: ['data-block-status'] },
321
331
  );
322
332
  }
323
- function activateMediaMutationObserver() {
324
- if (!mediaMutationObserver || mediaMutationObserver.active) {
333
+ function activateMediaMO() {
334
+ if (!mediaMO || mediaMO.active) {
325
335
  return;
326
336
  }
327
- mediaMutationObserver.active = true;
328
- mediaMutationObserver.observe(
337
+ mediaMO.active = true;
338
+ mediaMO.observe(
329
339
  document.body,
330
340
  { subtree: true, attributes: false, childList: true },
331
341
  );
@@ -334,16 +344,16 @@
334
344
  if (!window.IntersectionObserver) {
335
345
  return null;
336
346
  }
337
- activateBlocksMutationObserver();
338
- activateMediaMutationObserver();
347
+ activateBlocksMO();
348
+ activateMediaMO();
339
349
  const observer = new IntersectionObserver((entries) => {
340
350
  try {
341
351
  entries
342
- .filter((entry) => entry.isIntersecting)
343
- .forEach((entry) => {
344
- observer.unobserve(entry.target);
345
- const target = targetSelector(entry.target);
346
- const source = sourceSelector(entry.target);
352
+ .filter((e) => e.isIntersecting)
353
+ .forEach((e) => {
354
+ observer.unobserve(e.target);
355
+ const target = targetSelector(e.target);
356
+ const source = sourceSelector(e.target);
347
357
  sampleRUM(checkpoint, { target, source });
348
358
  });
349
359
  } catch (error) {
@@ -371,17 +381,16 @@
371
381
  }
372
382
  }
373
383
  function addFormTracking(parent) {
374
- activateBlocksMutationObserver();
375
- activateMediaMutationObserver();
384
+ activateBlocksMO();
385
+ activateMediaMO();
376
386
  parent.querySelectorAll('form').forEach((form) => {
377
- form.removeEventListener('submit', formSubmitListener);
378
- form.addEventListener('submit', formSubmitListener);
387
+ form.addEventListener('submit', (e) => sampleRUM('formsubmit', { target: targetSelector(e.target), source: sourceSelector(e.target) }), { once: true });
379
388
  });
380
389
  }
381
390
  function addObserver(ck, fn, block) {
382
391
  return DEFAULT_TRACKING_EVENTS.includes(ck) && fn(block);
383
392
  }
384
- function blocksMutationsCallback(mutations) {
393
+ function blocksMCB(mutations) {
385
394
  mutations
386
395
  .filter((m) => m.type === 'attributes' && m.attributeName === 'data-block-status')
387
396
  .filter((m) => m.target.dataset.blockStatus === 'loaded')
@@ -390,7 +399,7 @@
390
399
  addObserver('viewblock', addViewBlockTracking, m.target);
391
400
  });
392
401
  }
393
- function mediaMutationsCallback(mutations) {
402
+ function mediaMCB(mutations) {
394
403
  mutations
395
404
  .forEach((m) => {
396
405
  addObserver('viewmedia', addViewMediaTracking, m.target);
package/src/index.md5 CHANGED
@@ -1 +1 @@
1
- f87fdcb7d5c1e4526225b185d4f392fb
1
+ 8094422990fd7a9219bffb7309cb92a2