@adobe/helix-rum-enhancer 2.19.2 → 2.21.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/.husky/pre-commit CHANGED
@@ -1,2 +1 @@
1
1
  npx lint-staged
2
- npm test
package/.releaserc.cjs CHANGED
@@ -10,7 +10,18 @@ module.exports = {
10
10
  "assets": ["package.json", "CHANGELOG.md"],
11
11
  "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
12
12
  }],
13
- ["@semantic-release/github", {}]
13
+ ["@semantic-release/github", {
14
+ "assets": [
15
+ {
16
+ "path": "src/index.js",
17
+ "label": "RUM Enhancer JS"
18
+ },
19
+ {
20
+ "path": "src/index.md5",
21
+ "label": "RUM Enhancer Hash"
22
+ }
23
+ ]
24
+ }]
14
25
  ],
15
26
  branches: ['main'],
16
27
  };
package/CHANGELOG.md CHANGED
@@ -1,3 +1,25 @@
1
+ # [2.21.0](https://github.com/adobe/helix-rum-enhancer/compare/v2.20.0...v2.21.0) (2024-09-18)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **dom:** apply change ([67803e6](https://github.com/adobe/helix-rum-enhancer/commit/67803e60f57ef29b9ea1c51eea26d99c778b5f97))
7
+ * **dom:** better handling of empty type form inputs ([d53deb5](https://github.com/adobe/helix-rum-enhancer/commit/d53deb5c6e94ff18df2a01996c39b5cff7636fe4))
8
+ * **dom:** better handling of fake blocks ([66a9049](https://github.com/adobe/helix-rum-enhancer/commit/66a90497a45d754ed0fa4e8f8bc0917be64b3245))
9
+
10
+
11
+ ### Features
12
+
13
+ * **checkpoints:** add support for the `language` checkpoint ([f99755a](https://github.com/adobe/helix-rum-enhancer/commit/f99755a5cc8eef83b655a6beff5e7e371196c90d))
14
+ * **dom:** more fine-grained sourceselector implementation ([ada279d](https://github.com/adobe/helix-rum-enhancer/commit/ada279d423590fb3feeb304b3a57d6da4ee3c9bf))
15
+
16
+ # [2.20.0](https://github.com/adobe/helix-rum-enhancer/compare/v2.19.2...v2.20.0) (2024-08-19)
17
+
18
+
19
+ ### Features
20
+
21
+ * observe all mutations to capture late added medias ([#261](https://github.com/adobe/helix-rum-enhancer/issues/261)) ([56844b2](https://github.com/adobe/helix-rum-enhancer/commit/56844b2aa61fe5cc837cd5a887f0c8669a335e0f))
22
+
1
23
  ## [2.19.2](https://github.com/adobe/helix-rum-enhancer/compare/v2.19.1...v2.19.2) (2024-08-15)
2
24
 
3
25
 
package/README.md CHANGED
@@ -23,6 +23,49 @@ It will add following new checkpoints:
23
23
  - `reload`, `navigate`, `enter`: depending on how the current page was accessed
24
24
  - `formsubmit`: when a form is submitted
25
25
 
26
+ ### The `source` parameter and the `sourceSelector`
27
+
28
+ The `source` parameter is a string that can be used to identify the source of the event. It can be used to identify the source of the event, e.g. a button, a link, a form, etc.
29
+ It represents an idealized CSS selector that is both human-readable and specific enough to identify the source of the event in the document, even when not having access to the
30
+ orginal document. It is idealized because it pretends the DOM would use modern HTML with concise semantics, even if the actual document uses `class` values for things that would
31
+ be better represented by semantic HTML elements.
32
+
33
+ The `sourceSelector` function is a function that takes a DOM element and returns a descriptive `source` parameter. If the element has a `data-rum-source` attribute, its value is used as the `source` parameter. Otherwise, the function tries to generate a `source` parameter based on the element's tag name, class names, and text content.
34
+
35
+ The structure of the `source` parameter is as follows:
36
+
37
+ ```
38
+ <context> <element>#<identifier>
39
+ ```
40
+ All three parts are optional
41
+
42
+ `context` is
43
+ - `form` for form elements
44
+ - `dialog` for dialog elements, or parent containers that are fixed positioned and have a positive high z-index
45
+ - `.block-name` for Helix blocks
46
+ - `header`, `footer`, `nav`, `aside` for main site structure
47
+ - `#id` as a fallback, if a container ID is available
48
+
49
+ `element` is
50
+ - `button` for buttons, or links that look like buttons (e.g. with a class `button` or `btn` or `cta`)
51
+ - `img` for images
52
+ - `video` for videos
53
+ - `a` for links that are not buttons
54
+ - `input[type="text"]` for input elements (all types are supported)
55
+ - `select`, `textarea`, etc. for other form elements
56
+
57
+ `identifier` is
58
+ - the `id` attribute of the element, if provided
59
+ - the first `.class` if there are any
60
+ - else omitted
61
+
62
+ Even if an `identifier` is provided, having a `context` and `element` is recommended, as it makes the `source` parameter more readable and easier to understand.
63
+
64
+
65
+ #### Examples
66
+
67
+ - ``
68
+
26
69
  ## Development
27
70
 
28
71
  ### Build
package/modules/dom.js CHANGED
@@ -30,6 +30,59 @@ export const targetSelector = (element) => {
30
30
  }
31
31
  };
32
32
 
33
+ function walk(element, checkFn) {
34
+ if (!element || element === document.body || element === document.documentElement) {
35
+ return undefined;
36
+ }
37
+ const checkValue = checkFn(element);
38
+ return checkValue || walk(element.parentElement, checkFn);
39
+ }
40
+
41
+ function isDialog(element) {
42
+ // doing it well
43
+ if (element.tagName === 'DIALOG') return true;
44
+ // 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;
48
+ // doing it wrong
49
+ const computedStyle = window.getComputedStyle(element);
50
+ return (computedStyle && computedStyle.position === 'fixed' && computedStyle.zIndex > 100);
51
+ }
52
+
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);
58
+ return classes.some((className) => className.match(/button|cta/));
59
+ }
60
+ return element.getAttribute('role') === 'button';
61
+ }
62
+
63
+ function getSourceContext(element) {
64
+ if (element.closest('form')) return 'form';
65
+ const block = element.closest('.block[data-block-name]');
66
+ if (block) return `.${block.getAttribute('data-block-name')}`;
67
+ if (walk(element, isDialog)) return 'dialog';
68
+ if (element.closest('nav')) return 'nav';
69
+ if (element.closest('header')) return 'header';
70
+ if (element.closest('footer')) return 'footer';
71
+ if (element.closest('aside')) return 'aside';
72
+ return (walk(element, (e) => e.id && `#${e.id}`));
73
+ }
74
+
75
+ function getSourceElement(element) {
76
+ if (element.closest('form') && Array.from(element.closest('form').elements).includes(element)) return element.tagName.toLowerCase() + (element.tagName === 'INPUT' ? `[type='${element.getAttribute('type') || ''}']` : '');
77
+ if (walk(element, isButton)) return 'button';
78
+ return element.tagName.toLowerCase().match(/^(a|img|video)$/) && element.tagName.toLowerCase();
79
+ }
80
+
81
+ function getSourceIdentifier(element) {
82
+ if (element.id) return `#${element.id}`;
83
+ if (element.getAttribute('data-block-name')) return `.${element.getAttribute('data-block-name')}`;
84
+ return (element.classList.length > 0 && `.${element.classList[0]}`);
85
+ }
33
86
  export const sourceSelector = (element) => {
34
87
  try {
35
88
  if (!element || element === document.body || element === document.documentElement) {
@@ -38,41 +91,10 @@ export const sourceSelector = (element) => {
38
91
  if (element.getAttribute('data-rum-source')) {
39
92
  return element.getAttribute('data-rum-source');
40
93
  }
41
- const form = element.closest('form');
42
- let formElementSelector = '';
43
- if (form && Array.from(form.elements).includes(element)) {
44
- formElementSelector = element.tagName === 'INPUT' ? `form input[type='${element.getAttribute('type')}']` : `form ${element.tagName.toLowerCase()}`;
45
- }
46
-
47
- const blockName = element.closest('.block') ? element.closest('.block').getAttribute('data-block-name') : '';
48
- if (element.id || formElementSelector) {
49
- const id = element.id ? `#${element.id}` : '';
50
- return blockName ? `.${blockName} ${formElementSelector}${id}` : `${formElementSelector}${id}`;
51
- }
52
-
53
- if (element.getAttribute('data-block-name')) {
54
- return `.${element.getAttribute('data-block-name')}`;
55
- }
56
-
57
- const classes = Array.from(element.classList);
58
- const label = element.tagName.toLowerCase();
59
- const firstClass = classes.length > 0 ? `.${classes[0]}` : '';
60
- const labelWithClass = `${element.tagName.toLowerCase()}${firstClass}`;
61
- if (element.tagName.toLowerCase() === 'button'
62
- || element.type === 'button'
63
- || classes.some((className) => className.match(/button|cta/))) {
64
- let parent = element.parentElement;
65
- if (!parent) return labelWithClass;
66
- if (parent.id) return `#${parent.id} ${label}`;
67
- while (parent.tagName !== 'BODY' && !parent.id) parent = parent.parentElement;
68
- if (parent.id) return `#${parent.id} ${labelWithClass}`;
69
- return blockName ? `.${blockName} ${labelWithClass}` : labelWithClass;
70
- }
71
-
72
- const parent = sourceSelector(element.parentElement);
73
- if (parent) return parent;
74
-
75
- return labelWithClass;
94
+ const context = getSourceContext(element.parentElement) || '';
95
+ const elementName = getSourceElement(element) || '';
96
+ const identifier = getSourceIdentifier(element) || '';
97
+ return `${context} ${elementName}${identifier}`.trim() || `"${element.textContent.substring(0, 10)}"`;
76
98
  /* c8 ignore next 3 */
77
99
  } catch (error) {
78
100
  return null;
package/modules/fflags.js CHANGED
@@ -19,4 +19,5 @@ export const fflags = {
19
19
  disabled: (flag, callback) => !fflags.has(flag) && callback(),
20
20
  eagercwv: [683],
21
21
  example: [543, 770, 1136],
22
+ language: [543, 959, 1139, 620],
22
23
  };
package/modules/index.js CHANGED
@@ -26,9 +26,14 @@ const { sampleRUM, queue, isSelected } = (window.hlx && window.hlx.rum) ? window
26
26
  /* c8 ignore next */ : {};
27
27
 
28
28
  const formSubmitListener = (e) => sampleRUM('formsubmit', { target: targetSelector(e.target), source: sourceSelector(e.target) });
29
- // eslint-disable-next-line no-use-before-define
30
- const mutationObserver = window.MutationObserver ? new MutationObserver(mutationsCallback)
31
- /* c8 ignore next */ : null;
29
+
30
+ // eslint-disable-next-line no-use-before-define, max-len
31
+ const blocksMutationObserver = window.MutationObserver ? new MutationObserver(blocksMutationsCallback)
32
+ /* c8 ignore next */ : {};
33
+
34
+ // eslint-disable-next-line no-use-before-define, max-len
35
+ const mediaMutationObserver = window.MutationObserver ? new MutationObserver(mediaMutationsCallback)
36
+ /* c8 ignore next */ : {};
32
37
 
33
38
  function trackCheckpoint(checkpoint, data, t) {
34
39
  const { weight, id } = window.hlx.rum;
@@ -52,7 +57,7 @@ function trackCheckpoint(checkpoint, data, t) {
52
57
  }
53
58
 
54
59
  function processQueue() {
55
- while (queue.length) {
60
+ while (queue && queue.length) {
56
61
  const ck = queue.shift();
57
62
  trackCheckpoint(...ck);
58
63
  }
@@ -148,21 +153,36 @@ function addLoadResourceTracking() {
148
153
  }
149
154
 
150
155
  function activateBlocksMutationObserver() {
151
- if (!mutationObserver || mutationObserver.active) {
156
+ if (!blocksMutationObserver || blocksMutationObserver.active) {
152
157
  return;
153
158
  }
154
- mutationObserver.active = true;
155
- mutationObserver.observe(
159
+ blocksMutationObserver.active = true;
160
+ blocksMutationObserver.observe(
156
161
  document.body,
157
162
  // eslint-disable-next-line object-curly-newline
158
163
  { subtree: true, attributes: true, attributeFilter: ['data-block-status'] },
159
164
  );
160
165
  }
161
166
 
167
+ function activateMediaMutationObserver() {
168
+ if (!mediaMutationObserver || mediaMutationObserver.active) {
169
+ return;
170
+ }
171
+ mediaMutationObserver.active = true;
172
+ mediaMutationObserver.observe(
173
+ document.body,
174
+ // eslint-disable-next-line object-curly-newline
175
+ { subtree: true, attributes: false, childList: true },
176
+ );
177
+ }
178
+
162
179
  function getIntersectionObsever(checkpoint) {
163
- /* c8 ignore next */
164
- if (!window.IntersectionObserver) return null;
180
+ /* c8 ignore next 3 */
181
+ if (!window.IntersectionObserver) {
182
+ return null;
183
+ }
165
184
  activateBlocksMutationObserver();
185
+ activateMediaMutationObserver();
166
186
  const observer = new IntersectionObserver((entries) => {
167
187
  try {
168
188
  entries
@@ -188,11 +208,13 @@ function addViewBlockTracking(element) {
188
208
  }
189
209
  }
190
210
 
211
+ const observedMedia = new Set();
191
212
  function addViewMediaTracking(parent) {
192
213
  const mediaobserver = getIntersectionObsever('viewmedia');
193
214
  if (mediaobserver) {
194
215
  parent.querySelectorAll('img, video, audio, iframe').forEach((m) => {
195
- if (!m.closest('div .block') || m.closest('div[data-block-status="loaded"]')) {
216
+ if (!observedMedia.has(m)) {
217
+ observedMedia.add(m);
196
218
  mediaobserver.observe(m);
197
219
  }
198
220
  });
@@ -201,19 +223,32 @@ function addViewMediaTracking(parent) {
201
223
 
202
224
  function addFormTracking(parent) {
203
225
  activateBlocksMutationObserver();
226
+ activateMediaMutationObserver();
204
227
  parent.querySelectorAll('form').forEach((form) => {
205
228
  form.removeEventListener('submit', formSubmitListener); // listen only once
206
229
  form.addEventListener('submit', formSubmitListener);
207
230
  });
208
231
  }
209
232
 
210
- const addObserver = (ck, fn, block) => DEFAULT_TRACKING_EVENTS.includes(ck) && fn(block);
211
- function mutationsCallback(mutations) {
212
- mutations.filter((m) => m.type === 'attributes' && m.attributeName === 'data-block-status')
233
+ function addObserver(ck, fn, block) {
234
+ return DEFAULT_TRACKING_EVENTS.includes(ck) && fn(block);
235
+ }
236
+
237
+ function blocksMutationsCallback(mutations) {
238
+ // block specific mutations
239
+ mutations
240
+ .filter((m) => m.type === 'attributes' && m.attributeName === 'data-block-status')
213
241
  .filter((m) => m.target.dataset.blockStatus === 'loaded')
214
242
  .forEach((m) => {
215
243
  addObserver('form', addFormTracking, m.target);
216
244
  addObserver('viewblock', addViewBlockTracking, m.target);
245
+ });
246
+ }
247
+
248
+ function mediaMutationsCallback(mutations) {
249
+ // media mutations
250
+ mutations
251
+ .forEach((m) => {
217
252
  addObserver('viewmedia', addViewMediaTracking, m.target);
218
253
  });
219
254
  }
@@ -232,6 +267,11 @@ function addTrackingFromConfig() {
232
267
  addCookieConsentTracking(sampleRUM);
233
268
  addAdsParametersTracking(sampleRUM);
234
269
  addEmailParameterTracking(sampleRUM);
270
+ fflags.enabled('language', () => {
271
+ const target = navigator.language;
272
+ const source = document.documentElement.lang;
273
+ sampleRUM('language', { source, target });
274
+ });
235
275
  }
236
276
 
237
277
  function initEnhancer() {
@@ -241,6 +281,7 @@ function initEnhancer() {
241
281
  window.hlx.rum.collector = trackCheckpoint;
242
282
  processQueue();
243
283
  }
284
+ /* c8 ignore next 3 */
244
285
  } catch (error) {
245
286
  // something went wrong
246
287
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/helix-rum-enhancer",
3
- "version": "2.19.2",
3
+ "version": "2.21.0",
4
4
  "description": "Helix RUM Enhancer",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -33,8 +33,8 @@
33
33
  "reporter-options": "configFile=.mocha-multi.json"
34
34
  },
35
35
  "devDependencies": {
36
- "@adobe/eslint-config-helix": "2.0.6",
37
- "@adobe/helix-rum-js": "2.2.0",
36
+ "@adobe/eslint-config-helix": "2.0.7",
37
+ "@adobe/helix-rum-js": "2.4.0",
38
38
  "@esm-bundle/chai": "4.3.4-fix.0",
39
39
  "@semantic-release/changelog": "6.0.3",
40
40
  "@semantic-release/git": "10.0.1",
@@ -47,17 +47,18 @@
47
47
  "codecov": "3.8.3",
48
48
  "eslint": "8.57.0",
49
49
  "eslint-plugin-header": "3.1.1",
50
- "eslint-plugin-import": "2.29.1",
51
- "husky": "9.1.4",
52
- "jsdoc-to-markdown": "8.0.3",
50
+ "eslint-plugin-import": "2.30.0",
51
+ "husky": "9.1.5",
52
+ "jsdoc-to-markdown": "9.0.0",
53
53
  "junit-report-builder": "5.0.0",
54
- "lint-staged": "15.2.8",
54
+ "lint-staged": "15.2.10",
55
55
  "mocha": "10.7.3",
56
56
  "mocha-multi-reporters": "1.5.1",
57
- "rollup": "4.20.0",
57
+ "rollup": "4.21.2",
58
58
  "rollup-plugin-cleanup": "3.2.1",
59
+ "rollup-plugin-checksum": "1.0.1",
59
60
  "rollup-plugin-eslint-bundle": "9.0.0",
60
- "semantic-release": "24.0.0",
61
+ "semantic-release": "24.1.0",
61
62
  "web-vitals": "4.2.3"
62
63
  },
63
64
  "lint-staged": {
package/rollup.config.js CHANGED
@@ -11,7 +11,9 @@
11
11
  */
12
12
 
13
13
  import cleanup from 'rollup-plugin-cleanup';
14
- import eslint from 'rollup-plugin-eslint-bundle';
14
+ import pkg from 'rollup-plugin-checksum';
15
+
16
+ const checksum = pkg.default;
15
17
 
16
18
  const banner = `/*
17
19
  * Copyright 2024 Adobe. All rights reserved.
@@ -23,10 +25,7 @@ const banner = `/*
23
25
  * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
24
26
  * OF ANY KIND, either express or implied. See the License for the specific language
25
27
  * governing permissions and limitations under the License.
26
- */
27
-
28
- /* eslint-disable max-classes-per-file, wrap-iife */
29
- // eslint-disable-next-line func-names`;
28
+ */`;
30
29
 
31
30
  const bundles = [
32
31
  {
@@ -55,13 +54,12 @@ export default [...bundles.map(({ outputFile, source }) => ({
55
54
  ],
56
55
  plugins: [
57
56
  cleanup({
58
- comments: ['eslint', 'jsdoc', /^\//, /^\*(?!\sc8\s)(?!\n \* Copyright)/],
59
- maxEmptyLines: -1,
57
+ comments: [],
58
+ maxEmptyLines: 0,
60
59
  }),
61
- eslint({
62
- eslintOptions: {
63
- fix: true,
64
- },
60
+ checksum({
61
+ filename: `${outputFile.split('/').pop()}.md5`,
62
+ includeAssets: false,
65
63
  }),
66
64
  ],
67
65
  }))];
package/src/index.js CHANGED
@@ -9,9 +9,6 @@
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
-
13
- /* eslint-disable max-classes-per-file, wrap-iife */
14
- // eslint-disable-next-line func-names
15
12
  (function () {
16
13
  'use strict';
17
14
 
@@ -21,10 +18,10 @@
21
18
  .reduce((a, b) => a + b, 1) % 1371) !== -1
22
19
  || !!window.origin.match(/localhost/),
23
20
  enabled: (flag, callback) => fflags.has(flag) && callback(),
24
-
25
21
  disabled: (flag, callback) => !fflags.has(flag) && callback(),
26
22
  eagercwv: [683],
27
23
  example: [543, 770, 1136],
24
+ language: [543, 959, 1139, 620],
28
25
  };
29
26
 
30
27
  const KNOWN_PROPERTIES = ['weight', 'id', 'referer', 'checkpoint', 't', 'source', 'target', 'cwv', 'CLS', 'FID', 'LCP', 'INP', 'TTFB'];
@@ -32,26 +29,8 @@
32
29
  fflags.enabled('example', () => DEFAULT_TRACKING_EVENTS.push('example'));
33
30
 
34
31
  const urlSanitizers = {
35
- /**
36
- * Returns the full url.
37
- * If no url is provided, it defaults to window.location.href.
38
- * @param {string} url (default: window.location.href) The url to sanitize
39
- * @returns {string} The sanitized url
40
- */
41
32
  full: (url = window.location.href) => new URL(url).toString(),
42
- /**
43
- * Returns the origin of the provided url.
44
- * If no url is provided, it defaults to window.location.href.
45
- * @param {string} url (default: window.location.href) The url to sanitize
46
- * @returns {string} The sanitized url
47
- */
48
33
  origin: (url = window.location.href) => new URL(url).origin,
49
- /**
50
- * Returns the sanitized url: the origin and the path (no query params or hash)
51
- * If no url is provided, it defaults to window.location.href.
52
- * @param {string} url (default: window.location.href) The url to sanitize
53
- * @returns {string} The sanitized url
54
- */
55
34
  path: (url = window.location.href) => {
56
35
  const u = new URL(url);
57
36
  return `${u.origin}${u.pathname}`;
@@ -60,7 +39,6 @@
60
39
 
61
40
  const getTargetValue = (element) => element.getAttribute('data-rum-target') || element.getAttribute('href')
62
41
  || element.currentSrc || element.getAttribute('src') || element.dataset.action || element.action;
63
-
64
42
  const targetSelector = (element) => {
65
43
  try {
66
44
  if (!element) return undefined;
@@ -69,7 +47,6 @@
69
47
  value = getTargetValue(element.closest('a'));
70
48
  }
71
49
  if (value && !value.startsWith('https://')) {
72
- // resolve relative links
73
50
  value = new URL(value, window.location).href;
74
51
  }
75
52
  return value;
@@ -77,7 +54,51 @@
77
54
  return null;
78
55
  }
79
56
  };
80
-
57
+ function walk(element, checkFn) {
58
+ if (!element || element === document.body || element === document.documentElement) {
59
+ return undefined;
60
+ }
61
+ const checkValue = checkFn(element);
62
+ return checkValue || walk(element.parentElement, checkFn);
63
+ }
64
+ function isDialog(element) {
65
+ if (element.tagName === 'DIALOG') return true;
66
+ if (element.getAttribute('role') === 'dialog') return true;
67
+ if (element.getAttribute('role') === 'alertdialog') return true;
68
+ if (element.getAttribute('aria-modal') === 'true') return true;
69
+ const computedStyle = window.getComputedStyle(element);
70
+ return (computedStyle && computedStyle.position === 'fixed' && computedStyle.zIndex > 100);
71
+ }
72
+ function isButton(element) {
73
+ if (element.tagName === 'BUTTON') return true;
74
+ if (element.tagName === 'INPUT' && element.getAttribute('type') === 'button') return true;
75
+ if (element.tagName === 'A') {
76
+ const classes = Array.from(element.classList);
77
+ return classes.some((className) => className.match(/button|cta/));
78
+ }
79
+ return element.getAttribute('role') === 'button';
80
+ }
81
+ function getSourceContext(element) {
82
+ if (element.closest('form')) return 'form';
83
+ const block = element.closest('.block[data-block-name]');
84
+ if (block) return `.${block.getAttribute('data-block-name')}`;
85
+ if (walk(element, isDialog)) return 'dialog';
86
+ if (element.closest('nav')) return 'nav';
87
+ if (element.closest('header')) return 'header';
88
+ if (element.closest('footer')) return 'footer';
89
+ if (element.closest('aside')) return 'aside';
90
+ return (walk(element, (e) => e.id && `#${e.id}`));
91
+ }
92
+ function getSourceElement(element) {
93
+ if (element.closest('form') && Array.from(element.closest('form').elements).includes(element)) return element.tagName.toLowerCase() + (element.tagName === 'INPUT' ? `[type='${element.getAttribute('type') || ''}']` : '');
94
+ if (walk(element, isButton)) return 'button';
95
+ return element.tagName.toLowerCase().match(/^(a|img|video)$/) && element.tagName.toLowerCase();
96
+ }
97
+ function getSourceIdentifier(element) {
98
+ if (element.id) return `#${element.id}`;
99
+ if (element.getAttribute('data-block-name')) return `.${element.getAttribute('data-block-name')}`;
100
+ return (element.classList.length > 0 && `.${element.classList[0]}`);
101
+ }
81
102
  const sourceSelector = (element) => {
82
103
  try {
83
104
  if (!element || element === document.body || element === document.documentElement) {
@@ -86,41 +107,10 @@
86
107
  if (element.getAttribute('data-rum-source')) {
87
108
  return element.getAttribute('data-rum-source');
88
109
  }
89
- const form = element.closest('form');
90
- let formElementSelector = '';
91
- if (form && Array.from(form.elements).includes(element)) {
92
- formElementSelector = element.tagName === 'INPUT' ? `form input[type='${element.getAttribute('type')}']` : `form ${element.tagName.toLowerCase()}`;
93
- }
94
-
95
- const blockName = element.closest('.block') ? element.closest('.block').getAttribute('data-block-name') : '';
96
- if (element.id || formElementSelector) {
97
- const id = element.id ? `#${element.id}` : '';
98
- return blockName ? `.${blockName} ${formElementSelector}${id}` : `${formElementSelector}${id}`;
99
- }
100
-
101
- if (element.getAttribute('data-block-name')) {
102
- return `.${element.getAttribute('data-block-name')}`;
103
- }
104
-
105
- const classes = Array.from(element.classList);
106
- const label = element.tagName.toLowerCase();
107
- const firstClass = classes.length > 0 ? `.${classes[0]}` : '';
108
- const labelWithClass = `${element.tagName.toLowerCase()}${firstClass}`;
109
- if (element.tagName.toLowerCase() === 'button'
110
- || element.type === 'button'
111
- || classes.some((className) => className.match(/button|cta/))) {
112
- let parent = element.parentElement;
113
- if (!parent) return labelWithClass;
114
- if (parent.id) return `#${parent.id} ${label}`;
115
- while (parent.tagName !== 'BODY' && !parent.id) parent = parent.parentElement;
116
- if (parent.id) return `#${parent.id} ${labelWithClass}`;
117
- return blockName ? `.${blockName} ${labelWithClass}` : labelWithClass;
118
- }
119
-
120
- const parent = sourceSelector(element.parentElement);
121
- if (parent) return parent;
122
-
123
- return labelWithClass;
110
+ const context = getSourceContext(element.parentElement) || '';
111
+ const elementName = getSourceElement(element) || '';
112
+ const identifier = getSourceIdentifier(element) || '';
113
+ return `${context} ${elementName}${identifier}`.trim() || `"${element.textContent.substring(0, 10)}"`;
124
114
  } catch (error) {
125
115
  return null;
126
116
  }
@@ -130,12 +120,10 @@
130
120
  const cmpCookie = document.cookie.split(';')
131
121
  .map((c) => c.trim())
132
122
  .find((cookie) => cookie.startsWith('OptanonAlertBoxClosed='));
133
-
134
123
  if (cmpCookie) {
135
124
  sampleRUM('consent', { source: 'onetrust', target: 'hidden' });
136
125
  return;
137
126
  }
138
-
139
127
  let consentMutationObserver;
140
128
  const trackShowConsent = () => {
141
129
  if (document.querySelector('body > div#onetrust-consent-sdk')) {
@@ -147,27 +135,22 @@
147
135
  }
148
136
  return false;
149
137
  };
150
-
151
138
  if (!trackShowConsent()) {
152
- // eslint-disable-next-line max-len
153
139
  consentMutationObserver = window.MutationObserver
154
140
  ? new MutationObserver(trackShowConsent)
155
- : null;
141
+ : null;
156
142
  if (consentMutationObserver) {
157
143
  consentMutationObserver.observe(
158
144
  document.body,
159
- // eslint-disable-next-line object-curly-newline
160
145
  { attributes: false, childList: true, subtree: false },
161
146
  );
162
147
  }
163
148
  }
164
149
  }
165
-
166
150
  function addUTMParametersTracking(sampleRUM) {
167
151
  const usp = new URLSearchParams(window.location.search);
168
152
  [...usp.entries()]
169
153
  .filter(([key]) => key.startsWith('utm_'))
170
- // exclude keys that may leak PII
171
154
  .filter(([key]) => key !== 'utm_id')
172
155
  .filter(([key]) => key !== 'utm_term')
173
156
  .forEach(([source, target]) => sampleRUM('utm', { source, target }));
@@ -200,18 +183,16 @@
200
183
  }
201
184
 
202
185
  const { sampleRUM, queue, isSelected } = (window.hlx && window.hlx.rum) ? window.hlx.rum
203
- : {};
204
-
186
+ : {};
205
187
  const formSubmitListener = (e) => sampleRUM('formsubmit', { target: targetSelector(e.target), source: sourceSelector(e.target) });
206
- // eslint-disable-next-line no-use-before-define
207
- const mutationObserver = window.MutationObserver ? new MutationObserver(mutationsCallback)
208
- : null;
209
-
188
+ const blocksMutationObserver = window.MutationObserver ? new MutationObserver(blocksMutationsCallback)
189
+ : {};
190
+ const mediaMutationObserver = window.MutationObserver ? new MutationObserver(mediaMutationsCallback)
191
+ : {};
210
192
  function trackCheckpoint(checkpoint, data, t) {
211
193
  const { weight, id } = window.hlx.rum;
212
194
  if (isSelected) {
213
195
  const sendPing = (pdata = data) => {
214
- // eslint-disable-next-line object-curly-newline, max-len
215
196
  const body = JSON.stringify({ weight, id, referer: urlSanitizers[window.hlx.RUM_MASK_URL || 'path'](), checkpoint, t, ...data }, KNOWN_PROPERTIES);
216
197
  const { href: url, origin } = new URL(`.rum/${weight}`, sampleRUM.collectBaseURL || sampleRUM.baseURL);
217
198
  if (window.location.origin === origin) {
@@ -220,26 +201,22 @@
220
201
  } else {
221
202
  navigator.sendBeacon(url, body);
222
203
  }
223
- // eslint-disable-next-line no-console
224
204
  console.debug(`ping:${checkpoint}`, pdata);
225
205
  };
226
206
  sendPing(data);
227
207
  }
228
208
  }
229
-
230
209
  function processQueue() {
231
- while (queue.length) {
210
+ while (queue && queue.length) {
232
211
  const ck = queue.shift();
233
212
  trackCheckpoint(...ck);
234
213
  }
235
214
  }
236
-
237
215
  function addCWVTracking() {
238
216
  setTimeout(() => {
239
217
  try {
240
218
  const cwvScript = new URL('.rum/web-vitals/dist/web-vitals.iife.js', sampleRUM.baseURL).href;
241
219
  if (document.querySelector(`script[src="${cwvScript}"]`)) {
242
- // web vitals script has been loaded already
243
220
  return;
244
221
  }
245
222
  const script = document.createElement('script');
@@ -255,11 +232,7 @@
255
232
  }
256
233
  sampleRUM('cwv', data);
257
234
  };
258
-
259
235
  const isEager = (metric) => ['CLS', 'LCP'].includes(metric);
260
-
261
- // When loading `web-vitals` using a classic script, all the public
262
- // methods can be found on the `webVitals` global namespace.
263
236
  ['FID', 'INP', 'TTFB', 'CLS', 'LCP'].forEach((metric) => {
264
237
  const metricFn = window.webVitals[`on${metric}`];
265
238
  if (typeof metricFn === 'function') {
@@ -273,32 +246,26 @@
273
246
  };
274
247
  document.head.appendChild(script);
275
248
  } catch (error) {
276
- // something went wrong
277
249
  }
278
- }, 2000); // wait for delayed
250
+ }, 2000);
279
251
  }
280
-
281
252
  function addNavigationTracking() {
282
- // enter checkpoint when referrer is not the current page url
283
253
  const navigate = (source, type) => {
284
254
  const payload = { source, target: document.visibilityState };
285
- // reload: same page, navigate: same origin, enter: everything else
286
255
  if (type === 'reload' || source === window.location.href) {
287
256
  sampleRUM('reload', payload);
288
257
  } else if (type && type !== 'navigate') {
289
- sampleRUM(type, payload); // back, forward, prerender, etc.
258
+ sampleRUM(type, payload);
290
259
  } else if (source && window.location.origin === new URL(source).origin) {
291
- sampleRUM('navigate', payload); // internal navigation
260
+ sampleRUM('navigate', payload);
292
261
  } else {
293
- sampleRUM('enter', payload); // enter site
262
+ sampleRUM('enter', payload);
294
263
  }
295
264
  };
296
-
297
265
  new PerformanceObserver((list) => list
298
266
  .getEntries().map((entry) => navigate(window.hlx.referrer || document.referrer, entry.type)))
299
267
  .observe({ type: 'navigation', buffered: true });
300
268
  }
301
-
302
269
  function addLoadResourceTracking() {
303
270
  const observer = new PerformanceObserver((list) => {
304
271
  try {
@@ -315,39 +282,47 @@
315
282
  sampleRUM('missingresource', { source: entry.name, target: entry.hostname });
316
283
  });
317
284
  } catch (error) {
318
- // something went wrong
319
285
  }
320
286
  });
321
287
  observer.observe({ type: 'resource', buffered: true });
322
288
  }
323
-
324
289
  function activateBlocksMutationObserver() {
325
- if (!mutationObserver || mutationObserver.active) {
290
+ if (!blocksMutationObserver || blocksMutationObserver.active) {
326
291
  return;
327
292
  }
328
- mutationObserver.active = true;
329
- mutationObserver.observe(
293
+ blocksMutationObserver.active = true;
294
+ blocksMutationObserver.observe(
330
295
  document.body,
331
- // eslint-disable-next-line object-curly-newline
332
296
  { subtree: true, attributes: true, attributeFilter: ['data-block-status'] },
333
297
  );
334
298
  }
335
-
299
+ function activateMediaMutationObserver() {
300
+ if (!mediaMutationObserver || mediaMutationObserver.active) {
301
+ return;
302
+ }
303
+ mediaMutationObserver.active = true;
304
+ mediaMutationObserver.observe(
305
+ document.body,
306
+ { subtree: true, attributes: false, childList: true },
307
+ );
308
+ }
336
309
  function getIntersectionObsever(checkpoint) {
337
- if (!window.IntersectionObserver) return null;
310
+ if (!window.IntersectionObserver) {
311
+ return null;
312
+ }
338
313
  activateBlocksMutationObserver();
314
+ activateMediaMutationObserver();
339
315
  const observer = new IntersectionObserver((entries) => {
340
316
  try {
341
317
  entries
342
318
  .filter((entry) => entry.isIntersecting)
343
319
  .forEach((entry) => {
344
- observer.unobserve(entry.target); // observe only once
320
+ observer.unobserve(entry.target);
345
321
  const target = targetSelector(entry.target);
346
322
  const source = sourceSelector(entry.target);
347
323
  sampleRUM(checkpoint, { target, source });
348
324
  });
349
325
  } catch (error) {
350
- // something went wrong
351
326
  }
352
327
  });
353
328
  return observer;
@@ -359,37 +334,44 @@
359
334
  blocks.forEach((b) => blockobserver.observe(b));
360
335
  }
361
336
  }
362
-
337
+ const observedMedia = new Set();
363
338
  function addViewMediaTracking(parent) {
364
339
  const mediaobserver = getIntersectionObsever('viewmedia');
365
340
  if (mediaobserver) {
366
341
  parent.querySelectorAll('img, video, audio, iframe').forEach((m) => {
367
- if (!m.closest('div .block') || m.closest('div[data-block-status="loaded"]')) {
342
+ if (!observedMedia.has(m)) {
343
+ observedMedia.add(m);
368
344
  mediaobserver.observe(m);
369
345
  }
370
346
  });
371
347
  }
372
348
  }
373
-
374
349
  function addFormTracking(parent) {
375
350
  activateBlocksMutationObserver();
351
+ activateMediaMutationObserver();
376
352
  parent.querySelectorAll('form').forEach((form) => {
377
- form.removeEventListener('submit', formSubmitListener); // listen only once
353
+ form.removeEventListener('submit', formSubmitListener);
378
354
  form.addEventListener('submit', formSubmitListener);
379
355
  });
380
356
  }
381
-
382
- const addObserver = (ck, fn, block) => DEFAULT_TRACKING_EVENTS.includes(ck) && fn(block);
383
- function mutationsCallback(mutations) {
384
- mutations.filter((m) => m.type === 'attributes' && m.attributeName === 'data-block-status')
357
+ function addObserver(ck, fn, block) {
358
+ return DEFAULT_TRACKING_EVENTS.includes(ck) && fn(block);
359
+ }
360
+ function blocksMutationsCallback(mutations) {
361
+ mutations
362
+ .filter((m) => m.type === 'attributes' && m.attributeName === 'data-block-status')
385
363
  .filter((m) => m.target.dataset.blockStatus === 'loaded')
386
364
  .forEach((m) => {
387
365
  addObserver('form', addFormTracking, m.target);
388
366
  addObserver('viewblock', addViewBlockTracking, m.target);
367
+ });
368
+ }
369
+ function mediaMutationsCallback(mutations) {
370
+ mutations
371
+ .forEach((m) => {
389
372
  addObserver('viewmedia', addViewMediaTracking, m.target);
390
373
  });
391
374
  }
392
-
393
375
  function addTrackingFromConfig() {
394
376
  document.addEventListener('click', (event) => {
395
377
  sampleRUM('click', { target: targetSelector(event.target), source: sourceSelector(event.target) });
@@ -404,8 +386,12 @@
404
386
  addCookieConsentTracking(sampleRUM);
405
387
  addAdsParametersTracking(sampleRUM);
406
388
  addEmailParameterTracking(sampleRUM);
389
+ fflags.enabled('language', () => {
390
+ const target = navigator.language;
391
+ const source = document.documentElement.lang;
392
+ sampleRUM('language', { source, target });
393
+ });
407
394
  }
408
-
409
395
  function initEnhancer() {
410
396
  try {
411
397
  if (sampleRUM) {
@@ -414,9 +400,8 @@
414
400
  processQueue();
415
401
  }
416
402
  } catch (error) {
417
- // something went wrong
418
403
  }
419
404
  }
420
-
421
405
  initEnhancer();
406
+
422
407
  })();
package/src/index.md5 ADDED
@@ -0,0 +1 @@
1
+ 65cf540e7fe93b6d3d67567cbf215544
@@ -43,7 +43,7 @@ export default {
43
43
  context.url = '/node_modules/@adobe/helix-rum-js/dist/rum-standalone.js';
44
44
  await next();
45
45
  context.body = context.body
46
- .replace(/const weight.*/, 'const weight = 1;')
46
+ .replace(/const weight.*/, 'const weight = 1')
47
47
  .replace(/navigator\.sendBeacon/g, 'fakeSendBeacon')
48
48
  .replace('.rum/@adobe/helix-rum-enhancer@^2/src/index.js', 'src/index.map.js');
49
49
  return true;