@adobe/spacecat-shared-rum-api-client 2.31.0 → 2.32.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/package.json +1 -1
- package/src/common/rum-bundler-client.js +37 -1
- package/src/functions/traffic-analysis.js +129 -0
- package/src/index.js +2 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
# [@adobe/spacecat-shared-rum-api-client-v2.32.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-rum-api-client-v2.31.1...@adobe/spacecat-shared-rum-api-client-v2.32.0) (2025-07-11)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* add RUM API Client handler for paid traffic analysis import ([#841](https://github.com/adobe/spacecat-shared/issues/841)) ([6e6656e](https://github.com/adobe/spacecat-shared/commit/6e6656efb7e9741b52d26c21ea910988b219e2cc))
|
|
7
|
+
|
|
8
|
+
# [@adobe/spacecat-shared-rum-api-client-v2.31.1](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-rum-api-client-v2.31.0...@adobe/spacecat-shared-rum-api-client-v2.31.1) (2025-06-30)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* check status before parsing rum bundles ([#807](https://github.com/adobe/spacecat-shared/issues/807)) ([36566e4](https://github.com/adobe/spacecat-shared/commit/36566e4ab1cb8e72e3e5e0673df3171c7793ab82))
|
|
14
|
+
|
|
1
15
|
# [@adobe/spacecat-shared-rum-api-client-v2.31.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-rum-api-client-v2.30.0...@adobe/spacecat-shared-rum-api-client-v2.31.0) (2025-06-25)
|
|
2
16
|
|
|
3
17
|
|
package/package.json
CHANGED
|
@@ -57,6 +57,20 @@ function filterEvents(checkpoints = []) {
|
|
|
57
57
|
};
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
function sanitizeURL(url) {
|
|
61
|
+
try {
|
|
62
|
+
const parsedUrl = new URL(url);
|
|
63
|
+
if (parsedUrl.searchParams.has('domainkey')) {
|
|
64
|
+
parsedUrl.searchParams.set('domainkey', 'redacted');
|
|
65
|
+
}
|
|
66
|
+
return parsedUrl.toString();
|
|
67
|
+
/* c8 ignore next 4 */
|
|
68
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
69
|
+
} catch (e) {
|
|
70
|
+
return url;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
60
74
|
function constructUrl(domain, date, granularity, domainkey) {
|
|
61
75
|
const year = date.getUTCFullYear();
|
|
62
76
|
const month = (date.getUTCMonth() + 1).toString().padStart(2, '0');
|
|
@@ -246,6 +260,7 @@ async function fetchBundles(opts, log) {
|
|
|
246
260
|
const chunks = getUrlChunks(urls, CHUNK_SIZE);
|
|
247
261
|
|
|
248
262
|
let totalTransferSize = 0;
|
|
263
|
+
const failedUrls = [];
|
|
249
264
|
|
|
250
265
|
const result = [];
|
|
251
266
|
for (const chunk of chunks) {
|
|
@@ -255,7 +270,21 @@ async function fetchBundles(opts, log) {
|
|
|
255
270
|
totalTransferSize += parseInt(response.headers.get('content-length'), 10);
|
|
256
271
|
return response;
|
|
257
272
|
}));
|
|
258
|
-
|
|
273
|
+
|
|
274
|
+
const bundlesRaw = await Promise.all(
|
|
275
|
+
responses.map(async (response, index) => {
|
|
276
|
+
if (response.ok) {
|
|
277
|
+
return response.json();
|
|
278
|
+
} else {
|
|
279
|
+
const failedUrl = response.url || chunk[index];
|
|
280
|
+
log.warn(`Skipping response at index ${index}: status ${response.status} - url: ${sanitizeURL(failedUrl)}`);
|
|
281
|
+
failedUrls.push(failedUrl);
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
}),
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
const bundles = bundlesRaw.filter(Boolean);
|
|
259
288
|
bundles.forEach((b) => {
|
|
260
289
|
b.rumBundles
|
|
261
290
|
.filter((bundle) => !filterBotTraffic || !isBotTraffic(bundle))
|
|
@@ -264,6 +293,13 @@ async function fetchBundles(opts, log) {
|
|
|
264
293
|
});
|
|
265
294
|
}
|
|
266
295
|
log.info(`Retrieved RUM bundles. Total transfer size (in KB): ${(totalTransferSize / 1024).toFixed(2)}`);
|
|
296
|
+
|
|
297
|
+
// Add failedUrls to opts object for access by callers
|
|
298
|
+
if (failedUrls.length > 0) {
|
|
299
|
+
// eslint-disable-next-line no-param-reassign
|
|
300
|
+
opts.failedUrls = failedUrls;
|
|
301
|
+
}
|
|
302
|
+
|
|
267
303
|
return mergeBundlesWithSameId(result);
|
|
268
304
|
}
|
|
269
305
|
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2024 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { utils } from '@adobe/rum-distiller';
|
|
14
|
+
import { classifyTraffic } from '../common/traffic.js';
|
|
15
|
+
|
|
16
|
+
function getUTM(bundle, type) {
|
|
17
|
+
return bundle.events
|
|
18
|
+
.find((e) => e.checkpoint === 'utm' && e.source === `utm_${type}`)
|
|
19
|
+
?.target || null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function getCWV(bundle, metric) {
|
|
23
|
+
const measurements = bundle.events
|
|
24
|
+
.filter((e) => e.checkpoint === `cwv-${metric}`)
|
|
25
|
+
.map((e) => e.value);
|
|
26
|
+
|
|
27
|
+
return measurements.length > 0 ? Math.max(...measurements) : null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function containsEngagedScroll(bundle) {
|
|
31
|
+
return bundle.events
|
|
32
|
+
.some((e) => (e.checkpoint === 'viewmedia' || e.checkpoint === 'viewblock') && e.timeDelta >= 10000)
|
|
33
|
+
? 1 : 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function getNotFound(bundle) {
|
|
37
|
+
return bundle.events
|
|
38
|
+
.find((e) => e.checkpoint === '404')
|
|
39
|
+
?.source || null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function getReferrer(bundle) {
|
|
43
|
+
const enterCheckpoint = bundle.events
|
|
44
|
+
.find((e) => e.checkpoint === 'enter')
|
|
45
|
+
?.source;
|
|
46
|
+
|
|
47
|
+
const navigateCheckpoint = bundle.events
|
|
48
|
+
.find((e) => e.checkpoint === 'navigate')
|
|
49
|
+
?.source;
|
|
50
|
+
|
|
51
|
+
return navigateCheckpoint || enterCheckpoint || null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function getClicked(bundle) {
|
|
55
|
+
const latestClickEvent = bundle.events
|
|
56
|
+
.filter((e) => e.checkpoint === 'click')
|
|
57
|
+
.reduce((latest, current) => {
|
|
58
|
+
if (!latest || !latest.timeDelta) return current;
|
|
59
|
+
if (current?.timeDelta > latest.timeDelta) return current;
|
|
60
|
+
return latest;
|
|
61
|
+
}, null);
|
|
62
|
+
|
|
63
|
+
if (!latestClickEvent) return 0;
|
|
64
|
+
|
|
65
|
+
const isConsentClick = !!utils.reclassifyConsent(latestClickEvent).vendor;
|
|
66
|
+
|
|
67
|
+
if (isConsentClick) return 0;
|
|
68
|
+
|
|
69
|
+
return 1;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function getConsent(bundle) {
|
|
73
|
+
const consentBannerStatus = bundle.events
|
|
74
|
+
.find((e) => e.checkpoint === 'consent')
|
|
75
|
+
?.target;
|
|
76
|
+
|
|
77
|
+
const consentClick = bundle.events.find((e) => e.checkpoint === 'click' && utils.reclassifyConsent(e).vendor);
|
|
78
|
+
|
|
79
|
+
if (!consentClick) return consentBannerStatus || null;
|
|
80
|
+
|
|
81
|
+
return utils.reclassifyConsent(consentClick).target;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function trafficType(bundle, memo) {
|
|
85
|
+
const key = `${bundle.id}${bundle.url}${bundle.time}`;
|
|
86
|
+
if (memo[key]) return memo[key];
|
|
87
|
+
|
|
88
|
+
const type = classifyTraffic(bundle);
|
|
89
|
+
// eslint-disable-next-line no-param-reassign
|
|
90
|
+
memo[key] = type;
|
|
91
|
+
return type;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function handler(bundles) {
|
|
95
|
+
const memo = {};
|
|
96
|
+
|
|
97
|
+
const result = bundles.map((bundle) => {
|
|
98
|
+
/* eslint-disable camelcase */
|
|
99
|
+
const trafficData = trafficType(bundle, memo);
|
|
100
|
+
const clicked = getClicked(bundle);
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
path: new URL(bundle.url).pathname,
|
|
104
|
+
trf_type: trafficData.type,
|
|
105
|
+
trf_channel: trafficData.category,
|
|
106
|
+
trf_platform: trafficData.vendor || null,
|
|
107
|
+
device: bundle.userAgent.split(':')[0],
|
|
108
|
+
utm_source: getUTM(bundle, 'source'),
|
|
109
|
+
utm_medium: getUTM(bundle, 'medium'),
|
|
110
|
+
utm_campaign: getUTM(bundle, 'campaign'),
|
|
111
|
+
referrer: getReferrer(bundle),
|
|
112
|
+
consent: getConsent(bundle),
|
|
113
|
+
notfound: getNotFound(bundle),
|
|
114
|
+
pageviews: bundle.weight,
|
|
115
|
+
clicked,
|
|
116
|
+
engaged: containsEngagedScroll(bundle) || clicked,
|
|
117
|
+
lcp: getCWV(bundle, 'lcp'),
|
|
118
|
+
inp: getCWV(bundle, 'inp'),
|
|
119
|
+
cls: getCWV(bundle, 'cls'),
|
|
120
|
+
};
|
|
121
|
+
/* eslint-enable camelcase */
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
return result;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export default {
|
|
128
|
+
handler,
|
|
129
|
+
};
|
package/src/index.js
CHANGED
|
@@ -24,6 +24,7 @@ import trafficMetrics from './functions/traffic-metrics.js';
|
|
|
24
24
|
import rageclick from './functions/opportunities/rageclick.js';
|
|
25
25
|
import highInorganicHighBounceRate from './functions/opportunities/high-inorganic-high-bounce-rate.js';
|
|
26
26
|
import highOrganicLowCtr from './functions/opportunities/high-organic-low-ctr.js';
|
|
27
|
+
import trafficAnalysis from './functions/traffic-analysis.js';
|
|
27
28
|
|
|
28
29
|
// exported for tests
|
|
29
30
|
export const RUM_BUNDLER_API_HOST = 'https://bundles.aem.page';
|
|
@@ -42,6 +43,7 @@ const HANDLERS = {
|
|
|
42
43
|
'high-organic-low-ctr': highOrganicLowCtr,
|
|
43
44
|
pageviews,
|
|
44
45
|
trafficMetrics,
|
|
46
|
+
'traffic-analysis': trafficAnalysis,
|
|
45
47
|
};
|
|
46
48
|
|
|
47
49
|
function sanitize(opts) {
|
|
@@ -122,7 +124,6 @@ export default class RUMAPIClient {
|
|
|
122
124
|
|
|
123
125
|
try {
|
|
124
126
|
const domainkey = await this._getDomainkey(opts);
|
|
125
|
-
|
|
126
127
|
const bundles = await fetchBundles({
|
|
127
128
|
...opts,
|
|
128
129
|
domainkey,
|
|
@@ -130,7 +131,6 @@ export default class RUMAPIClient {
|
|
|
130
131
|
}, this.log);
|
|
131
132
|
|
|
132
133
|
this.log.info(`Query "${query}" fetched ${bundles.length} bundles`);
|
|
133
|
-
|
|
134
134
|
return handler(bundles, opts);
|
|
135
135
|
} catch (e) {
|
|
136
136
|
throw new Error(`Query '${query}' failed. Opts: ${JSON.stringify(sanitize(opts))}. Reason: ${e.message}`);
|
|
@@ -164,7 +164,6 @@ export default class RUMAPIClient {
|
|
|
164
164
|
}, this.log);
|
|
165
165
|
|
|
166
166
|
const results = {};
|
|
167
|
-
|
|
168
167
|
this.log.info(`Multi query ${JSON.stringify(queries.join(', '))} fetched ${bundles.length} bundles`);
|
|
169
168
|
|
|
170
169
|
// Execute each query handler sequentially
|