@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/spacecat-shared-rum-api-client",
3
- "version": "2.31.0",
3
+ "version": "2.32.0",
4
4
  "description": "Shared modules of the Spacecat Services - Rum API client",
5
5
  "type": "module",
6
6
  "engines": {
@@ -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
- const bundles = await Promise.all(responses.map((response) => response.json()));
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