@adobe/helix-rum-enhancer 2.20.0 → 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/.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,18 @@
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
+
1
16
  # [2.20.0](https://github.com/adobe/helix-rum-enhancer/compare/v2.19.2...v2.20.0) (2024-08-19)
2
17
 
3
18
 
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
@@ -267,6 +267,11 @@ function addTrackingFromConfig() {
267
267
  addCookieConsentTracking(sampleRUM);
268
268
  addAdsParametersTracking(sampleRUM);
269
269
  addEmailParameterTracking(sampleRUM);
270
+ fflags.enabled('language', () => {
271
+ const target = navigator.language;
272
+ const source = document.documentElement.lang;
273
+ sampleRUM('language', { source, target });
274
+ });
270
275
  }
271
276
 
272
277
  function initEnhancer() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/helix-rum-enhancer",
3
- "version": "2.20.0",
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,15 +47,16 @@
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.9",
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
61
  "semantic-release": "24.1.0",
61
62
  "web-vitals": "4.2.3"
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,23 +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
-
207
- // eslint-disable-next-line no-use-before-define, max-len
208
188
  const blocksMutationObserver = window.MutationObserver ? new MutationObserver(blocksMutationsCallback)
209
- : {};
210
-
211
- // eslint-disable-next-line no-use-before-define, max-len
189
+ : {};
212
190
  const mediaMutationObserver = window.MutationObserver ? new MutationObserver(mediaMutationsCallback)
213
- : {};
214
-
191
+ : {};
215
192
  function trackCheckpoint(checkpoint, data, t) {
216
193
  const { weight, id } = window.hlx.rum;
217
194
  if (isSelected) {
218
195
  const sendPing = (pdata = data) => {
219
- // eslint-disable-next-line object-curly-newline, max-len
220
196
  const body = JSON.stringify({ weight, id, referer: urlSanitizers[window.hlx.RUM_MASK_URL || 'path'](), checkpoint, t, ...data }, KNOWN_PROPERTIES);
221
197
  const { href: url, origin } = new URL(`.rum/${weight}`, sampleRUM.collectBaseURL || sampleRUM.baseURL);
222
198
  if (window.location.origin === origin) {
@@ -225,26 +201,22 @@
225
201
  } else {
226
202
  navigator.sendBeacon(url, body);
227
203
  }
228
- // eslint-disable-next-line no-console
229
204
  console.debug(`ping:${checkpoint}`, pdata);
230
205
  };
231
206
  sendPing(data);
232
207
  }
233
208
  }
234
-
235
209
  function processQueue() {
236
210
  while (queue && queue.length) {
237
211
  const ck = queue.shift();
238
212
  trackCheckpoint(...ck);
239
213
  }
240
214
  }
241
-
242
215
  function addCWVTracking() {
243
216
  setTimeout(() => {
244
217
  try {
245
218
  const cwvScript = new URL('.rum/web-vitals/dist/web-vitals.iife.js', sampleRUM.baseURL).href;
246
219
  if (document.querySelector(`script[src="${cwvScript}"]`)) {
247
- // web vitals script has been loaded already
248
220
  return;
249
221
  }
250
222
  const script = document.createElement('script');
@@ -260,11 +232,7 @@
260
232
  }
261
233
  sampleRUM('cwv', data);
262
234
  };
263
-
264
235
  const isEager = (metric) => ['CLS', 'LCP'].includes(metric);
265
-
266
- // When loading `web-vitals` using a classic script, all the public
267
- // methods can be found on the `webVitals` global namespace.
268
236
  ['FID', 'INP', 'TTFB', 'CLS', 'LCP'].forEach((metric) => {
269
237
  const metricFn = window.webVitals[`on${metric}`];
270
238
  if (typeof metricFn === 'function') {
@@ -278,32 +246,26 @@
278
246
  };
279
247
  document.head.appendChild(script);
280
248
  } catch (error) {
281
- // something went wrong
282
249
  }
283
- }, 2000); // wait for delayed
250
+ }, 2000);
284
251
  }
285
-
286
252
  function addNavigationTracking() {
287
- // enter checkpoint when referrer is not the current page url
288
253
  const navigate = (source, type) => {
289
254
  const payload = { source, target: document.visibilityState };
290
- // reload: same page, navigate: same origin, enter: everything else
291
255
  if (type === 'reload' || source === window.location.href) {
292
256
  sampleRUM('reload', payload);
293
257
  } else if (type && type !== 'navigate') {
294
- sampleRUM(type, payload); // back, forward, prerender, etc.
258
+ sampleRUM(type, payload);
295
259
  } else if (source && window.location.origin === new URL(source).origin) {
296
- sampleRUM('navigate', payload); // internal navigation
260
+ sampleRUM('navigate', payload);
297
261
  } else {
298
- sampleRUM('enter', payload); // enter site
262
+ sampleRUM('enter', payload);
299
263
  }
300
264
  };
301
-
302
265
  new PerformanceObserver((list) => list
303
266
  .getEntries().map((entry) => navigate(window.hlx.referrer || document.referrer, entry.type)))
304
267
  .observe({ type: 'navigation', buffered: true });
305
268
  }
306
-
307
269
  function addLoadResourceTracking() {
308
270
  const observer = new PerformanceObserver((list) => {
309
271
  try {
@@ -320,12 +282,10 @@
320
282
  sampleRUM('missingresource', { source: entry.name, target: entry.hostname });
321
283
  });
322
284
  } catch (error) {
323
- // something went wrong
324
285
  }
325
286
  });
326
287
  observer.observe({ type: 'resource', buffered: true });
327
288
  }
328
-
329
289
  function activateBlocksMutationObserver() {
330
290
  if (!blocksMutationObserver || blocksMutationObserver.active) {
331
291
  return;
@@ -333,11 +293,9 @@
333
293
  blocksMutationObserver.active = true;
334
294
  blocksMutationObserver.observe(
335
295
  document.body,
336
- // eslint-disable-next-line object-curly-newline
337
296
  { subtree: true, attributes: true, attributeFilter: ['data-block-status'] },
338
297
  );
339
298
  }
340
-
341
299
  function activateMediaMutationObserver() {
342
300
  if (!mediaMutationObserver || mediaMutationObserver.active) {
343
301
  return;
@@ -345,11 +303,9 @@
345
303
  mediaMutationObserver.active = true;
346
304
  mediaMutationObserver.observe(
347
305
  document.body,
348
- // eslint-disable-next-line object-curly-newline
349
306
  { subtree: true, attributes: false, childList: true },
350
307
  );
351
308
  }
352
-
353
309
  function getIntersectionObsever(checkpoint) {
354
310
  if (!window.IntersectionObserver) {
355
311
  return null;
@@ -361,13 +317,12 @@
361
317
  entries
362
318
  .filter((entry) => entry.isIntersecting)
363
319
  .forEach((entry) => {
364
- observer.unobserve(entry.target); // observe only once
320
+ observer.unobserve(entry.target);
365
321
  const target = targetSelector(entry.target);
366
322
  const source = sourceSelector(entry.target);
367
323
  sampleRUM(checkpoint, { target, source });
368
324
  });
369
325
  } catch (error) {
370
- // something went wrong
371
326
  }
372
327
  });
373
328
  return observer;
@@ -379,7 +334,6 @@
379
334
  blocks.forEach((b) => blockobserver.observe(b));
380
335
  }
381
336
  }
382
-
383
337
  const observedMedia = new Set();
384
338
  function addViewMediaTracking(parent) {
385
339
  const mediaobserver = getIntersectionObsever('viewmedia');
@@ -392,22 +346,18 @@
392
346
  });
393
347
  }
394
348
  }
395
-
396
349
  function addFormTracking(parent) {
397
350
  activateBlocksMutationObserver();
398
351
  activateMediaMutationObserver();
399
352
  parent.querySelectorAll('form').forEach((form) => {
400
- form.removeEventListener('submit', formSubmitListener); // listen only once
353
+ form.removeEventListener('submit', formSubmitListener);
401
354
  form.addEventListener('submit', formSubmitListener);
402
355
  });
403
356
  }
404
-
405
357
  function addObserver(ck, fn, block) {
406
358
  return DEFAULT_TRACKING_EVENTS.includes(ck) && fn(block);
407
359
  }
408
-
409
360
  function blocksMutationsCallback(mutations) {
410
- // block specific mutations
411
361
  mutations
412
362
  .filter((m) => m.type === 'attributes' && m.attributeName === 'data-block-status')
413
363
  .filter((m) => m.target.dataset.blockStatus === 'loaded')
@@ -416,15 +366,12 @@
416
366
  addObserver('viewblock', addViewBlockTracking, m.target);
417
367
  });
418
368
  }
419
-
420
369
  function mediaMutationsCallback(mutations) {
421
- // media mutations
422
370
  mutations
423
371
  .forEach((m) => {
424
372
  addObserver('viewmedia', addViewMediaTracking, m.target);
425
373
  });
426
374
  }
427
-
428
375
  function addTrackingFromConfig() {
429
376
  document.addEventListener('click', (event) => {
430
377
  sampleRUM('click', { target: targetSelector(event.target), source: sourceSelector(event.target) });
@@ -439,8 +386,12 @@
439
386
  addCookieConsentTracking(sampleRUM);
440
387
  addAdsParametersTracking(sampleRUM);
441
388
  addEmailParameterTracking(sampleRUM);
389
+ fflags.enabled('language', () => {
390
+ const target = navigator.language;
391
+ const source = document.documentElement.lang;
392
+ sampleRUM('language', { source, target });
393
+ });
442
394
  }
443
-
444
395
  function initEnhancer() {
445
396
  try {
446
397
  if (sampleRUM) {
@@ -449,9 +400,8 @@
449
400
  processQueue();
450
401
  }
451
402
  } catch (error) {
452
- // something went wrong
453
403
  }
454
404
  }
455
-
456
405
  initEnhancer();
406
+
457
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;