@adobe/helix-rum-enhancer 1.11.0 → 2.1.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,16 +1,28 @@
1
- # [1.11.0](https://github.com/adobe/helix-rum-enhancer/compare/v1.10.0...v1.11.0) (2024-01-17)
1
+ # [2.1.0](https://github.com/adobe/helix-rum-enhancer/compare/v2.0.0...v2.1.0) (2024-01-22)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **loadresource:** enable loadresource checkpoint for all origins ([0be8de7](https://github.com/adobe/helix-rum-enhancer/commit/0be8de7ee3e57afdd60a2b9af743bedd061e907d))
2
7
 
3
8
 
4
9
  ### Features
5
10
 
6
- * **observer:** enable tracking of JSON loading for all browsers that have a PerformanceObserver implementation ([c049f93](https://github.com/adobe/helix-rum-enhancer/commit/c049f930f0cc1dc548e42bd8e76cd97709a873d7))
11
+ * **observer:** enable tracking of JSON loading for all browsers that have a PerformanceObserver implementation ([45b5e64](https://github.com/adobe/helix-rum-enhancer/commit/45b5e6428fb9c51e8fc4d477c574d44a361b3ec2))
7
12
 
8
- # [1.10.0](https://github.com/adobe/helix-rum-enhancer/compare/v1.9.1...v1.10.0) (2024-01-16)
13
+ # [2.0.0](https://github.com/adobe/helix-rum-enhancer/compare/v1.9.1...v2.0.0) (2024-01-16)
9
14
 
10
15
 
11
16
  ### Features
12
17
 
13
- * **sourceselector:** highlight clicks on buttons and CTAs ([4cd62b8](https://github.com/adobe/helix-rum-enhancer/commit/4cd62b86eb08c496712deba42d3f011b8cbac5c6))
18
+ * prepare compatibility with @adobe/helix-rum-js@2 ([b1cef07](https://github.com/adobe/helix-rum-enhancer/commit/b1cef07ec59c8134056a9aaf25a331016aeffe39))
19
+ * prepare compatibility with @adobe/helix-rum-js@2 ([702a873](https://github.com/adobe/helix-rum-enhancer/commit/702a8731d783850d26b2daae3a839e7ee4192e47))
20
+
21
+
22
+ ### BREAKING CHANGES
23
+
24
+ * this version can only be used from helix-rum-js@v2 and up
25
+ * this version can only be used from helix-rum-js@v2 and up
14
26
 
15
27
  ## [1.9.1](https://github.com/adobe/helix-rum-enhancer/compare/v1.9.0...v1.9.1) (2024-01-11)
16
28
 
package/README.md CHANGED
@@ -4,10 +4,9 @@
4
4
 
5
5
  ## Status
6
6
  [![codecov](https://img.shields.io/codecov/c/github/adobe/helix-rum-enhancer.svg)](https://codecov.io/gh/adobe/helix-rum-enhancer)
7
- [![CircleCI](https://img.shields.io/circleci/project/github/adobe/helix-rum-enhancer.svg)](https://circleci.com/gh/adobe/helix-rum-enhancer)
7
+ [![GitHub Actions](https://img.shields.io/github/actions/workflow/status/adobe/helix-rum-enhancer/main.yaml)](https://github.com/adobe/helix-rum-enhancer/actions/workflows/main.yaml)
8
8
  [![GitHub license](https://img.shields.io/github/license/adobe/helix-rum-enhancer.svg)](https://github.com/adobe/helix-rum-enhancer/blob/master/LICENSE.txt)
9
9
  [![GitHub issues](https://img.shields.io/github/issues/adobe/helix-rum-enhancer.svg)](https://github.com/adobe/helix-rum-enhancer/issues)
10
- [![LGTM Code Quality Grade: JavaScript](https://img.shields.io/lgtm/grade/javascript/g/adobe/helix-rum-enhancer.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/adobe/helix-rum-enhancer)
11
10
  [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release)
12
11
 
13
12
  ## Usage
@@ -17,10 +16,13 @@ This library is meant to be used in conjunction with and loaded by the `sampleRU
17
16
  It will add following new checkpoints:
18
17
 
19
18
  - `cwv`: Core Web Vitals Collection
19
+ - `pagesviewed`: number that helps to estimate how many pages are seen in average by a user.
20
20
  - `click`: A click anywhere in the document
21
21
  - `viewblock`: A Helix block has been scrolled into the viewport
22
22
  - `viewmedia`: An image or video hosted by Helix Media Bus has been scrolled into the viewport
23
- - `enter`: when page loads and the referrer is different than the current page.
23
+ - `reload`, `navigate`, `enter`: depending on how the current page was accessed
24
+ - `formsubmit`: when a form is submitted
25
+ - `leave`: when the user leaves the page
24
26
 
25
27
  ## Development
26
28
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/helix-rum-enhancer",
3
- "version": "1.11.0",
3
+ "version": "2.1.0",
4
4
  "description": "Helix RUM Enhancer",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -9,6 +9,7 @@
9
9
  "lint": "eslint .",
10
10
  "docs": "npx jsdoc2md -c .jsdoc.json --files 'src/*.js' > docs/API.md",
11
11
  "semantic-release": "semantic-release",
12
+ "semantic-release-dry": "semantic-release --dry-run --branches $CI_BRANCH",
12
13
  "prepare": "husky install"
13
14
  },
14
15
  "repository": {
@@ -32,7 +33,7 @@
32
33
  "@semantic-release/changelog": "6.0.3",
33
34
  "@semantic-release/git": "10.0.1",
34
35
  "@semantic-release/npm": "11.0.2",
35
- "c8": "9.0.0",
36
+ "c8": "9.1.0",
36
37
  "codecov": "3.8.3",
37
38
  "eslint": "8.56.0",
38
39
  "eslint-plugin-header": "3.1.1",
package/src/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright 2022 Adobe. All rights reserved.
2
+ * Copyright 2023 Adobe. All rights reserved.
3
3
  * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
4
  * you may not use this file except in compliance with the License. You may obtain a copy
5
5
  * of the License at http://www.apache.org/licenses/LICENSE-2.0
@@ -9,66 +9,21 @@
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
- const { sampleRUM } = window.hlx.rum;
13
-
14
- sampleRUM.baseURL = sampleRUM.baseURL || new URL('https://rum.hlx.page');
15
-
16
- sampleRUM.blockobserver = (window.IntersectionObserver) ? new IntersectionObserver((entries) => {
17
- entries
18
- .filter((entry) => entry.isIntersecting)
19
- .forEach((entry) => {
20
- sampleRUM.blockobserver.unobserve(entry.target); // observe only once
21
- const target = sampleRUM.targetselector(entry.target);
22
- const source = sampleRUM.sourceselector(entry.target);
23
- sampleRUM('viewblock', { target, source });
24
- });
25
- }, { threshold: 0.25 }) : { observe: () => { } };
26
-
27
- sampleRUM.mediaobserver = (window.IntersectionObserver) ? new IntersectionObserver((entries) => {
28
- entries
29
- .filter((entry) => entry.isIntersecting)
30
- .forEach((entry) => {
31
- sampleRUM.mediaobserver.unobserve(entry.target); // observe only once
32
- const target = sampleRUM.targetselector(entry.target);
33
- const source = sampleRUM.sourceselector(entry.target);
34
- sampleRUM('viewmedia', { target, source });
35
- });
36
- }, { threshold: 0.25 }) : { observe: () => { } };
37
-
38
- sampleRUM.drain('observe', ((elements) => {
39
- elements.forEach((element) => {
40
- if (element.tagName.toLowerCase() === 'img'
41
- || element.tagName.toLowerCase() === 'video'
42
- || element.tagName.toLowerCase() === 'audio'
43
- || element.tagName.toLowerCase() === 'iframe') {
44
- sampleRUM.mediaobserver.observe(element);
45
- } else {
46
- sampleRUM.blockobserver.observe(element);
47
- }
48
- });
49
- }));
50
-
51
- const navigate = (source, type) => {
52
- const payload = { source, target: document.visibilityState };
53
- // reload: same page, navigate: same origin, enter: everything else
54
- if (type === 'reload' || source === window.location.href) {
55
- sampleRUM('reload', payload);
56
- } else if (type !== 'navigate') {
57
- sampleRUM(type, payload); // back, forward, prerender, etc.
58
- } else if (source && window.location.origin === new URL(source).origin) {
59
- sampleRUM('navigate', payload); // internal navigation
60
- } else {
61
- sampleRUM('enter', payload); // enter site
62
- }
63
- };
12
+ /* eslint-env browser */
13
+ const KNOWN_PROPERTIES = ['weight', 'id', 'referer', 'checkpoint', 't', 'source', 'target', 'cwv', 'CLS', 'FID', 'LCP', 'INP', 'TTFB'];
14
+ const DEFAULT_TRACKING_EVENTS = ['click', 'cwv', 'form', 'enterleave', 'viewblock', 'viewmedia', 'loadresource'];
15
+ const SESSION_STORAGE_KEY = 'aem-rum';
16
+ const { sampleRUM, queue, isSelected } = window.hlx.rum;
64
17
 
65
- new PerformanceObserver((list) => list
66
- .getEntries().map((entry) => navigate(document.referrer, entry.type)))
67
- .observe({ type: 'navigation', buffered: true });
18
+ const urlSanitizers = {
19
+ full: () => window.location.href,
20
+ origin: () => window.location.origin,
21
+ path: () => window.location.href.replace(/\?.*$/, ''),
22
+ };
68
23
 
69
- sampleRUM.targetselector = (element) => {
70
- let value = element.getAttribute('data-rum-target') || element.getAttribute('href') || element.currentSrc || element.getAttribute('src')
71
- || element.dataset.action || element.action;
24
+ const targetselector = (element) => {
25
+ let value = element.getAttribute('data-rum-target') || element.getAttribute('href')
26
+ || element.currentSrc || element.getAttribute('src') || element.dataset.action || element.action;
72
27
  if (value && !value.startsWith('https://')) {
73
28
  // resolve relative links
74
29
  value = new URL(value, window.location).href;
@@ -76,42 +31,7 @@ sampleRUM.targetselector = (element) => {
76
31
  return value;
77
32
  };
78
33
 
79
- sampleRUM.drain('cwv', (() => {
80
- const cwvScript = new URL('.rum/web-vitals/dist/web-vitals.iife.js', sampleRUM.baseURL).href;
81
- if (document.querySelector(`script[src="${cwvScript}"]`)) {
82
- // web vitals script has been loaded already
83
- return;
84
- }
85
- // use classic script to avoid CORS issues
86
- const script = document.createElement('script');
87
- script.src = cwvScript;
88
- script.onload = () => {
89
- const storeCWV = (measurement) => {
90
- const data = { cwv: {} };
91
- data.cwv[measurement.name] = measurement.value;
92
- sampleRUM('cwv', data);
93
- };
94
- // When loading `web-vitals` using a classic script, all the public
95
- // methods can be found on the `webVitals` global namespace.
96
- ['CLS', 'FID', 'LCP', 'INP', 'TTFB']
97
- .map((metric) => window.webVitals[`get${metric}`])
98
- .filter((metric) => typeof metric === 'function')
99
- .forEach((invokeMetric) => {
100
- invokeMetric(storeCWV);
101
- });
102
- };
103
- document.head.appendChild(script);
104
- }));
105
-
106
- sampleRUM.drain('leave', ((event = {}) => {
107
- if (sampleRUM.left || (event.type === 'visibilitychange' && document.visibilityState !== 'hidden')) {
108
- return;
109
- }
110
- sampleRUM.left = true;
111
- sampleRUM('leave');
112
- }));
113
-
114
- sampleRUM.sourceselector = (element) => {
34
+ const sourceselector = (element) => {
115
35
  if (element === document.body || element === document.documentElement || !element) {
116
36
  return undefined;
117
37
  }
@@ -124,8 +44,8 @@ sampleRUM.sourceselector = (element) => {
124
44
  formElementSelector = element.tagName === 'INPUT' ? `form input[type='${element.getAttribute('type')}']` : `form ${element.tagName.toLowerCase()}`;
125
45
  }
126
46
 
127
- const blockName = element.closest('.block') ? element.closest('.block').getAttribute('data-block-name') : '';
128
47
  if (element.id || formElementSelector) {
48
+ const blockName = element.closest('.block') ? element.closest('.block').getAttribute('data-block-name') : '';
129
49
  const id = element.id ? `#${element.id}` : '';
130
50
  return blockName ? `.${blockName} ${formElementSelector}${id}` : `${formElementSelector}${id}`;
131
51
  }
@@ -133,34 +53,211 @@ sampleRUM.sourceselector = (element) => {
133
53
  if (element.getAttribute('data-block-name')) {
134
54
  return `.${element.getAttribute('data-block-name')}`;
135
55
  }
56
+ return sourceselector(element.parentElement);
57
+ };
136
58
 
137
- if (Array.from(element.classList).some((className) => className.match(/button|cta/))) {
138
- return blockName ? `.${blockName} .button` : '.button';
59
+ const formSubmitListener = (e) => sampleRUM('formsubmit', { target: targetselector(e.target), source: sourceselector(e.target) });
60
+ // eslint-disable-next-line no-use-before-define
61
+ const mutationObserver = window.MutationObserver ? new MutationObserver(mutationsCallback) : null;
62
+
63
+ // eslint-disable-next-line no-unused-vars
64
+ function optedIn(checkpoint, data) {
65
+ // TODO: check config service to know if
66
+ return true;
67
+ }
68
+ // Gets configured collection from the config service for the current domain
69
+ function getCollectionConfig() {
70
+ return DEFAULT_TRACKING_EVENTS;
71
+ }
72
+
73
+ function trackCheckpoint(checkpoint, data, t) {
74
+ const { weight, id } = window.hlx.rum;
75
+ if (optedIn(checkpoint, data) && isSelected) {
76
+ const sendPing = (pdata = data) => {
77
+ // eslint-disable-next-line object-curly-newline, max-len
78
+ const body = JSON.stringify({ weight, id, sanitizeURL: urlSanitizers[window.hlx.RUM_MASK_URL || 'path'], checkpoint, t, ...data }, KNOWN_PROPERTIES);
79
+ const url = new URL(`.rum/${weight}`, sampleRUM.baseURL).href;
80
+ navigator.sendBeacon(url, body);
81
+ // eslint-disable-next-line no-console
82
+ console.debug(`ping:${checkpoint}`, pdata);
83
+ };
84
+ sendPing(data);
139
85
  }
140
- return sampleRUM.sourceselector(element.parentElement);
141
- };
86
+ }
87
+
88
+ function processQueue() {
89
+ while (queue.length) {
90
+ const ck = queue.shift();
91
+ trackCheckpoint(...ck);
92
+ }
93
+ }
94
+
95
+ function addCWVTracking() {
96
+ setTimeout(() => {
97
+ const cwvScript = new URL('.rum/web-vitals/dist/web-vitals.iife.js', sampleRUM.baseURL).href;
98
+ if (document.querySelector(`script[src="${cwvScript}"]`)) {
99
+ // web vitals script has been loaded already
100
+ return;
101
+ }
102
+ const script = document.createElement('script');
103
+ script.src = cwvScript;
104
+ script.onload = () => {
105
+ const storeCWV = (measurement) => {
106
+ const data = { cwv: {} };
107
+ data.cwv[measurement.name] = measurement.value;
108
+ sampleRUM('cwv', data);
109
+ };
110
+ // When loading `web-vitals` using a classic script, all the public
111
+ // methods can be found on the `webVitals` global namespace.
112
+ ['CLS', 'FID', 'LCP', 'INP', 'TTFB']
113
+ .map((metric) => window.webVitals[`get${metric}`])
114
+ .filter((metric) => typeof metric === 'function')
115
+ .forEach((invokeMetric) => {
116
+ invokeMetric(storeCWV);
117
+ });
118
+ };
119
+ document.head.appendChild(script);
120
+ }, 2000); // wait for delayed
121
+ }
142
122
 
143
- document.addEventListener('click', (event) => {
144
- sampleRUM('click', { target: sampleRUM.targetselector(event.target), source: sampleRUM.sourceselector(event.target) });
145
- });
123
+ function addEnterLeaveTracking() {
124
+ // enter checkpoint when referrer is not the current page url
125
+ const navigate = (source, type) => {
126
+ const payload = { source, target: document.visibilityState };
127
+ // reload: same page, navigate: same origin, enter: everything else
128
+ if (type === 'reload' || source === window.location.href) {
129
+ sampleRUM('reload', payload);
130
+ } else if (type !== 'navigate') {
131
+ sampleRUM(type, payload); // back, forward, prerender, etc.
132
+ } else if (source && window.location.origin === new URL(source).origin) {
133
+ sampleRUM('navigate', payload); // internal navigation
134
+ } else {
135
+ sampleRUM('enter', payload); // enter site
136
+ }
137
+ };
138
+ navigate(document.referrer);
139
+
140
+ new PerformanceObserver((list) => list
141
+ .getEntries().map((entry) => navigate(document.referrer, entry.type)))
142
+ .observe({ type: 'navigation', buffered: true });
143
+
144
+ const leave = ((event) => {
145
+ if (leave.left || (event.type === 'visibilitychange' && document.visibilityState !== 'hidden')) {
146
+ return;
147
+ }
148
+ leave.left = true;
149
+ sampleRUM('leave');
150
+ });
151
+ window.addEventListener('visibilitychange', ((event) => leave(event)));
152
+ window.addEventListener('pagehide', ((event) => leave(event)));
153
+ }
146
154
 
147
- // Track form submissions
148
- document.querySelectorAll('form').forEach((form) => {
149
- form.addEventListener('submit', (event) => {
150
- sampleRUM('formsubmit', { target: sampleRUM.targetselector(event.target), source: sampleRUM.sourceselector(event.target) });
155
+ function addLoadResourceTracking() {
156
+ const observer = new PerformanceObserver((list) => {
157
+ list.getEntries()
158
+ .filter((entry) => !entry.responseStatus || entry.responseStatus < 400)
159
+ .filter((entry) => window.location.hostname === new URL(entry.name).hostname)
160
+ .filter((entry) => new URL(entry.name).pathname.match('.*(\\.plain\\.html|\\.json)$'))
161
+ .forEach((entry) => {
162
+ sampleRUM('loadresource', { source: entry.name, target: Math.round(entry.duration) });
163
+ });
151
164
  });
152
- });
153
-
154
- window.addEventListener('visibilitychange', ((event) => sampleRUM.leave(event)));
155
- window.addEventListener('pagehide', ((event) => sampleRUM.leave(event)));
156
-
157
- const observer = new PerformanceObserver((list) => {
158
- list.getEntries()
159
- .filter((entry) => !entry.responseStatus || entry.responseStatus < 400)
160
- .filter((entry) => window.location.hostname === new URL(entry.name).hostname)
161
- .filter((entry) => new URL(entry.name).pathname.match('.*(\\.plain\\.html|\\.json)$'))
162
- .forEach((entry) => {
163
- sampleRUM('loadresource', { source: entry.name, target: Math.round(entry.duration) });
165
+ observer.observe({ type: 'resource', buffered: true });
166
+ }
167
+
168
+ function activateBlocksMutationObserver() {
169
+ if (!mutationObserver || mutationObserver.active) {
170
+ return;
171
+ }
172
+ mutationObserver.active = true;
173
+ mutationObserver.observe(
174
+ document.body,
175
+ // eslint-disable-next-line object-curly-newline
176
+ { subtree: true, attributes: true, attributeFilter: ['data-block-status'] },
177
+ );
178
+ }
179
+
180
+ function getIntersectionObsever(checkpoint) {
181
+ if (!window.IntersectionObserver) {
182
+ return null;
183
+ }
184
+ activateBlocksMutationObserver();
185
+ const observer = new IntersectionObserver((entries) => {
186
+ entries
187
+ .filter((entry) => entry.isIntersecting)
188
+ .forEach((entry) => {
189
+ observer.unobserve(entry.target); // observe only once
190
+ const target = targetselector(entry.target);
191
+ const source = sourceselector(entry.target);
192
+ sampleRUM(checkpoint, { target, source });
193
+ });
194
+ }, { threshold: 0.25 });
195
+ return observer;
196
+ }
197
+ function addViewBlockTracking(element) {
198
+ const blockobserver = getIntersectionObsever('viewblock');
199
+ if (blockobserver) {
200
+ const blocks = element.getAttribute('data-block-status') ? [element] : element.querySelectorAll('div[data-block-status="loaded"]');
201
+ blocks.forEach((b) => blockobserver.observe(b));
202
+ }
203
+ }
204
+
205
+ function addViewMediaTracking(parent) {
206
+ const mediaobserver = getIntersectionObsever('viewmedia');
207
+ if (mediaobserver) {
208
+ parent.querySelectorAll('picture > img, video, audio, iframe').forEach((m) => {
209
+ if (!m.closest('div .block') || m.closest('div[data-block-status="loaded"]')) {
210
+ mediaobserver.observe(m);
211
+ }
212
+ });
213
+ }
214
+ }
215
+
216
+ function addFormTracking(parent) {
217
+ activateBlocksMutationObserver();
218
+ parent.querySelectorAll('form').forEach((form) => {
219
+ form.removeEventListener('submit', formSubmitListener); // listen only once
220
+ form.addEventListener('submit', formSubmitListener);
221
+ });
222
+ }
223
+
224
+ const addObserver = (ck, fn, block) => getCollectionConfig().includes(ck) && fn(block);
225
+ function mutationsCallback(mutations) {
226
+ mutations.filter((m) => m.type === 'attributes' && m.attributeName === 'data-block-status')
227
+ .filter((m) => m.target.dataset.blockStatus === 'loaded')
228
+ .forEach((m) => {
229
+ addObserver('form', addFormTracking, m.target);
230
+ addObserver('viewblock', addViewBlockTracking, m.target);
231
+ addObserver('viewmedia', addViewMediaTracking, m.target);
164
232
  });
165
- });
166
- observer.observe({ type: 'resource', buffered: true });
233
+ }
234
+
235
+ function addTrackingFromConfig() {
236
+ const trackingFunctions = {
237
+ click: () => {
238
+ document.addEventListener('click', (event) => {
239
+ sampleRUM('click', { target: targetselector(event.target), source: sourceselector(event.target) });
240
+ });
241
+ },
242
+ cwv: () => addCWVTracking(),
243
+ form: () => addFormTracking(window.document.body),
244
+ enterleave: () => addEnterLeaveTracking(),
245
+ loadresource: () => addLoadResourceTracking(),
246
+ viewblock: () => addViewBlockTracking(window.document.body),
247
+ viewmedia: () => addViewMediaTracking(window.document.body),
248
+ };
249
+
250
+ getCollectionConfig().filter((ck) => trackingFunctions[ck])
251
+ .forEach((ck) => trackingFunctions[ck]());
252
+ }
253
+
254
+ function initEnhancer() {
255
+ // eslint-disable-next-line max-len
256
+ const rumStorage = sessionStorage.getItem(SESSION_STORAGE_KEY) ? JSON.parse(sessionStorage.getItem(SESSION_STORAGE_KEY)) : {};
257
+ sampleRUM('pagesviewed', { source: rumStorage.pages });
258
+ addTrackingFromConfig();
259
+ window.hlx.rum.collector = trackCheckpoint;
260
+ processQueue();
261
+ }
262
+
263
+ initEnhancer();