@adobe/spacecat-shared-utils 1.28.4 → 1.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-utils-v1.30.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-utils-v1.29.0...@adobe/spacecat-shared-utils-v1.30.0) (2025-02-06)
2
+
3
+
4
+ ### Features
5
+
6
+ * adding more opportunities utils for forms ([#580](https://github.com/adobe/spacecat-shared/issues/580)) ([4cb7d5d](https://github.com/adobe/spacecat-shared/commit/4cb7d5d94ff887932809416b3cf918fc3b56a58d))
7
+
8
+ # [@adobe/spacecat-shared-utils-v1.29.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-utils-v1.28.4...@adobe/spacecat-shared-utils-v1.29.0) (2025-02-03)
9
+
10
+
11
+ ### Features
12
+
13
+ * add utility api for forms metrics ([#566](https://github.com/adobe/spacecat-shared/issues/566)) ([a7365b6](https://github.com/adobe/spacecat-shared/commit/a7365b6eaabf6178a7febd2f25a4083e60d0156b))
14
+
1
15
  # [@adobe/spacecat-shared-utils-v1.28.4](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-utils-v1.28.3...@adobe/spacecat-shared-utils-v1.28.4) (2025-02-03)
2
16
 
3
17
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/spacecat-shared-utils",
3
- "version": "1.28.4",
3
+ "version": "1.30.0",
4
4
  "description": "Shared modules of the Spacecat Services - utils",
5
5
  "type": "module",
6
6
  "engines": {
@@ -39,7 +39,7 @@
39
39
  "chai": "5.1.2",
40
40
  "chai-as-promised": "8.0.1",
41
41
  "husky": "9.1.7",
42
- "nock": "13.5.6",
42
+ "nock": "14.0.0",
43
43
  "sinon": "19.0.2",
44
44
  "sinon-chai": "4.0.0"
45
45
  },
@@ -0,0 +1,196 @@
1
+ /*
2
+ * Copyright 2025 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
+ const DAILY_PAGEVIEW_THRESHOLD = 200;
14
+ const CR_THRESHOLD_RATIO = 0.2;
15
+ const MOBILE = 'mobile';
16
+ const DESKTOP = 'desktop';
17
+
18
+ /**
19
+ * Aggregates the form vitals by device type.
20
+ *
21
+ * @param {*} formVitalsCollection - form vitals collection
22
+ * @returns {object} - aggregated form vitals by device type
23
+ */
24
+ function aggregateFormVitalsByDevice(formVitalsCollection) {
25
+ const resultMap = new Map();
26
+
27
+ formVitalsCollection.forEach((item) => {
28
+ const {
29
+ url, formview = {}, formengagement = {}, pageview = {}, formsubmit = {},
30
+ } = item;
31
+
32
+ const totals = {
33
+ formview: { total: 0, desktop: 0, mobile: 0 },
34
+ formengagement: { total: 0, desktop: 0, mobile: 0 },
35
+ pageview: { total: 0, desktop: 0, mobile: 0 },
36
+ formsubmit: { total: 0, desktop: 0, mobile: 0 },
37
+ };
38
+
39
+ const calculateSums = (metric, initialTarget) => {
40
+ const updatedTarget = { ...initialTarget }; // Create a new object to store the updated totals
41
+ Object.entries(metric).forEach(([key, value]) => {
42
+ updatedTarget.total += value;
43
+ if (key.startsWith(DESKTOP)) {
44
+ updatedTarget.desktop += value;
45
+ } else if (key.startsWith(MOBILE)) {
46
+ updatedTarget.mobile += value;
47
+ }
48
+ });
49
+ return updatedTarget; // Return the updated target
50
+ };
51
+
52
+ totals.formview = calculateSums(formview, totals.formview);
53
+ totals.formengagement = calculateSums(formengagement, totals.formengagement);
54
+ totals.pageview = calculateSums(pageview, totals.pageview);
55
+ totals.formsubmit = calculateSums(formsubmit, totals.formsubmit);
56
+
57
+ resultMap.set(url, totals);
58
+ });
59
+
60
+ return resultMap;
61
+ }
62
+
63
+ function hasHighPageViews(interval, pageViews) {
64
+ return pageViews > DAILY_PAGEVIEW_THRESHOLD * interval;
65
+ }
66
+
67
+ function hasLowerConversionRate(formSubmit, formViews) {
68
+ return formSubmit / formViews < CR_THRESHOLD_RATIO;
69
+ }
70
+
71
+ function hasLowFormViews(pageViews, formViews) {
72
+ return formViews > 0 && (formViews / pageViews) < 0.7;
73
+ }
74
+
75
+ function hasHighPageViewLowFormCtr(ctaPageViews, ctaClicks, ctaPageTotalClicks, formPageViews) {
76
+ return ctaPageTotalClicks > 0
77
+ && (ctaClicks / ctaPageTotalClicks) < 0.4
78
+ && (formPageViews / ctaPageViews) < 0.1;
79
+ }
80
+
81
+ /**
82
+ * Returns the form urls with high form views and low conversion rate
83
+ *
84
+ * @param {*} formVitalsCollection - form vitals collection
85
+ * @returns {Array} - urls with high form views and low conversion rate
86
+ */
87
+ export function getHighFormViewsLowConversionMetrics(formVitalsCollection, interval) {
88
+ const resultMap = aggregateFormVitalsByDevice(formVitalsCollection);
89
+ const urls = [];
90
+ resultMap.forEach((metrics, url) => {
91
+ const pageViews = metrics.pageview.total;
92
+ // Default to pageViews if formViews are not available
93
+ const formViews = metrics.formview.total || pageViews;
94
+ const formEngagement = metrics.formengagement.total;
95
+ const formSubmit = metrics.formsubmit.total || formEngagement;
96
+
97
+ if (hasHighPageViews(interval, pageViews) && hasLowerConversionRate(formSubmit, formViews)) {
98
+ urls.push({
99
+ url,
100
+ pageViews,
101
+ formViews,
102
+ formEngagement,
103
+ formSubmit,
104
+ });
105
+ }
106
+ });
107
+ return urls;
108
+ }
109
+
110
+ /**
111
+ * Returns the form urls with high page views and low form views
112
+ *
113
+ * @param resultMap
114
+ * @returns {*[]}
115
+ */
116
+ export function getHighPageViewsLowFormViewsMetrics(formVitalsCollection, interval) {
117
+ const urls = [];
118
+ const resultMap = aggregateFormVitalsByDevice(formVitalsCollection);
119
+ resultMap.forEach((metrics, url) => {
120
+ const { total: pageViews } = metrics.pageview;
121
+ const { total: formViews } = metrics.formview;
122
+ const { total: formEngagement } = metrics.formengagement;
123
+
124
+ if (hasHighPageViews(interval, pageViews) && hasLowFormViews(pageViews, formViews)) {
125
+ urls.push({
126
+ url,
127
+ pageViews,
128
+ formViews,
129
+ formEngagement,
130
+ });
131
+ }
132
+ });
133
+ return urls;
134
+ }
135
+
136
+ /**
137
+ * Returns the form urls with high page views containing ctr and low form views
138
+ * @param formVitalsCollection
139
+ * @param formVitalsByDevice
140
+ * @returns {*[]}
141
+ */
142
+ export function getHighPageViewsLowFormCtrMetrics(formVitalsCollection, interval) {
143
+ const urls = [];
144
+ const formVitalsByDevice = aggregateFormVitalsByDevice(formVitalsCollection);
145
+ formVitalsCollection.forEach((entry) => {
146
+ const { forminternalnavigation, pageview } = entry;
147
+ // Calculate `x`: sum of pageview for the URL with the highest sum
148
+ let x = 0;
149
+ let maxPageviewUrl = null;
150
+ if (forminternalnavigation) {
151
+ forminternalnavigation.forEach((nav) => {
152
+ if (nav.pageview) {
153
+ const pageviewSum = Object.values(nav.pageview).reduce((sum, val) => sum + val, 0);
154
+ if (pageviewSum > x) {
155
+ x = pageviewSum;
156
+ maxPageviewUrl = nav;
157
+ }
158
+ }
159
+ });
160
+ }
161
+
162
+ // Skip entry if no valid maxPageviewUrl is found
163
+ if (!maxPageviewUrl) return;
164
+
165
+ // Calculate `y`: find the CTA with the highest clicks and include the source
166
+ const y = maxPageviewUrl.CTAs.reduce((maxCta, cta) => {
167
+ if (cta.clicks > (maxCta.clicks || 0)) {
168
+ return cta;
169
+ }
170
+ return maxCta;
171
+ }, { clicks: 0, source: '' });
172
+
173
+ // Get `z`: totalClicksOnPage for the matched URL
174
+ const z = maxPageviewUrl.totalClicksOnPage || 0;
175
+ // Calculate `f`: sum of `pageview` for `formengagement`
176
+ const f = Object.values(pageview).reduce((sum, val) => sum + val, 0);
177
+
178
+ // Evaluate conditions and add URL to the result if all are met
179
+ if (hasHighPageViews(interval, x) && hasHighPageViewLowFormCtr(x, y.clicks, z, f)) {
180
+ const deviceData = formVitalsByDevice.get(entry.url);
181
+ if (deviceData != null) {
182
+ urls.push({
183
+ url: entry.url,
184
+ pageViews: deviceData.pageview.total,
185
+ formViews: deviceData.formview.total,
186
+ formEngagement: deviceData.formengagement.total,
187
+ CTA: {
188
+ url: maxPageviewUrl.url,
189
+ source: y.source,
190
+ },
191
+ });
192
+ }
193
+ }
194
+ });
195
+ return urls;
196
+ }
package/src/index.d.ts CHANGED
@@ -163,7 +163,32 @@ declare function replacePlaceholders(content: string, placeholders: object): str
163
163
  * or null if an error occurs.
164
164
  */
165
165
  declare function getPrompt(placeholders: object, filename: string, log: object):
166
- Promise<string|null>;
166
+ Promise<string | null>;
167
+
168
+ /**
169
+ * Retrieves the high-form-view-low-form-conversion metrics from the provided array of form vitals.
170
+ * @param {Object[]} formVitals - An array of form vitals.
171
+ * @param {number} interval - The interval in days.
172
+ * @returns {Object[]} - An array of high-form-view-low-form-conversion metrics.
173
+ */
174
+ declare function getHighFormViewsLowConversionMetrics(formVitals: object[], interval: number):
175
+ object[];
176
+
177
+ /**
178
+ * Retrieves the high-page-view-low-form-view metrics from the provided array of form vitals.
179
+ * @param {Object[]} formVitals - An array of form vitals.
180
+ * @returns {Object[]} - An array of high-page-view-low-form-view metrics.
181
+ */
182
+ declare function getHighPageViewsLowFormViewsMetrics(formVitals: object[], interval: number):
183
+ object[];
184
+
185
+ /**
186
+ * Retrieves the high-page-view-low-form-ctr metrics from the provided array of form vitals.
187
+ * @param {Object[]} formVitals - An array of form vitals.
188
+ * @returns {Object[]} - An array of high-page-view-low-form-ctr metrics.
189
+ */
190
+ declare function getHighPageViewsLowFormCtrMetrics(formVitals: object[], interval: number):
191
+ object[];
167
192
 
168
193
  /**
169
194
  * Retrieves stored metrics from S3.
@@ -179,7 +204,7 @@ declare function getPrompt(placeholders: object, filename: string, log: object):
179
204
  * @returns {Promise<any|*[]>} - The stored metrics
180
205
  */
181
206
  export function getStoredMetrics(config: object, context: object):
182
- Promise<Array<object>>;
207
+ Promise<Array<object>>;
183
208
 
184
209
  /**
185
210
  * Stores metrics in S3.
@@ -198,8 +223,8 @@ export function getStoredMetrics(config: object, context: object):
198
223
  export function storeMetrics(content: object, config: object, context: object): Promise<string>;
199
224
 
200
225
  export function s3Wrapper(fn: (request: object, context: object) => Promise<Response>):
201
- (request: object, context: object) => Promise<Response>;
226
+ (request: object, context: object) => Promise<Response>;
202
227
 
203
- export function fetch(url: string|Request, options?: RequestOptions): Promise<Response>;
228
+ export function fetch(url: string | Request, options?: RequestOptions): Promise<Response>;
204
229
 
205
- export function tracingFetch(url: string|Request, options?: RequestOptions): Promise<Response>;
230
+ export function tracingFetch(url: string | Request, options?: RequestOptions): Promise<Response>;
package/src/index.js CHANGED
@@ -61,3 +61,4 @@ export { s3Wrapper } from './s3.js';
61
61
 
62
62
  export { fetch } from './adobe-fetch.js';
63
63
  export { tracingFetch } from './tracing-fetch.js';
64
+ export { getHighFormViewsLowConversionMetrics, getHighPageViewsLowFormViewsMetrics, getHighPageViewsLowFormCtrMetrics } from './formcalc.js';