@adobe/spacecat-shared-rum-api-client 2.29.0 → 2.31.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.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
+
3
+
4
+ ### Features
5
+
6
+ * expose urls with most paid traffic ([#825](https://github.com/adobe/spacecat-shared/issues/825)) ([88dde01](https://github.com/adobe/spacecat-shared/commit/88dde01843ae32643a54d93868b7ef7eec10a62a))
7
+
8
+ # [@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)
9
+
10
+
11
+ ### Features
12
+
13
+ * SITES-32914 implement `startDate` and `endDate` in RUM client ([#819](https://github.com/adobe/spacecat-shared/issues/819)) ([1520465](https://github.com/adobe/spacecat-shared/commit/15204653ca7a1c3f04b1df30afc1a72f74d2b254))
14
+
1
15
  # [@adobe/spacecat-shared-rum-api-client-v2.29.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-rum-api-client-v2.28.1...@adobe/spacecat-shared-rum-api-client-v2.29.0) (2025-06-20)
2
16
 
3
17
 
package/README.md CHANGED
@@ -46,6 +46,21 @@ const result = await rumApiClient.query('cwv', opts);
46
46
  console.log(`Query result: ${result}`);
47
47
  ```
48
48
 
49
+ **Using startTime and endTime for precise date ranges:**
50
+
51
+ ```js
52
+ const opts = {
53
+ domain: 'www.aem.live',
54
+ domainkey: '<domain-key>',
55
+ granularity: 'daily',
56
+ startTime: '2024-01-01T00:00:00Z',
57
+ endTime: '2024-01-31T23:59:59Z'
58
+ };
59
+
60
+ const result = await rumApiClient.query('cwv', opts);
61
+ console.log(`Query result: ${result}`);
62
+ ```
63
+
49
64
  **Note**: All query names must be lowercase.
50
65
 
51
66
  ### Query Options: the 'opts' object
@@ -54,8 +69,10 @@ console.log(`Query result: ${result}`);
54
69
  |-------------|----------|---------|----------------------------------------------------------|
55
70
  | domain | yes | | The domain for which to fetch data. |
56
71
  | domainkey | no | | Provide directly or omit to auto-fetch using `RUM_ADMIN_KEY`. |
57
- | interval | no | 7 | Interval in days (integer). |
72
+ | interval | no | 7 | Interval in days (integer). Ignored when startTime/endTime are provided. |
58
73
  | granularity | no | daily | 'daily' or 'hourly'. |
74
+ | startTime | no | | Start time in ISO 8601 format (e.g., "2024-01-01T00:00:00Z"). Must be before endTime. Format `YYYY-MM-DD` or `YYYY-MM-DDTHH:MM:SSZ` |
75
+ | endTime | no | | End time in ISO 8601 format (e.g., "2024-01-31T23:59:59Z"). Must be after startTime. Format `YYYY-MM-DD` or `YYYY-MM-DDTHH:MM:SSZ` |
59
76
 
60
77
 
61
78
  ### Retrieving and Caching the Domainkey
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/spacecat-shared-rum-api-client",
3
- "version": "2.29.0",
3
+ "version": "2.31.0",
4
4
  "description": "Shared modules of the Spacecat Services - Rum API client",
5
5
  "type": "module",
6
6
  "engines": {
@@ -22,6 +22,24 @@ const ONE_DAY = ONE_HOUR * HOURS_IN_DAY;
22
22
 
23
23
  const CHUNK_SIZE = 31;
24
24
 
25
+ /**
26
+ * Parses a date string and returns a Date object.
27
+ * Supports both ISO 8601 format (e.g., "2024-01-01T00:00:00Z")
28
+ * and simple date format (e.g., "2024-01-01").
29
+ * For simple date strings, assumes UTC timezone.
30
+ * @param {string} dateString - The date string to parse
31
+ * @returns {Date} The parsed Date object
32
+ */
33
+ function parseDate(dateString) {
34
+ // If it's a simple date string (YYYY-MM-DD), convert to ISO format
35
+ if (/^\d{4}-\d{2}-\d{2}$/.test(dateString)) {
36
+ return new Date(`${dateString}T00:00:00.000Z`);
37
+ }
38
+
39
+ // Otherwise, let the Date constructor handle it (ISO 8601, etc.)
40
+ return new Date(dateString);
41
+ }
42
+
25
43
  function isBotTraffic(bundle) {
26
44
  return bundle?.userAgent?.includes('bot');
27
45
  }
@@ -54,6 +72,24 @@ function getUrlChunks(urls, chunkSize) {
54
72
  .map((_, index) => urls.slice(index * chunkSize, (index + 1) * chunkSize));
55
73
  }
56
74
 
75
+ function generateUrlsForDateRange(startDate, endDate, domain, granularity, domainkey) {
76
+ const urls = [];
77
+ const currentDate = parseDate(startDate);
78
+ const endDateTime = parseDate(endDate);
79
+
80
+ while (currentDate < endDateTime) {
81
+ urls.push(constructUrl(domain, currentDate, granularity, domainkey));
82
+
83
+ if (granularity.toUpperCase() === GRANULARITY.HOURLY) {
84
+ currentDate.setUTCHours(currentDate.getUTCHours() + 1);
85
+ } else {
86
+ currentDate.setUTCDate(currentDate.getUTCDate() + 1);
87
+ }
88
+ }
89
+
90
+ return urls;
91
+ }
92
+
57
93
  /* c8 ignore start */
58
94
  /*
59
95
  * throw-away code for a single customer who customized the experimentation engine
@@ -165,23 +201,46 @@ async function fetchBundles(opts, log) {
165
201
  granularity = GRANULARITY.DAILY,
166
202
  checkpoints = [],
167
203
  filterBotTraffic = true,
204
+ startTime,
205
+ endTime,
168
206
  } = opts;
169
207
 
170
208
  if (!hasText(domain) || !hasText(domainkey)) {
171
209
  throw new Error('Missing required parameters');
172
210
  }
173
211
 
174
- const multiplier = granularity.toUpperCase() === GRANULARITY.HOURLY ? ONE_HOUR : ONE_DAY;
175
- const range = granularity.toUpperCase() === GRANULARITY.HOURLY
176
- ? interval * HOURS_IN_DAY
177
- : interval + 1;
212
+ // Validate startTime and endTime if provided
213
+ if (startTime && endTime) {
214
+ const start = parseDate(startTime);
215
+ const end = parseDate(endTime);
178
216
 
179
- const urls = [];
180
- const currentDate = new Date();
217
+ if (Number.isNaN(start.getTime()) || Number.isNaN(end.getTime())) {
218
+ throw new Error('Invalid startTime or endTime format. Use ISO 8601 format (e.g., "2024-01-01T00:00:00Z") or simple date format (e.g., "2024-01-01")');
219
+ }
181
220
 
182
- for (let i = 0; i < range; i += 1) {
183
- const date = new Date(currentDate.getTime() - i * multiplier);
184
- urls.push(constructUrl(domain, date, granularity, domainkey));
221
+ if (start >= end) {
222
+ throw new Error('startTime must be before endTime');
223
+ }
224
+ }
225
+
226
+ let urls = [];
227
+
228
+ if (startTime && endTime) {
229
+ // Use custom date range
230
+ urls = generateUrlsForDateRange(startTime, endTime, domain, granularity, domainkey);
231
+ } else {
232
+ // Use existing interval-based logic
233
+ const multiplier = granularity.toUpperCase() === GRANULARITY.HOURLY ? ONE_HOUR : ONE_DAY;
234
+ const range = granularity.toUpperCase() === GRANULARITY.HOURLY
235
+ ? interval * HOURS_IN_DAY
236
+ : interval + 1;
237
+
238
+ const currentDate = new Date();
239
+
240
+ for (let i = 0; i < range; i += 1) {
241
+ const date = new Date(currentDate.getTime() - i * multiplier);
242
+ urls.push(constructUrl(domain, date, granularity, domainkey));
243
+ }
185
244
  }
186
245
 
187
246
  const chunks = getUrlChunks(urls, CHUNK_SIZE);
@@ -197,7 +256,6 @@ async function fetchBundles(opts, log) {
197
256
  return response;
198
257
  }));
199
258
  const bundles = await Promise.all(responses.map((response) => response.json()));
200
-
201
259
  bundles.forEach((b) => {
202
260
  b.rumBundles
203
261
  .filter((bundle) => !filterBotTraffic || !isBotTraffic(bundle))
@@ -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.d.ts CHANGED
@@ -28,6 +28,16 @@ export interface RUMAPIOptions {
28
28
  */
29
29
  interval?: number;
30
30
 
31
+ /**
32
+ * Start Date
33
+ */
34
+ startDate?: string;
35
+
36
+ /**
37
+ * End Date
38
+ */
39
+ endDate?: string;
40
+
31
41
  /**
32
42
  * Granularity can be 'hourly' or 'daily'.
33
43
  * @default 'daily'