@adobe/spacecat-shared-rum-api-client 2.11.0 → 2.12.1
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/package.json +3 -3
- package/src/common/aggregateFns.js +0 -44
- package/src/common/traffic.js +19 -0
- package/src/functions/cwv.js +27 -53
- package/src/functions/experiment.js +67 -144
- package/src/functions/opportunities/high-inorganic-high-bounce-rate.js +33 -14
- package/src/functions/traffic-acquisition.js +14 -14
- package/src/utils.js +20 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
# [@adobe/spacecat-shared-rum-api-client-v2.12.1](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-rum-api-client-v2.12.0...@adobe/spacecat-shared-rum-api-client-v2.12.1) (2024-11-23)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* **deps:** update external fixes ([#454](https://github.com/adobe/spacecat-shared/issues/454)) ([325cf8d](https://github.com/adobe/spacecat-shared/commit/325cf8dded5fcabadaf7d8fdd510d33aeafd08a7))
|
|
7
|
+
|
|
8
|
+
# [@adobe/spacecat-shared-rum-api-client-v2.12.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-rum-api-client-v2.11.0...@adobe/spacecat-shared-rum-api-client-v2.12.0) (2024-11-21)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* **data-cruncher:** port queries to the data cruncher from RUM Explorer ([#375](https://github.com/adobe/spacecat-shared/issues/375)) ([599ebfc](https://github.com/adobe/spacecat-shared/commit/599ebfc7a7e14580ee1af9e42eac0d208acd5d94))
|
|
14
|
+
|
|
1
15
|
# [@adobe/spacecat-shared-rum-api-client-v2.11.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-rum-api-client-v2.10.0...@adobe/spacecat-shared-rum-api-client-v2.11.0) (2024-11-20)
|
|
2
16
|
|
|
3
17
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adobe/spacecat-shared-rum-api-client",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.12.1",
|
|
4
4
|
"description": "Shared modules of the Spacecat Services - Rum API client",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -39,8 +39,8 @@
|
|
|
39
39
|
"@adobe/helix-shared-wrap": "2.0.2",
|
|
40
40
|
"@adobe/helix-universal": "5.0.6",
|
|
41
41
|
"@adobe/spacecat-shared-utils": "1.22.4",
|
|
42
|
+
"@adobe/rum-distiller": "1.9.0",
|
|
42
43
|
"aws4": "1.13.2",
|
|
43
|
-
"d3-array": "3.2.4",
|
|
44
44
|
"urijs": "^1.19.11"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
@@ -49,6 +49,6 @@
|
|
|
49
49
|
"nock": "13.5.6",
|
|
50
50
|
"sinon": "19.0.2",
|
|
51
51
|
"sinon-chai": "4.0.0",
|
|
52
|
-
"typescript": "5.
|
|
52
|
+
"typescript": "5.7.2"
|
|
53
53
|
}
|
|
54
54
|
}
|
|
@@ -12,48 +12,6 @@
|
|
|
12
12
|
|
|
13
13
|
import { extractTrafficHints, classifyVendor, getSecondLevelDomain } from './traffic.js';
|
|
14
14
|
|
|
15
|
-
/**
|
|
16
|
-
* Calculates the total page views by URL from an array of bundles.
|
|
17
|
-
* @param {Array<Object>} bundles - An array of RUM bundles (NOT Flat bundles).
|
|
18
|
-
* @returns {Object} An object where keys are URLs and values are the total page views for each URL.
|
|
19
|
-
*/
|
|
20
|
-
function pageviewsByUrl(bundles) {
|
|
21
|
-
return bundles.reduce((acc, cur) => {
|
|
22
|
-
if (!acc[cur.url]) acc[cur.url] = 0;
|
|
23
|
-
acc[cur.url] += cur.weight;
|
|
24
|
-
return acc;
|
|
25
|
-
}, {});
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Calculates the Click-Through Rate (CTR) by URL.
|
|
30
|
-
* CTR is defined as the total number of sessions with at least one click event
|
|
31
|
-
* divided by the total number of pageviews for each URL.
|
|
32
|
-
*
|
|
33
|
-
* @param {Array<Object>} bundles - An array of RUM bundles (NOT Flat bundles).
|
|
34
|
-
* @returns {Object} - An object where the key is the URL and the value is the CTR value.
|
|
35
|
-
*/
|
|
36
|
-
function getCTRByUrl(bundles) {
|
|
37
|
-
const aggregated = bundles.reduce((acc, bundle) => {
|
|
38
|
-
const { url } = bundle;
|
|
39
|
-
if (!acc[url]) {
|
|
40
|
-
acc[url] = { sessionsWithClick: 0, totalPageviews: 0 };
|
|
41
|
-
}
|
|
42
|
-
const hasClick = bundle.events.some((event) => event.checkpoint === 'click');
|
|
43
|
-
|
|
44
|
-
acc[url].totalPageviews += bundle.weight;
|
|
45
|
-
if (hasClick) {
|
|
46
|
-
acc[url].sessionsWithClick += bundle.weight;
|
|
47
|
-
}
|
|
48
|
-
return acc;
|
|
49
|
-
}, {});
|
|
50
|
-
return Object.entries(aggregated)
|
|
51
|
-
.reduce((acc, [url, { sessionsWithClick, totalPageviews }]) => {
|
|
52
|
-
acc[url] = (sessionsWithClick / totalPageviews);
|
|
53
|
-
return acc;
|
|
54
|
-
}, {});
|
|
55
|
-
}
|
|
56
|
-
|
|
57
15
|
/**
|
|
58
16
|
* Calculates the Click-Through Rate (CTR) by URL and Referrer.
|
|
59
17
|
* CTR is defined as the total number of sessions with at least one click event per referrer.
|
|
@@ -132,7 +90,5 @@ function getSiteAvgCTR(bundles) {
|
|
|
132
90
|
|
|
133
91
|
export {
|
|
134
92
|
getSiteAvgCTR,
|
|
135
|
-
getCTRByUrl,
|
|
136
93
|
getCTRByUrlAndVendor,
|
|
137
|
-
pageviewsByUrl,
|
|
138
94
|
};
|
package/src/common/traffic.js
CHANGED
|
@@ -122,6 +122,7 @@ const anyOf = (truth) => (text) => {
|
|
|
122
122
|
return truth === text;
|
|
123
123
|
};
|
|
124
124
|
|
|
125
|
+
/* c8 ignore next 1 */
|
|
125
126
|
const none = (input) => (Array.isArray(input) ? input.length === 0 : !hasText(input));
|
|
126
127
|
|
|
127
128
|
const not = (truth) => (text) => {
|
|
@@ -198,6 +199,7 @@ const RULES = (domain) => ([
|
|
|
198
199
|
]);
|
|
199
200
|
|
|
200
201
|
export function extractTrafficHints(bundle) {
|
|
202
|
+
/* c8 ignore next 1 */
|
|
201
203
|
const findEvent = (checkpoint, source = '') => bundle.events.find((e) => e.checkpoint === checkpoint && (!source || e.source === source)) || {};
|
|
202
204
|
|
|
203
205
|
const referrer = findEvent('enter').source || '';
|
|
@@ -252,3 +254,20 @@ export function classifyTrafficSource(url, referrer, utmSource, utmMedium, track
|
|
|
252
254
|
vendor,
|
|
253
255
|
};
|
|
254
256
|
}
|
|
257
|
+
|
|
258
|
+
export function classifyTraffic(bundle) {
|
|
259
|
+
const {
|
|
260
|
+
url,
|
|
261
|
+
weight,
|
|
262
|
+
referrer,
|
|
263
|
+
utmSource,
|
|
264
|
+
utmMedium,
|
|
265
|
+
tracking,
|
|
266
|
+
} = extractTrafficHints(bundle);
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
url,
|
|
270
|
+
weight,
|
|
271
|
+
...classifyTrafficSource(url, referrer, utmSource, utmMedium, tracking),
|
|
272
|
+
};
|
|
273
|
+
}
|
package/src/functions/cwv.js
CHANGED
|
@@ -10,62 +10,36 @@
|
|
|
10
10
|
* governing permissions and limitations under the License.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const CWV_METRICS = ['lcp', 'cls', 'inp', 'ttfb'].map((metric) => `cwv-${metric}`);
|
|
18
|
-
|
|
19
|
-
function collectCWVs(groupedByUrlIdTime) {
|
|
20
|
-
const { url, items: itemsByUrl } = groupedByUrlIdTime;
|
|
21
|
-
|
|
22
|
-
// first level: grouped by url
|
|
23
|
-
const CWVs = itemsByUrl.reduce((acc, { items: itemsById }) => {
|
|
24
|
-
// second level: grouped by id
|
|
25
|
-
const itemsByTime = itemsById.flatMap((itemById) => itemById.items);
|
|
26
|
-
// third level: grouped by time
|
|
27
|
-
const maximums = itemsByTime.reduce((values, item) => {
|
|
28
|
-
// each session (id-time) can contain multiple measurement for the same metric
|
|
29
|
-
// we need to find the maximum per metric type
|
|
30
|
-
// eslint-disable-next-line no-param-reassign
|
|
31
|
-
values[item.checkpoint] = Math.max(values[item.checkpoint] || 0, item.value);
|
|
32
|
-
return values;
|
|
33
|
-
}, {});
|
|
34
|
-
|
|
35
|
-
// max values per id for each metric type are collected into an array
|
|
36
|
-
CWV_METRICS.forEach((metric) => {
|
|
37
|
-
if (!acc[metric]) acc[metric] = [];
|
|
38
|
-
if (maximums[metric]) {
|
|
39
|
-
acc[metric].push(maximums[metric]);
|
|
40
|
-
}
|
|
41
|
-
});
|
|
42
|
-
return acc;
|
|
43
|
-
}, {});
|
|
44
|
-
|
|
45
|
-
return {
|
|
46
|
-
url,
|
|
47
|
-
lcp: quantile(CWVs['cwv-lcp'], 0.75) || null,
|
|
48
|
-
lcpCount: CWVs['cwv-lcp'].length,
|
|
49
|
-
cls: quantile(CWVs['cwv-cls'], 0.75) || null,
|
|
50
|
-
clsCount: CWVs['cwv-cls'].length,
|
|
51
|
-
inp: quantile(CWVs['cwv-inp'], 0.75) || null,
|
|
52
|
-
inpCount: CWVs['cwv-inp'].length,
|
|
53
|
-
ttfb: quantile(CWVs['cwv-ttfb'], 0.75) || null,
|
|
54
|
-
ttfbCount: CWVs['cwv-ttfb'].length,
|
|
55
|
-
};
|
|
56
|
-
}
|
|
13
|
+
import {
|
|
14
|
+
DataChunks, series, facets,
|
|
15
|
+
} from '@adobe/rum-distiller';
|
|
16
|
+
import { loadBundles } from '../utils.js';
|
|
57
17
|
|
|
58
18
|
function handler(bundles) {
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
19
|
+
const dataChunks = new DataChunks();
|
|
20
|
+
|
|
21
|
+
loadBundles(bundles, dataChunks);
|
|
22
|
+
|
|
23
|
+
dataChunks.addFacet('urls', facets.url);
|
|
24
|
+
|
|
25
|
+
dataChunks.addSeries('lcp', series.lcp);
|
|
26
|
+
dataChunks.addSeries('cls', series.cls);
|
|
27
|
+
dataChunks.addSeries('inp', series.inp);
|
|
28
|
+
dataChunks.addSeries('ttfb', series.ttfb);
|
|
29
|
+
|
|
30
|
+
return dataChunks.facets.urls.map((urlFacet) => ({
|
|
31
|
+
url: urlFacet.value,
|
|
32
|
+
pageviews: urlFacet.weight,
|
|
33
|
+
lcp: urlFacet.metrics.lcp.percentile(75) || null,
|
|
34
|
+
lcpCount: urlFacet.metrics.lcp.count,
|
|
35
|
+
cls: urlFacet.metrics.cls.percentile(75) || null,
|
|
36
|
+
clsCount: urlFacet.metrics.cls.count,
|
|
37
|
+
inp: urlFacet.metrics.inp.percentile(75) || null,
|
|
38
|
+
inpCount: urlFacet.metrics.inp.count,
|
|
39
|
+
ttfb: urlFacet.metrics.ttfb.percentile(75) || null,
|
|
40
|
+
ttfbCount: urlFacet.metrics.ttfb.count,
|
|
41
|
+
}))
|
|
64
42
|
.filter((row) => row.lcp || row.cls || row.inp || row.ttfb) // filter out pages with no cwv data
|
|
65
|
-
.map((acc) => {
|
|
66
|
-
acc.pageviews = pageviews[acc.url];
|
|
67
|
-
return acc;
|
|
68
|
-
})
|
|
69
43
|
.sort((a, b) => b.pageviews - a.pageviews); // sort desc by pageviews
|
|
70
44
|
}
|
|
71
45
|
|
|
@@ -10,159 +10,82 @@
|
|
|
10
10
|
* governing permissions and limitations under the License.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const CHECKPOINTS = [...EXPERIMENT_CHECKPOINT, ...METRIC_CHECKPOINTS];
|
|
13
|
+
import { DataChunks } from '@adobe/rum-distiller';
|
|
14
|
+
import { generateKey, DELIMITER, loadBundles } from '../utils.js';
|
|
16
15
|
|
|
17
|
-
|
|
18
|
-
return typeof name === 'string'
|
|
19
|
-
? name.toLowerCase().replace(/[^0-9a-z]/gi, '-').replace(/-+/g, '-').replace(/^-|-$/g, '')
|
|
20
|
-
: '';
|
|
21
|
-
}
|
|
16
|
+
const METRICS = ['click', 'convert', 'formsubmit'];
|
|
22
17
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
if (!experimentObject) {
|
|
26
|
-
experimentObject = {
|
|
27
|
-
experiment: toClassName(experimentName),
|
|
28
|
-
variants: [],
|
|
29
|
-
};
|
|
30
|
-
urlInsights.push(experimentObject);
|
|
31
|
-
}
|
|
32
|
-
return experimentObject;
|
|
33
|
-
}
|
|
18
|
+
const experimentsFacetFn = (bundle) => bundle.events.filter((e) => e.checkpoint === 'experiment').map((e) => generateKey(bundle.url, e.source));
|
|
19
|
+
const variantsFacetFn = (bundle) => bundle.events.filter((e) => e.checkpoint === 'experiment').map((e) => generateKey(bundle.url, e.source, e.target));
|
|
34
20
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
variantObject = {
|
|
39
|
-
name: variantName,
|
|
40
|
-
views: 0,
|
|
41
|
-
samples: 0,
|
|
42
|
-
click: {},
|
|
43
|
-
convert: {},
|
|
44
|
-
formsubmit: {},
|
|
45
|
-
};
|
|
46
|
-
variants.push(variantObject);
|
|
47
|
-
}
|
|
48
|
-
return variantObject;
|
|
49
|
-
}
|
|
21
|
+
const checkpointsFacetFn = (bundle) => {
|
|
22
|
+
const experiments = bundle.events.filter((e) => e.checkpoint === 'experiment');
|
|
23
|
+
const metrics = bundle.events.filter((c) => METRICS.includes(c.checkpoint) && c.source);
|
|
50
24
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
bundleDate.setHours(0, 0, 0, 0);
|
|
58
|
-
if (!experimentObject.inferredStartDate && !experimentObject.inferredEndDate) {
|
|
59
|
-
// eslint-disable-next-line no-param-reassign
|
|
60
|
-
experimentObject.inferredStartDate = time;
|
|
61
|
-
// eslint-disable-next-line no-param-reassign
|
|
62
|
-
experimentObject.inferredEndDate = time;
|
|
63
|
-
} else {
|
|
64
|
-
const inferredStartDateObj = new Date(experimentObject.inferredStartDate);
|
|
65
|
-
const inferredEndDateObj = new Date(experimentObject.inferredEndDate);
|
|
66
|
-
if (bundleTime < inferredStartDateObj) {
|
|
67
|
-
// eslint-disable-next-line no-param-reassign
|
|
68
|
-
experimentObject.inferredStartDate = time;
|
|
69
|
-
}
|
|
70
|
-
if (bundleTime > inferredEndDateObj) {
|
|
71
|
-
// eslint-disable-next-line no-param-reassign
|
|
72
|
-
experimentObject.inferredEndDate = time;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
25
|
+
const keys = experiments.flatMap(
|
|
26
|
+
(exp) => metrics.flatMap((metric) => [
|
|
27
|
+
generateKey(bundle.url, exp.source, exp.target, metric.checkpoint, metric.source),
|
|
28
|
+
generateKey(bundle.url, exp.source, exp.target, metric.checkpoint, '*'),
|
|
29
|
+
]),
|
|
30
|
+
);
|
|
76
31
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
for (const checkpoint of METRIC_CHECKPOINTS) {
|
|
80
|
-
metrics[checkpoint] = {};
|
|
81
|
-
}
|
|
82
|
-
for (const event of bundle.events) {
|
|
83
|
-
if (METRIC_CHECKPOINTS.includes(event.checkpoint)) {
|
|
84
|
-
const { source, checkpoint } = event;
|
|
85
|
-
if (!metrics[checkpoint][source]) {
|
|
86
|
-
metrics[checkpoint][source] = {
|
|
87
|
-
value: bundle.weight,
|
|
88
|
-
samples: 1,
|
|
89
|
-
};
|
|
90
|
-
} else {
|
|
91
|
-
metrics[checkpoint][source].value += bundle.weight;
|
|
92
|
-
metrics[checkpoint][source].samples += 1;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
return metrics;
|
|
97
|
-
}
|
|
32
|
+
return [...new Set(keys)];
|
|
33
|
+
};
|
|
98
34
|
|
|
99
35
|
function handler(bundles) {
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
}
|
|
151
|
-
// add global interactionsCount if there's any interaction
|
|
152
|
-
const hasInteraction = Object.values(metrics).some((m) => Object.keys(m).length > 0);
|
|
153
|
-
if (hasInteraction) {
|
|
154
|
-
if (!variantObject.interactionsCount) {
|
|
155
|
-
variantObject.interactionsCount = weight;
|
|
156
|
-
} else {
|
|
157
|
-
variantObject.interactionsCount += weight;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
return experimentInsights;
|
|
36
|
+
const dataChunks = new DataChunks();
|
|
37
|
+
|
|
38
|
+
loadBundles(bundles.filter((bundle) => bundle.events.some((event) => event.checkpoint === 'experiment')), dataChunks);
|
|
39
|
+
|
|
40
|
+
dataChunks.addFacet('experiments', experimentsFacetFn);
|
|
41
|
+
dataChunks.addFacet('variants', variantsFacetFn);
|
|
42
|
+
dataChunks.addFacet('checkpoints', checkpointsFacetFn);
|
|
43
|
+
|
|
44
|
+
dataChunks.addSeries('experimenttime', (bundle) => new Date(bundle.time).getTime());
|
|
45
|
+
dataChunks.addSeries('views', (bundle) => bundle.weight);
|
|
46
|
+
dataChunks.addSeries('interaction', (bundle) => (bundle.events.find((e) => e.checkpoint === 'click' || e.checkpoint === 'formsubmit' || e.checkpoint === 'convert') ? bundle.weight : undefined));
|
|
47
|
+
|
|
48
|
+
const { experiments, variants, checkpoints } = dataChunks.facets;
|
|
49
|
+
|
|
50
|
+
const result = {};
|
|
51
|
+
experiments.forEach((uev) => {
|
|
52
|
+
const [url, experiment] = uev.value.split(DELIMITER);
|
|
53
|
+
if (!result[url]) result[url] = [];
|
|
54
|
+
result[url].push({
|
|
55
|
+
experiment,
|
|
56
|
+
variants: [],
|
|
57
|
+
inferredStartDate: new Date(uev.metrics.experimenttime.min).toISOString(),
|
|
58
|
+
inferredEndDate: new Date(uev.metrics.experimenttime.max).toISOString(),
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
variants.forEach((uev) => {
|
|
62
|
+
const [url, experiment, variant] = uev.value.split(DELIMITER);
|
|
63
|
+
const eIdx = result[url].findIndex((e) => e.experiment === experiment);
|
|
64
|
+
result[url][eIdx].variants.push({
|
|
65
|
+
name: variant,
|
|
66
|
+
click: {},
|
|
67
|
+
formsubmit: {},
|
|
68
|
+
convert: {},
|
|
69
|
+
interactionsCount: uev.metrics.interaction.sum,
|
|
70
|
+
samples: uev.metrics.views.count,
|
|
71
|
+
views: uev.metrics.views.sum,
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
checkpoints.forEach((uev) => {
|
|
75
|
+
const [url, experiment, variant, checkpoint, source] = uev.value.split(DELIMITER);
|
|
76
|
+
const eIdx = result[url].findIndex((e) => e.experiment === experiment);
|
|
77
|
+
const vIdx = result[url][eIdx].variants.findIndex((v) => v.name === variant);
|
|
78
|
+
|
|
79
|
+
result[url][eIdx].variants[vIdx][checkpoint][source] = {
|
|
80
|
+
value: uev.metrics.views.sum,
|
|
81
|
+
samples: uev.metrics.views.count,
|
|
82
|
+
};
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
return result;
|
|
163
86
|
}
|
|
164
87
|
|
|
165
88
|
export default {
|
|
166
89
|
handler,
|
|
167
|
-
checkpoints:
|
|
90
|
+
checkpoints: [...METRICS, 'experiment'],
|
|
168
91
|
};
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
* governing permissions and limitations under the License.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import
|
|
14
|
-
import {
|
|
13
|
+
import { DataChunks, facets } from '@adobe/rum-distiller';
|
|
14
|
+
import { loadBundles, trafficSeriesFn } from '../../utils.js';
|
|
15
15
|
|
|
16
16
|
const HOMEPAGE_PAID_TRAFFIC_THRESHOLD = 0.8;
|
|
17
17
|
const NON_HOMEPAGE_PAID_TRAFFIC_THRESHOLD = 0.5;
|
|
@@ -44,8 +44,7 @@ function convertToOpportunity(traffic) {
|
|
|
44
44
|
};
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
function hasHighInorganicTraffic(
|
|
48
|
-
const { url, paid, total } = traffic;
|
|
47
|
+
function hasHighInorganicTraffic(url, paid, total) {
|
|
49
48
|
const isHomepage = new URL(url).pathname === '/';
|
|
50
49
|
const threshold = isHomepage
|
|
51
50
|
? HOMEPAGE_PAID_TRAFFIC_THRESHOLD
|
|
@@ -53,19 +52,39 @@ function hasHighInorganicTraffic(traffic) {
|
|
|
53
52
|
return paid / total > threshold;
|
|
54
53
|
}
|
|
55
54
|
|
|
56
|
-
function hasHighBounceRate(ctr) {
|
|
57
|
-
return ctr < BOUNCE_RATE_THRESHOLD;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
55
|
function handler(bundles, opts = {}) {
|
|
61
56
|
const { interval = 7 } = opts;
|
|
62
|
-
const trafficByUrl = trafficAcquisition.handler(bundles);
|
|
63
|
-
const ctrByUrl = getCTRByUrl(bundles);
|
|
64
57
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
58
|
+
const dataChunks = new DataChunks();
|
|
59
|
+
|
|
60
|
+
loadBundles(bundles, dataChunks);
|
|
61
|
+
|
|
62
|
+
dataChunks.addFacet('urls', facets.url);
|
|
63
|
+
|
|
64
|
+
dataChunks.addSeries('views', (bundle) => bundle.weight);
|
|
65
|
+
dataChunks.addSeries('clicks', (bundle) => (bundle.events.some((e) => e.checkpoint === 'click') ? bundle.weight : 0));
|
|
66
|
+
|
|
67
|
+
const memo = {};
|
|
68
|
+
dataChunks.addSeries('earned', trafficSeriesFn(memo, 'earned'));
|
|
69
|
+
dataChunks.addSeries('owned', trafficSeriesFn(memo, 'owned'));
|
|
70
|
+
dataChunks.addSeries('paid', trafficSeriesFn(memo, 'paid'));
|
|
71
|
+
|
|
72
|
+
return dataChunks.facets.urls
|
|
73
|
+
.filter((url) => url.metrics.views.sum > interval * DAILY_PAGEVIEW_THRESHOLD)
|
|
74
|
+
.filter((url) => hasHighInorganicTraffic(
|
|
75
|
+
url.value,
|
|
76
|
+
url.metrics.paid.sum,
|
|
77
|
+
url.metrics.views.sum,
|
|
78
|
+
))
|
|
79
|
+
.filter((url) => url.metrics.clicks.sum / url.metrics.views.sum < BOUNCE_RATE_THRESHOLD)
|
|
80
|
+
.map((url) => ({
|
|
81
|
+
url: url.value,
|
|
82
|
+
total: url.metrics.views.sum,
|
|
83
|
+
earned: url.metrics.earned.sum,
|
|
84
|
+
owned: url.metrics.owned.sum,
|
|
85
|
+
paid: url.metrics.paid.sum,
|
|
86
|
+
bounceRate: 1 - url.metrics.clicks.sum / url.metrics.views.sum,
|
|
87
|
+
}))
|
|
69
88
|
.map(convertToOpportunity);
|
|
70
89
|
}
|
|
71
90
|
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* governing permissions and limitations under the License.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import {
|
|
13
|
+
import { classifyTraffic } from '../common/traffic.js';
|
|
14
14
|
|
|
15
15
|
const MAIN_TYPES = ['total', 'paid', 'earned', 'owned'];
|
|
16
16
|
|
|
@@ -40,21 +40,21 @@ function transformFormat(trafficSources) {
|
|
|
40
40
|
}));
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
function formatTraffic(row) {
|
|
44
|
+
const {
|
|
45
|
+
url, weight, type, category, vendor,
|
|
46
|
+
} = row;
|
|
47
|
+
return {
|
|
48
|
+
url,
|
|
49
|
+
weight,
|
|
50
|
+
trafficSource: vendor ? `${type}:${category}:${vendor}` : `${type}:${category}`,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
43
54
|
function handler(bundles) {
|
|
44
55
|
const trafficSources = bundles
|
|
45
|
-
.map(
|
|
46
|
-
.map(
|
|
47
|
-
const {
|
|
48
|
-
type,
|
|
49
|
-
category,
|
|
50
|
-
vendor,
|
|
51
|
-
} = classifyTrafficSource(row.url, row.referrer, row.utmSource, row.utmMedium, row.tracking);
|
|
52
|
-
return {
|
|
53
|
-
url: row.url,
|
|
54
|
-
weight: row.weight,
|
|
55
|
-
trafficSource: vendor ? `${type}:${category}:${vendor}` : `${type}:${category}`,
|
|
56
|
-
};
|
|
57
|
-
})
|
|
56
|
+
.map(classifyTraffic)
|
|
57
|
+
.map(formatTraffic)
|
|
58
58
|
.reduce(collectByUrlAndTrafficSource, {});
|
|
59
59
|
|
|
60
60
|
return transformFormat(trafficSources)
|
package/src/utils.js
CHANGED
|
@@ -11,6 +11,26 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { context as h2, h1 } from '@adobe/fetch';
|
|
14
|
+
import { utils } from '@adobe/rum-distiller';
|
|
15
|
+
import { classifyTraffic } from './common/traffic.js';
|
|
16
|
+
|
|
17
|
+
export const DELIMITER = '≡';
|
|
18
|
+
|
|
19
|
+
export const generateKey = (...keys) => keys.join(DELIMITER);
|
|
20
|
+
|
|
21
|
+
export const trafficSeriesFn = (memo, type) => (bundle) => {
|
|
22
|
+
const key = generateKey(bundle.url, bundle.id, bundle.time);
|
|
23
|
+
if (!memo[key]) {
|
|
24
|
+
// eslint-disable-next-line no-param-reassign
|
|
25
|
+
memo[key] = classifyTraffic(bundle).type;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return type === memo[key] ? bundle.weight : 0;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const loadBundles = (bundles, dataChunks) => {
|
|
32
|
+
dataChunks.load([{ rumBundles: bundles.map(utils.addCalculatedProps) }]);
|
|
33
|
+
};
|
|
14
34
|
|
|
15
35
|
/* c8 ignore next 3 */
|
|
16
36
|
export const { fetch } = process.env.HELIX_FETCH_FORCE_HTTP1
|