@adobe/helix-rum-enhancer 1.9.1 → 2.0.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,17 @@
1
+ # [2.0.0](https://github.com/adobe/helix-rum-enhancer/compare/v1.9.1...v2.0.0) (2024-01-16)
2
+
3
+
4
+ ### Features
5
+
6
+ * prepare compatibility with @adobe/helix-rum-js@2 ([b1cef07](https://github.com/adobe/helix-rum-enhancer/commit/b1cef07ec59c8134056a9aaf25a331016aeffe39))
7
+ * prepare compatibility with @adobe/helix-rum-js@2 ([702a873](https://github.com/adobe/helix-rum-enhancer/commit/702a8731d783850d26b2daae3a839e7ee4192e47))
8
+
9
+
10
+ ### BREAKING CHANGES
11
+
12
+ * this version can only be used from helix-rum-js@v2 and up
13
+ * this version can only be used from helix-rum-js@v2 and up
14
+
1
15
  ## [1.9.1](https://github.com/adobe/helix-rum-enhancer/compare/v1.9.0...v1.9.1) (2024-01-11)
2
16
 
3
17
 
package/README.md CHANGED
@@ -17,10 +17,13 @@ This library is meant to be used in conjunction with and loaded by the `sampleRU
17
17
  It will add following new checkpoints:
18
18
 
19
19
  - `cwv`: Core Web Vitals Collection
20
+ - `pagesviewed`: number that helps to estimate how many pages are seen in average by a user.
20
21
  - `click`: A click anywhere in the document
21
22
  - `viewblock`: A Helix block has been scrolled into the viewport
22
23
  - `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.
24
+ - `reload`, `navigate`, `enter`: depending on how the current page was accessed
25
+ - `formsubmit`: when a form is submitted
26
+ - `leave`: when the user leaves the page
24
27
 
25
28
  ## Development
26
29
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/helix-rum-enhancer",
3
- "version": "1.9.1",
3
+ "version": "2.0.0",
4
4
  "description": "Helix RUM Enhancer",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -32,7 +32,7 @@
32
32
  "@semantic-release/changelog": "6.0.3",
33
33
  "@semantic-release/git": "10.0.1",
34
34
  "@semantic-release/npm": "11.0.2",
35
- "c8": "9.0.0",
35
+ "c8": "9.1.0",
36
36
  "codecov": "3.8.3",
37
37
  "eslint": "8.56.0",
38
38
  "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'];
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
  }
@@ -133,32 +53,217 @@ sampleRUM.sourceselector = (element) => {
133
53
  if (element.getAttribute('data-block-name')) {
134
54
  return `.${element.getAttribute('data-block-name')}`;
135
55
  }
136
- return sampleRUM.sourceselector(element.parentElement);
56
+ return sourceselector(element.parentElement);
137
57
  };
138
58
 
139
- document.addEventListener('click', (event) => {
140
- sampleRUM('click', { target: sampleRUM.targetselector(event.target), source: sampleRUM.sourceselector(event.target) });
141
- });
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
+ if (window.location.hostname === 'blog.adobe.com') {
71
+ return ['loadresource', ...DEFAULT_TRACKING_EVENTS];
72
+ }
73
+ // TODO: configured collection should come from config service
74
+ return DEFAULT_TRACKING_EVENTS;
75
+ }
76
+
77
+ function trackCheckpoint(checkpoint, data, t) {
78
+ const { weight, id } = window.hlx.rum;
79
+ if (optedIn(checkpoint, data) && isSelected) {
80
+ const sendPing = (pdata = data) => {
81
+ // eslint-disable-next-line object-curly-newline, max-len
82
+ const body = JSON.stringify({ weight, id, sanitizeURL: urlSanitizers[window.hlx.RUM_MASK_URL || 'path'], checkpoint, t, ...data }, KNOWN_PROPERTIES);
83
+ const url = new URL(`.rum/${weight}`, sampleRUM.baseURL).href;
84
+ navigator.sendBeacon(url, body);
85
+ // eslint-disable-next-line no-console
86
+ console.debug(`ping:${checkpoint}`, pdata);
87
+ };
88
+ sendPing(data);
89
+ }
90
+ }
91
+
92
+ function processQueue() {
93
+ while (queue.length) {
94
+ const ck = queue.shift();
95
+ trackCheckpoint(...ck);
96
+ }
97
+ }
98
+
99
+ function addCWVTracking() {
100
+ setTimeout(() => {
101
+ const cwvScript = new URL('.rum/web-vitals/dist/web-vitals.iife.js', sampleRUM.baseURL).href;
102
+ if (document.querySelector(`script[src="${cwvScript}"]`)) {
103
+ // web vitals script has been loaded already
104
+ return;
105
+ }
106
+ const script = document.createElement('script');
107
+ script.src = cwvScript;
108
+ script.onload = () => {
109
+ const storeCWV = (measurement) => {
110
+ const data = { cwv: {} };
111
+ data.cwv[measurement.name] = measurement.value;
112
+ sampleRUM('cwv', data);
113
+ };
114
+ // When loading `web-vitals` using a classic script, all the public
115
+ // methods can be found on the `webVitals` global namespace.
116
+ ['CLS', 'FID', 'LCP', 'INP', 'TTFB']
117
+ .map((metric) => window.webVitals[`get${metric}`])
118
+ .filter((metric) => typeof metric === 'function')
119
+ .forEach((invokeMetric) => {
120
+ invokeMetric(storeCWV);
121
+ });
122
+ };
123
+ document.head.appendChild(script);
124
+ }, 2000); // wait for delayed
125
+ }
126
+
127
+ function addEnterLeaveTracking() {
128
+ // enter checkpoint when referrer is not the current page url
129
+ const navigate = (source, type) => {
130
+ const payload = { source, target: document.visibilityState };
131
+ // reload: same page, navigate: same origin, enter: everything else
132
+ if (type === 'reload' || source === window.location.href) {
133
+ sampleRUM('reload', payload);
134
+ } else if (type !== 'navigate') {
135
+ sampleRUM(type, payload); // back, forward, prerender, etc.
136
+ } else if (source && window.location.origin === new URL(source).origin) {
137
+ sampleRUM('navigate', payload); // internal navigation
138
+ } else {
139
+ sampleRUM('enter', payload); // enter site
140
+ }
141
+ };
142
+ navigate(document.referrer);
142
143
 
143
- // Track form submissions
144
- document.querySelectorAll('form').forEach((form) => {
145
- form.addEventListener('submit', (event) => {
146
- sampleRUM('formsubmit', { target: sampleRUM.targetselector(event.target), source: sampleRUM.sourceselector(event.target) });
144
+ new PerformanceObserver((list) => list
145
+ .getEntries().map((entry) => navigate(document.referrer, entry.type)))
146
+ .observe({ type: 'navigation', buffered: true });
147
+
148
+ const leave = ((event) => {
149
+ if (leave.left || (event.type === 'visibilitychange' && document.visibilityState !== 'hidden')) {
150
+ return;
151
+ }
152
+ leave.left = true;
153
+ sampleRUM('leave');
147
154
  });
148
- });
155
+ window.addEventListener('visibilitychange', ((event) => leave(event)));
156
+ window.addEventListener('pagehide', ((event) => leave(event)));
157
+ }
149
158
 
150
- window.addEventListener('visibilitychange', ((event) => sampleRUM.leave(event)));
151
- window.addEventListener('pagehide', ((event) => sampleRUM.leave(event)));
159
+ function addLoadResourceTracking() {
160
+ if (window.location.hostname === 'blog.adobe.com') {
161
+ const observer = new PerformanceObserver((list) => {
162
+ list.getEntries()
163
+ .filter((entry) => !entry.responseStatus || entry.responseStatus < 400)
164
+ .filter((entry) => window.location.hostname === new URL(entry.name).hostname)
165
+ .filter((entry) => new URL(entry.name).pathname.match('.*(\\.plain\\.html|\\.json)$'))
166
+ .forEach((entry) => {
167
+ sampleRUM('loadresource', { source: entry.name, target: Math.round(entry.duration) });
168
+ });
169
+ });
170
+ observer.observe({ type: 'resource', buffered: true });
171
+ }
172
+ }
152
173
 
153
- if (window.location.hostname === 'blog.adobe.com') {
154
- const observer = new PerformanceObserver((list) => {
155
- list.getEntries()
156
- .filter((entry) => !entry.responseStatus || entry.responseStatus < 400)
157
- .filter((entry) => window.location.hostname === new URL(entry.name).hostname)
158
- .filter((entry) => new URL(entry.name).pathname.match('.*(\\.plain\\.html|\\.json)$'))
174
+ function activateBlocksMutationObserver() {
175
+ if (!mutationObserver || mutationObserver.active) {
176
+ return;
177
+ }
178
+ mutationObserver.active = true;
179
+ mutationObserver.observe(
180
+ document.body,
181
+ // eslint-disable-next-line object-curly-newline
182
+ { subtree: true, attributes: true, attributeFilter: ['data-block-status'] },
183
+ );
184
+ }
185
+
186
+ function getIntersectionObsever(checkpoint) {
187
+ if (!window.IntersectionObserver) {
188
+ return null;
189
+ }
190
+ activateBlocksMutationObserver();
191
+ const observer = new IntersectionObserver((entries) => {
192
+ entries
193
+ .filter((entry) => entry.isIntersecting)
159
194
  .forEach((entry) => {
160
- sampleRUM('loadresource', { source: entry.name, target: Math.round(entry.duration) });
195
+ observer.unobserve(entry.target); // observe only once
196
+ const target = targetselector(entry.target);
197
+ const source = sourceselector(entry.target);
198
+ sampleRUM(checkpoint, { target, source });
161
199
  });
200
+ }, { threshold: 0.25 });
201
+ return observer;
202
+ }
203
+ function addViewBlockTracking(element) {
204
+ const blockobserver = getIntersectionObsever('viewblock');
205
+ if (blockobserver) {
206
+ const blocks = element.getAttribute('data-block-status') ? [element] : element.querySelectorAll('div[data-block-status="loaded"]');
207
+ blocks.forEach((b) => blockobserver.observe(b));
208
+ }
209
+ }
210
+
211
+ function addViewMediaTracking(parent) {
212
+ const mediaobserver = getIntersectionObsever('viewmedia');
213
+ if (mediaobserver) {
214
+ parent.querySelectorAll('picture > img, video, audio, iframe').forEach((m) => {
215
+ if (!m.closest('div .block') || m.closest('div[data-block-status="loaded"]')) {
216
+ mediaobserver.observe(m);
217
+ }
218
+ });
219
+ }
220
+ }
221
+
222
+ function addFormTracking(parent) {
223
+ activateBlocksMutationObserver();
224
+ parent.querySelectorAll('form').forEach((form) => {
225
+ form.removeEventListener('submit', formSubmitListener); // listen only once
226
+ form.addEventListener('submit', formSubmitListener);
162
227
  });
163
- observer.observe({ type: 'resource', buffered: true });
164
228
  }
229
+
230
+ const addObserver = (ck, fn, block) => getCollectionConfig().includes(ck) && fn(block);
231
+ function mutationsCallback(mutations) {
232
+ mutations.filter((m) => m.type === 'attributes' && m.attributeName === 'data-block-status')
233
+ .filter((m) => m.target.dataset.blockStatus === 'loaded')
234
+ .forEach((m) => {
235
+ addObserver('form', addFormTracking, m.target);
236
+ addObserver('viewblock', addViewBlockTracking, m.target);
237
+ addObserver('viewmedia', addViewMediaTracking, m.target);
238
+ });
239
+ }
240
+
241
+ function addTrackingFromConfig() {
242
+ const trackingFunctions = {
243
+ click: () => {
244
+ document.addEventListener('click', (event) => {
245
+ sampleRUM('click', { target: targetselector(event.target), source: sourceselector(event.target) });
246
+ });
247
+ },
248
+ cwv: () => addCWVTracking(),
249
+ form: () => addFormTracking(window.document.body),
250
+ enterleave: () => addEnterLeaveTracking(),
251
+ loadresource: () => addLoadResourceTracking(),
252
+ viewblock: () => addViewBlockTracking(window.document.body),
253
+ viewmedia: () => addViewMediaTracking(window.document.body),
254
+ };
255
+
256
+ getCollectionConfig().filter((ck) => trackingFunctions[ck])
257
+ .forEach((ck) => trackingFunctions[ck]());
258
+ }
259
+
260
+ function initEnhancer() {
261
+ // eslint-disable-next-line max-len
262
+ const rumStorage = sessionStorage.getItem(SESSION_STORAGE_KEY) ? JSON.parse(sessionStorage.getItem(SESSION_STORAGE_KEY)) : {};
263
+ sampleRUM('pagesviewed', { source: rumStorage.pages });
264
+ addTrackingFromConfig();
265
+ window.hlx.rum.collector = trackCheckpoint;
266
+ processQueue();
267
+ }
268
+
269
+ initEnhancer();