@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 +14 -0
- package/README.md +4 -1
- package/package.json +2 -2
- package/src/index.js +219 -114
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`:
|
|
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": "
|
|
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.
|
|
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
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
18
|
+
const urlSanitizers = {
|
|
19
|
+
full: () => window.location.href,
|
|
20
|
+
origin: () => window.location.origin,
|
|
21
|
+
path: () => window.location.href.replace(/\?.*$/, ''),
|
|
22
|
+
};
|
|
68
23
|
|
|
69
|
-
|
|
70
|
-
let value = element.getAttribute('data-rum-target') || element.getAttribute('href')
|
|
71
|
-
|
|
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
|
-
|
|
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
|
|
56
|
+
return sourceselector(element.parentElement);
|
|
137
57
|
};
|
|
138
58
|
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
151
|
-
window.
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
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();
|