@adobe/spacecat-shared-rum-api-client 2.28.0 → 2.29.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.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
+
3
+
4
+ ### Features
5
+
6
+ * 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))
7
+
8
+ # [@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)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * **deps:** update external fixes ([#779](https://github.com/adobe/spacecat-shared/issues/779)) ([07f8cce](https://github.com/adobe/spacecat-shared/commit/07f8cce73e33bfb9c61fe14f2ef28012b872437d))
14
+
1
15
  # [@adobe/spacecat-shared-rum-api-client-v2.28.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-rum-api-client-v2.27.2...@adobe/spacecat-shared-rum-api-client-v2.28.0) (2025-05-30)
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.28.0",
3
+ "version": "2.29.0",
4
4
  "description": "Shared modules of the Spacecat Services - Rum API client",
5
5
  "type": "module",
6
6
  "engines": {
@@ -46,7 +46,7 @@
46
46
  "devDependencies": {
47
47
  "chai": "5.2.0",
48
48
  "chai-as-promised": "8.0.1",
49
- "nock": "14.0.4",
49
+ "nock": "14.0.5",
50
50
  "sinon": "20.0.0",
51
51
  "sinon-chai": "4.0.0",
52
52
  "typescript": "5.8.3"
@@ -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, sources, siteAvgCTR, ctrByUrlAndVendor, pageOnTime,
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
- function hasHighOrganicTraffic(interval, traffic) {
110
- const { earned } = traffic;
111
- return earned >= DAILY_EARNED_THRESHOLD * interval;
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
- function hasLowerCTR(ctr, siteAvgCTR) {
115
- return ctr < CTR_THRESHOLD_RATIO * siteAvgCTR;
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
- function handler(bundles, opts = {}) {
119
- const { interval = 7 } = opts;
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 trafficByUrl.filter((traffic) => traffic.total > interval * DAILY_PAGEVIEW_THRESHOLD)
126
- .filter(hasHighOrganicTraffic.bind(null, interval))
127
- .filter((traffic) => hasLowerCTR(ctrByUrlAndVendor[traffic.url].value, siteAvgCTR))
128
- .map((traffic) => ({
129
- ...traffic,
130
- ctr: ctrByUrlAndVendor[traffic.url].value,
131
- siteAvgCTR,
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,