@adobe/spacecat-shared-rum-api-client 2.28.1 → 2.30.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.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
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* 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))
|
|
7
|
+
|
|
8
|
+
# [@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)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* high organic low ctr detection using traffic instead of thresholds ([#815](https://github.com/adobe/spacecat-shared/issues/815)) ([54569fb](https://github.com/adobe/spacecat-shared/commit/54569fb5d9b5fbe8400da0aadbae148380866fda))
|
|
14
|
+
|
|
1
15
|
# [@adobe/spacecat-shared-rum-api-client-v2.28.1](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-rum-api-client-v2.28.0...@adobe/spacecat-shared-rum-api-client-v2.28.1) (2025-05-31)
|
|
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
|
@@ -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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
212
|
+
// Validate startTime and endTime if provided
|
|
213
|
+
if (startTime && endTime) {
|
|
214
|
+
const start = parseDate(startTime);
|
|
215
|
+
const end = parseDate(endTime);
|
|
178
216
|
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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))
|
|
@@ -13,16 +13,15 @@
|
|
|
13
13
|
import trafficAcquisition from '../traffic-acquisition.js';
|
|
14
14
|
import { getCTRByUrlAndVendor, getSiteAvgCTR } from '../../common/aggregateFns.js';
|
|
15
15
|
|
|
16
|
-
const DAILY_EARNED_THRESHOLD = 1000;
|
|
17
|
-
const CTR_THRESHOLD_RATIO = 0.95;
|
|
18
|
-
const DAILY_PAGEVIEW_THRESHOLD = 1000;
|
|
19
16
|
const VENDORS_TO_CONSIDER = 5;
|
|
17
|
+
const MAX_OPPORTUNITIES = 10;
|
|
20
18
|
|
|
21
19
|
const MAIN_TYPES = ['paid', 'earned', 'owned'];
|
|
22
20
|
|
|
23
21
|
function convertToOpportunity(traffic) {
|
|
24
22
|
const {
|
|
25
|
-
url, total, ctr, paid, owned, earned,
|
|
23
|
+
url, total, ctr, paid, percentileScore, owned, earned,
|
|
24
|
+
sources, siteAvgCTR, ctrByUrlAndVendor, pageOnTime,
|
|
26
25
|
} = traffic;
|
|
27
26
|
|
|
28
27
|
const vendors = sources.reduce((acc, { type, views }) => {
|
|
@@ -51,6 +50,7 @@ function convertToOpportunity(traffic) {
|
|
|
51
50
|
trackedKPISiteAverage: siteAvgCTR,
|
|
52
51
|
pageViews: total,
|
|
53
52
|
samples: total, // todo: get the actual number of samples
|
|
53
|
+
percentileScore,
|
|
54
54
|
metrics: [{
|
|
55
55
|
type: 'traffic',
|
|
56
56
|
vendor: '*',
|
|
@@ -106,33 +106,48 @@ function convertToOpportunity(traffic) {
|
|
|
106
106
|
return opportunity;
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
109
|
+
/**
|
|
110
|
+
* Sort pages by earned AND overall traffic using percentile scoring.
|
|
111
|
+
* @param {Array} pages - List of { url, total, earned }
|
|
112
|
+
* @returns {Array} List of pages sorted by joint strength
|
|
113
|
+
*/
|
|
114
|
+
function sortPagesByEarnedAndOverallTraffic(pages) {
|
|
115
|
+
if (!Array.isArray(pages) || pages.length === 0) return [];
|
|
113
116
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
+
const sortedOverall = [...pages].sort((a, b) => a.total - b.total);
|
|
118
|
+
const sortedEarned = [...pages].sort((a, b) => {
|
|
119
|
+
if (a.earned === b.earned) {
|
|
120
|
+
return a.total - b.total;
|
|
121
|
+
}
|
|
122
|
+
return a.earned - b.earned;
|
|
123
|
+
});
|
|
124
|
+
const n = pages.length;
|
|
125
|
+
|
|
126
|
+
const percentiles = pages.map((p) => {
|
|
127
|
+
const totalPercentile = sortedOverall.findIndex((x) => x.url === p.url) / (n - 1);
|
|
128
|
+
const earnedPercentile = sortedEarned.findIndex((x) => x.url === p.url) / (n - 1);
|
|
129
|
+
const percentileScore = totalPercentile * earnedPercentile;
|
|
130
|
+
return { ...p, percentileScore };
|
|
131
|
+
});
|
|
117
132
|
|
|
118
|
-
|
|
119
|
-
|
|
133
|
+
return percentiles.sort((a, b) => b.percentileScore - a.percentileScore);
|
|
134
|
+
}
|
|
120
135
|
|
|
136
|
+
function handler(bundles) {
|
|
121
137
|
const trafficByUrl = trafficAcquisition.handler(bundles);
|
|
122
138
|
const ctrByUrlAndVendor = getCTRByUrlAndVendor(bundles);
|
|
123
139
|
const siteAvgCTR = getSiteAvgCTR(bundles);
|
|
140
|
+
const pagesSortedByEarnedAndOverallTraffic = sortPagesByEarnedAndOverallTraffic(
|
|
141
|
+
trafficByUrl,
|
|
142
|
+
).slice(0, MAX_OPPORTUNITIES);
|
|
124
143
|
|
|
125
|
-
return
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
ctrByUrlAndVendor: ctrByUrlAndVendor[traffic.url].vendors,
|
|
133
|
-
pageOnTime: traffic.maxTimeDelta,
|
|
134
|
-
}))
|
|
135
|
-
.map(convertToOpportunity);
|
|
144
|
+
return pagesSortedByEarnedAndOverallTraffic.map((traffic) => ({
|
|
145
|
+
...traffic,
|
|
146
|
+
ctr: ctrByUrlAndVendor[traffic.url].value,
|
|
147
|
+
siteAvgCTR,
|
|
148
|
+
ctrByUrlAndVendor: ctrByUrlAndVendor[traffic.url].vendors,
|
|
149
|
+
pageOnTime: traffic.maxTimeDelta,
|
|
150
|
+
})).map(convertToOpportunity);
|
|
136
151
|
}
|
|
137
152
|
|
|
138
153
|
export default {
|
|
@@ -47,7 +47,7 @@ function formatTraffic(row) {
|
|
|
47
47
|
url, weight, type, category, vendor, events = [],
|
|
48
48
|
} = row;
|
|
49
49
|
|
|
50
|
-
const maxTimeDelta = events.reduce((max, e) => Math.max(max, e.timeDelta), 0);
|
|
50
|
+
const maxTimeDelta = events.reduce((max, e) => Math.max(max, e.timeDelta || 0), 0);
|
|
51
51
|
|
|
52
52
|
return {
|
|
53
53
|
url,
|
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'
|