@adobe/spacecat-shared-rum-api-client 2.30.0 → 2.31.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 CHANGED
@@ -1,3 +1,17 @@
1
+ # [@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)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * check status before parsing rum bundles ([#807](https://github.com/adobe/spacecat-shared/issues/807)) ([36566e4](https://github.com/adobe/spacecat-shared/commit/36566e4ab1cb8e72e3e5e0673df3171c7793ab82))
7
+
8
+ # [@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)
9
+
10
+
11
+ ### Features
12
+
13
+ * expose urls with most paid traffic ([#825](https://github.com/adobe/spacecat-shared/issues/825)) ([88dde01](https://github.com/adobe/spacecat-shared/commit/88dde01843ae32643a54d93868b7ef7eec10a62a))
14
+
1
15
  # [@adobe/spacecat-shared-rum-api-client-v2.30.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-rum-api-client-v2.29.0...@adobe/spacecat-shared-rum-api-client-v2.30.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.30.0",
3
+ "version": "2.31.1",
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
 
@@ -15,8 +15,21 @@ import { DELIMITER, generateKey, loadBundles } from '../utils.js';
15
15
  import { classifyTraffic } from '../common/traffic.js';
16
16
  import { getPageType, isConsentClick } from '../common/page.js';
17
17
 
18
- function getTrafficSource(bundle) {
19
- return classifyTraffic(bundle).type;
18
+ function getTrafficSource(bundle, memo) {
19
+ const id = `${bundle.id}-${bundle.url}-${bundle.time}`;
20
+ if (id in memo) {
21
+ return memo[id];
22
+ }
23
+ const result = classifyTraffic(bundle);
24
+ // eslint-disable-next-line no-param-reassign
25
+ memo[id] = result;
26
+ return result;
27
+ }
28
+
29
+ function getTrafficSourceKey(bundle, memo) {
30
+ const classifyResult = getTrafficSource(bundle, memo);
31
+ const { type, category, vendor } = classifyResult;
32
+ return `${type}:${category}:${vendor}`;
20
33
  }
21
34
 
22
35
  function getDeviceType(bundle) {
@@ -35,36 +48,54 @@ function addPageTypeDeviceTypeFacet(dataChunks, pageTypes) {
35
48
  });
36
49
  }
37
50
 
38
- function addPageTypeTrafficSourceDeviceTypes(dataChunks, pageTypes) {
51
+ function addPageTypeTrafficSourceDeviceTypes(dataChunks, pageTypes, memo) {
39
52
  dataChunks.addFacet('pageTrafficDeviceTypes', (bundle) => {
40
53
  const deviceType = getDeviceType(bundle);
41
54
  const pageType = getPageType(bundle, pageTypes);
42
- return generateKey(pageType, getTrafficSource(bundle), deviceType);
55
+ return generateKey(pageType, getTrafficSourceKey(bundle, memo), deviceType);
43
56
  });
44
57
  }
45
58
 
46
- function addPageTypeTrafficSourceFacet(dataChunks, pageTypes) {
59
+ function addPageTypeTrafficSourceFacet(dataChunks, pageTypes, memo) {
47
60
  dataChunks.addFacet('pageTypeTrafficSources', (bundle) => {
48
61
  const pageType = getPageType(bundle, pageTypes);
49
- return generateKey(pageType, getTrafficSource(bundle));
62
+ return generateKey(pageType, getTrafficSourceKey(bundle, memo));
50
63
  });
51
64
  }
52
65
 
53
- function handler(bundles, options = { pageTypes: null }) {
66
+ /**
67
+ * Handler for traffic metrics.
68
+ * @param {Array} bundles - The RUM bundles.
69
+ * @param {Object} options - Options object.
70
+ * @param {Object} [options.pageTypes] - Page type regex or mapping.
71
+ * @param {string} [options.trafficType] - Eg, 'paid', 'earned', 'owned', 'all'. Defaults to 'all'.
72
+ */
73
+ function handler(bundles, options = { pageTypes: null, trafficType: 'all' }) {
54
74
  const dataChunks = new DataChunks();
55
- const { pageTypes: pageTypeOpt } = options;
75
+ const trafficSourceMemo = {};
76
+ const { pageTypes: pageTypeOpt, trafficType = 'all' } = options;
77
+
78
+ let filteredBundles = bundles;
79
+ if (trafficType && trafficType !== 'all') {
80
+ filteredBundles = bundles
81
+ .filter((bundle) => getTrafficSource(bundle, trafficSourceMemo).type === trafficType);
82
+ }
83
+
84
+ const getTS = (bundle) => getTrafficSourceKey(bundle, trafficSourceMemo);
56
85
 
57
- loadBundles(bundles, dataChunks);
86
+ loadBundles(filteredBundles, dataChunks);
58
87
 
59
88
  const metricFilter = (metrics) => {
60
- const { ctr, enters, sumOfAllClicks } = metrics;
89
+ const {
90
+ ctr, enters, sumOfAllClicks, facet,
91
+ } = metrics;
61
92
  return {
62
- ctr: ctr.sum / ctr.weight,
93
+ ctr: ctr.weight !== 0 ? ctr.sum / ctr.weight : 0,
63
94
  clickedSessions: ctr.sum,
64
- totalSessions: ctr.weight,
95
+ pageViews: facet.weight,
65
96
  sessionsWithEnter: enters.sum,
66
- clicksOverViews: ctr.weight ? ctr.sum / ctr.weight : 0,
67
- bounceRate: ctr.weight ? (1 - (ctr.sum / ctr.weight)) : 0,
97
+ clicksOverViews: ctr.weight !== 0 ? ctr.sum / ctr.weight : 0,
98
+ bounceRate: ctr.weight !== 0 ? (1 - (ctr.sum / ctr.weight)) : 1,
68
99
  totalNumClicks: sumOfAllClicks.sum,
69
100
  avgClicksPerSession: ctr.sum ? sumOfAllClicks.sum / ctr.sum : 0,
70
101
  };
@@ -72,25 +103,25 @@ function handler(bundles, options = { pageTypes: null }) {
72
103
 
73
104
  dataChunks.addFacet('urls', (bundle) => bundle.url);
74
105
 
75
- dataChunks.addFacet('trafficSources', (bundle) => getTrafficSource(bundle));
106
+ dataChunks.addFacet('trafficSources', (bundle) => getTS(bundle));
76
107
 
77
- dataChunks.addFacet('urlTrafficSources', (bundle) => generateKey(bundle.url, getTrafficSource(bundle)));
108
+ dataChunks.addFacet('urlTrafficSources', (bundle) => generateKey(bundle.url, getTS(bundle)));
78
109
 
79
110
  dataChunks.addFacet('urlDeviceTypes', (bundle) => generateKey(bundle.url, getDeviceType(bundle)));
80
111
 
81
112
  dataChunks.addFacet('deviceTypes', (bundle) => getDeviceType(bundle));
82
113
 
83
- dataChunks.addFacet('urlTrafficSourceDeviceTypes', (bundle) => generateKey(bundle.url, getTrafficSource(bundle), getDeviceType(bundle)));
114
+ dataChunks.addFacet('urlTrafficSourceDeviceTypes', (bundle) => generateKey(bundle.url, getTS(bundle), getDeviceType(bundle)));
84
115
 
85
- dataChunks.addFacet('deviceTypeTrafficSources', (bundle) => generateKey(getDeviceType(bundle), getTrafficSource(bundle)));
116
+ dataChunks.addFacet('deviceTypeTrafficSources', (bundle) => generateKey(getDeviceType(bundle), getTS(bundle)));
86
117
 
87
118
  addPageTypeFacet(dataChunks, pageTypeOpt);
88
119
 
89
- addPageTypeTrafficSourceFacet(dataChunks, pageTypeOpt);
120
+ addPageTypeTrafficSourceFacet(dataChunks, pageTypeOpt, trafficSourceMemo);
90
121
 
91
122
  addPageTypeDeviceTypeFacet(dataChunks, pageTypeOpt);
92
123
 
93
- addPageTypeTrafficSourceDeviceTypes(dataChunks, pageTypeOpt);
124
+ addPageTypeTrafficSourceDeviceTypes(dataChunks, pageTypeOpt, trafficSourceMemo);
94
125
 
95
126
  dataChunks.addSeries('ctr', (bundle) => {
96
127
  const isClicked = bundle.events.some((e) => e.checkpoint === 'click');
@@ -111,100 +142,108 @@ function handler(bundles, options = { pageTypes: null }) {
111
142
  return containsEnter ? bundle.weight : 0;
112
143
  });
113
144
 
114
- const urls = dataChunks.facets.urls.map((facet) => {
115
- const url = facet.value;
116
- return {
117
- ...metricFilter(facet.metrics),
118
- url,
119
- };
120
- });
145
+ const urls = dataChunks.facets.urls.map((facet) => ({
146
+ ...metricFilter({ ...facet.metrics, facet }),
147
+ url: facet.value,
148
+ urls: [facet.value],
149
+ }));
121
150
 
122
151
  const pageType = dataChunks.facets.pageType.map((facet) => {
123
152
  const type = facet.value;
124
153
  return {
125
- ...metricFilter(facet.metrics),
154
+ ...metricFilter({ ...facet.metrics, facet }),
126
155
  type,
156
+ urls: [...new Set(facet.entries.map((b) => b.url))],
127
157
  };
128
158
  });
129
159
 
130
160
  const deviceTypes = dataChunks.facets.deviceTypes.map((facet) => {
131
161
  const deviceType = facet.value;
132
162
  return {
133
- ...metricFilter(facet.metrics),
163
+ ...metricFilter({ ...facet.metrics, facet }),
134
164
  deviceType,
165
+ urls: [...new Set(facet.entries.map((b) => b.url))],
135
166
  };
136
167
  });
137
168
 
138
169
  const urlDeviceTypes = dataChunks.facets.urlDeviceTypes.map((facet) => {
139
170
  const [url, deviceType] = facet.value.split(DELIMITER);
140
171
  return {
141
- ...metricFilter(facet.metrics),
172
+ ...metricFilter({ ...facet.metrics, facet }),
142
173
  url,
143
174
  deviceType,
175
+ urls: [...new Set(facet.entries.map((b) => b.url))],
144
176
  };
145
177
  });
146
178
 
147
179
  const trafficSources = dataChunks.facets.trafficSources.map((facet) => {
148
180
  const source = facet.value;
149
181
  return {
150
- ...metricFilter(facet.metrics),
182
+ ...metricFilter({ ...facet.metrics, facet }),
151
183
  source,
184
+ urls: [...new Set(facet.entries.map((b) => b.url))],
152
185
  };
153
186
  });
154
187
 
155
188
  const urlTrafficSources = dataChunks.facets.urlTrafficSources.map((facet) => {
156
189
  const [url, source] = facet.value.split(DELIMITER);
157
190
  return {
158
- ...metricFilter(facet.metrics),
191
+ ...metricFilter({ ...facet.metrics, facet }),
159
192
  url,
160
193
  source,
194
+ urls: [...new Set(facet.entries.map((b) => b.url))],
161
195
  };
162
196
  });
163
197
 
164
198
  const urlTrafficSourceDeviceTypes = dataChunks.facets.urlTrafficSourceDeviceTypes.map((facet) => {
165
199
  const [url, source, deviceType] = facet.value.split(DELIMITER);
166
200
  return {
167
- ...metricFilter(facet.metrics),
201
+ ...metricFilter({ ...facet.metrics, facet }),
168
202
  url,
169
203
  source,
170
204
  deviceType,
205
+ urls: [...new Set(facet.entries.map((b) => b.url))],
171
206
  };
172
207
  });
173
208
 
174
209
  const pageTypeTrafficSources = dataChunks.facets.pageTypeTrafficSources.map((facet) => {
175
210
  const [type, source] = facet.value.split(DELIMITER);
176
211
  return {
177
- ...metricFilter(facet.metrics),
212
+ ...metricFilter({ ...facet.metrics, facet }),
178
213
  type,
179
214
  source,
215
+ urls: [...new Set(facet.entries.map((b) => b.url))],
180
216
  };
181
217
  });
182
218
 
183
219
  const pageTypeDeviceTypes = dataChunks.facets.pageTypeDeviceTypes.map((facet) => {
184
220
  const [type, deviceType] = facet.value.split(DELIMITER);
185
221
  return {
186
- ...metricFilter(facet.metrics),
222
+ ...metricFilter({ ...facet.metrics, facet }),
187
223
  type,
188
224
  deviceType,
225
+ urls: [...new Set(facet.entries.map((b) => b.url))],
189
226
  };
190
227
  });
191
228
 
192
229
  const deviceTypeTrafficSources = dataChunks.facets.deviceTypeTrafficSources.map((facet) => {
193
230
  const [deviceType, source] = facet.value.split(DELIMITER);
194
231
  return {
195
- ...metricFilter(facet.metrics),
232
+ ...metricFilter({ ...facet.metrics, facet }),
196
233
  deviceType,
197
234
  source,
235
+ urls: [...new Set(facet.entries.map((b) => b.url))],
198
236
  };
199
237
  });
200
238
 
201
239
  const pageTrafficDeviceTypes = dataChunks.facets.pageTrafficDeviceTypes.map((facet) => {
202
240
  const [type, source, deviceType] = facet.value.split(DELIMITER);
203
241
  return {
204
- ...metricFilter(facet.metrics),
242
+ ...metricFilter({ ...facet.metrics, facet }),
205
243
  type,
206
244
  source,
207
245
  deviceType,
246
+ urls: [...new Set(facet.entries.map((b) => b.url))],
208
247
  };
209
248
  });
210
249
 
package/src/index.js CHANGED
@@ -122,7 +122,6 @@ export default class RUMAPIClient {
122
122
 
123
123
  try {
124
124
  const domainkey = await this._getDomainkey(opts);
125
-
126
125
  const bundles = await fetchBundles({
127
126
  ...opts,
128
127
  domainkey,
@@ -130,7 +129,6 @@ export default class RUMAPIClient {
130
129
  }, this.log);
131
130
 
132
131
  this.log.info(`Query "${query}" fetched ${bundles.length} bundles`);
133
-
134
132
  return handler(bundles, opts);
135
133
  } catch (e) {
136
134
  throw new Error(`Query '${query}' failed. Opts: ${JSON.stringify(sanitize(opts))}. Reason: ${e.message}`);
@@ -164,7 +162,6 @@ export default class RUMAPIClient {
164
162
  }, this.log);
165
163
 
166
164
  const results = {};
167
-
168
165
  this.log.info(`Multi query ${JSON.stringify(queries.join(', '))} fetched ${bundles.length} bundles`);
169
166
 
170
167
  // Execute each query handler sequentially