@adobe/spacecat-shared-utils 1.106.0 → 1.107.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,15 @@
1
+ ## [@adobe/spacecat-shared-utils-v1.107.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-utils-v1.106.1...@adobe/spacecat-shared-utils-v1.107.0) (2026-03-30)
2
+
3
+ ### Features
4
+
5
+ * add onboard config and opportunity maps ([#1464](https://github.com/adobe/spacecat-shared/issues/1464)) ([9bd59f5](https://github.com/adobe/spacecat-shared/commit/9bd59f5e2b05f3fd29ea7568daf0e369bd4d2535))
6
+
7
+ ## [@adobe/spacecat-shared-utils-v1.106.1](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-utils-v1.106.0...@adobe/spacecat-shared-utils-v1.106.1) (2026-03-28)
8
+
9
+ ### Bug Fixes
10
+
11
+ * **deps:** update external fixes ([#1477](https://github.com/adobe/spacecat-shared/issues/1477)) ([67bdd1a](https://github.com/adobe/spacecat-shared/commit/67bdd1a2c497bed088bc1e54ae22e60c171308d1))
12
+
1
13
  ## [@adobe/spacecat-shared-utils-v1.106.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-utils-v1.105.1...@adobe/spacecat-shared-utils-v1.106.0) (2026-03-23)
2
14
 
3
15
  ### Features
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/spacecat-shared-utils",
3
- "version": "1.106.0",
3
+ "version": "1.107.0",
4
4
  "description": "Shared modules of the Spacecat Services - utils",
5
5
  "type": "module",
6
6
  "exports": {
@@ -53,8 +53,8 @@
53
53
  },
54
54
  "dependencies": {
55
55
  "@adobe/fetch": "4.2.3",
56
- "@aws-sdk/client-s3": "3.1014.0",
57
- "@aws-sdk/client-sqs": "3.1014.0",
56
+ "@aws-sdk/client-s3": "3.1019.0",
57
+ "@aws-sdk/client-sqs": "3.1019.0",
58
58
  "@json2csv/plainjs": "7.0.6",
59
59
  "aws-xray-sdk": "3.12.0",
60
60
  "cheerio": "1.2.0",
package/src/index.js CHANGED
@@ -141,3 +141,25 @@ export {
141
141
  getTokenTypeForOpportunity,
142
142
  getCurrentCycle,
143
143
  } from './token-grant-config.js';
144
+
145
+ export {
146
+ AUDIT_OPPORTUNITY_MAP,
147
+ getOpportunitiesForAudit,
148
+ getAuditsForOpportunity,
149
+ getAllOpportunityTypes,
150
+ getAllAuditTypes,
151
+ } from './opportunity/audit-mapping.js';
152
+
153
+ export {
154
+ DEPENDENCY_SOURCES,
155
+ OPPORTUNITY_DEPENDENCY_MAP,
156
+ getDependenciesForOpportunity,
157
+ getOpportunitiesForSource,
158
+ } from './opportunity/dependency-mapping.js';
159
+
160
+ export {
161
+ OPPORTUNITY_TITLES,
162
+ getOpportunityTitle,
163
+ } from './opportunity/opportunity-titles.js';
164
+
165
+ export { computeAuditCompletion } from './opportunity/audit-completion.js';
@@ -0,0 +1,65 @@
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
+ /**
14
+ * Computes which audit types are pending vs completed from already-fetched audit records.
15
+ *
16
+ * The onboardStartTime anchor is the same value written to onboardConfig.lastStartTime
17
+ * in the site config by the api-service and passed via SQS/step function message to the
18
+ * task-processor. Callers supply it from whichever source is available to them.
19
+ *
20
+ * An audit is pending if:
21
+ * - No DB record exists for it yet, OR
22
+ * - Its auditedAt timestamp predates or ties with onboardStartTime (<=).
23
+ * An exact match indicates an in-flight audit captured before the run completed.
24
+ *
25
+ * If onboardStartTime is absent (site onboarded before this feature), any existing
26
+ * record is treated as completed — only a missing record counts as pending.
27
+ *
28
+ * Pure function — no DB calls.
29
+ *
30
+ * @param {string[]} auditTypes - Audit types to check
31
+ * @param {number|undefined} onboardStartTime - Onboard start timestamp in ms
32
+ * @param {Array} latestAudits - Already-fetched audit records (getAuditType, getAuditedAt)
33
+ * @returns {{ pendingAuditTypes: string[], completedAuditTypes: string[] }}
34
+ */
35
+ export function computeAuditCompletion(auditTypes, onboardStartTime, latestAudits) {
36
+ const pendingAuditTypes = [];
37
+ const completedAuditTypes = [];
38
+ const auditsByType = {};
39
+
40
+ if (latestAudits) {
41
+ for (const audit of latestAudits) {
42
+ auditsByType[audit.getAuditType()] = audit;
43
+ }
44
+ }
45
+
46
+ for (const auditType of auditTypes) {
47
+ const audit = auditsByType[auditType];
48
+ if (!audit) {
49
+ pendingAuditTypes.push(auditType);
50
+ } else if (onboardStartTime) {
51
+ const auditedAt = new Date(audit.getAuditedAt()).getTime();
52
+ // NaN or timestamp predating/matching trigger → pending
53
+ if (Number.isNaN(auditedAt) || auditedAt <= onboardStartTime) {
54
+ pendingAuditTypes.push(auditType);
55
+ } else {
56
+ completedAuditTypes.push(auditType);
57
+ }
58
+ } else {
59
+ // No onboard start time — treat existing record as completed.
60
+ completedAuditTypes.push(auditType);
61
+ }
62
+ }
63
+
64
+ return { pendingAuditTypes, completedAuditTypes };
65
+ }
@@ -0,0 +1,120 @@
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
+ /**
14
+ * Maps each audit type to the opportunity types it can generate.
15
+ * Derived from the HANDLERS registry in spacecat-audit-worker/src/index.js.
16
+ *
17
+ * Audits that are data-collection only (e.g. apex, paid, lhs-mobile, cdn-logs-*)
18
+ * and produce no opportunities are intentionally omitted.
19
+ *
20
+ * Key: audit type string
21
+ * Value: array of opportunity type strings produced by that audit
22
+ */
23
+ export const AUDIT_OPPORTUNITY_MAP = {
24
+ // Core SEO
25
+ 'broken-backlinks': ['broken-backlinks'],
26
+ 'broken-internal-links': ['broken-internal-links'],
27
+ canonical: ['canonical'],
28
+ hreflang: ['hreflang'],
29
+ 'meta-tags': ['meta-tags'],
30
+ sitemap: ['sitemap'],
31
+ 'sitemap-product-coverage': ['sitemap-product-coverage'],
32
+ 'structured-data': ['structured-data'],
33
+ 'redirect-chains': ['redirect-chains'],
34
+
35
+ // Performance
36
+ cwv: ['cwv'],
37
+ prerender: ['prerender'],
38
+
39
+ // Accessibility & content
40
+ accessibility: ['accessibility'],
41
+ 'alt-text': ['alt-text'],
42
+ headings: ['headings'],
43
+ 'no-cta-above-the-fold': ['no-cta-above-the-fold'],
44
+ readability: ['readability'],
45
+
46
+ // Forms — one audit, multiple opportunity types
47
+ 'forms-opportunities': [
48
+ 'high-form-views-low-conversions',
49
+ 'high-page-views-low-form-nav',
50
+ 'high-page-views-low-form-views',
51
+ 'form-accessibility',
52
+ ],
53
+
54
+ // Experimentation
55
+ 'experimentation-opportunities': ['high-organic-low-ctr'],
56
+
57
+ // Commerce
58
+ 'product-metatags': ['product-metatags'],
59
+
60
+ // Security
61
+ 'security-csp': ['security-csp'],
62
+ 'security-vulnerabilities': ['security-vulnerabilities'],
63
+ 'security-permissions': ['security-permissions'],
64
+ 'security-permissions-redundant': ['security-permissions-redundant'],
65
+
66
+ // LLMO / content quality
67
+ 'llm-blocked': ['llm-blocked'],
68
+ 'llm-error-pages': ['llm-error-pages'],
69
+ faqs: ['faqs'],
70
+ 'related-urls': ['related-urls'],
71
+ toc: ['toc'],
72
+
73
+ // Experimentation (ESS signals)
74
+ 'experimentation-ess-daily': ['experimentation-ess-daily'],
75
+ 'experimentation-ess-monthly': ['experimentation-ess-monthly'],
76
+
77
+ // Offsite / brand analysis
78
+ 'cited-analysis': ['cited-analysis'],
79
+ 'wikipedia-analysis': ['wikipedia-analysis'],
80
+ 'reddit-analysis': ['reddit-analysis'],
81
+ 'youtube-analysis': ['youtube-analysis'],
82
+ };
83
+
84
+ // ─── Query helpers ───────────────────────────────────────────────────────────
85
+
86
+ /**
87
+ * Returns all opportunity types that a given audit type can generate.
88
+ * @param {string} auditType
89
+ * @returns {string[]}
90
+ */
91
+ export function getOpportunitiesForAudit(auditType) {
92
+ return AUDIT_OPPORTUNITY_MAP[auditType] || [];
93
+ }
94
+
95
+ /**
96
+ * Returns all audit types that produce a given opportunity type.
97
+ * @param {string} opportunityType
98
+ * @returns {string[]}
99
+ */
100
+ export function getAuditsForOpportunity(opportunityType) {
101
+ return Object.entries(AUDIT_OPPORTUNITY_MAP)
102
+ .filter(([, opps]) => opps.includes(opportunityType))
103
+ .map(([auditType]) => auditType);
104
+ }
105
+
106
+ /**
107
+ * Returns all unique opportunity types across all audits.
108
+ * @returns {string[]}
109
+ */
110
+ export function getAllOpportunityTypes() {
111
+ return [...new Set(Object.values(AUDIT_OPPORTUNITY_MAP).flat())];
112
+ }
113
+
114
+ /**
115
+ * Returns all audit types defined in the map.
116
+ * @returns {string[]}
117
+ */
118
+ export function getAllAuditTypes() {
119
+ return Object.keys(AUDIT_OPPORTUNITY_MAP);
120
+ }
@@ -0,0 +1,122 @@
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
+ /**
14
+ * Dependency sources used by opportunities.
15
+ * RUM — Real User Monitoring (Adobe RUM / helix-rum-js)
16
+ * AHREFSImport — Ahrefs import job (organic traffic, backlinks, keywords)
17
+ * GSC — Google Search Console import
18
+ * scraping — Page HTML scraping (spacecat-scraper / spacecat-content-scraper)
19
+ * ExternalAPI — Third-party API call at audit run-time (Wikipedia, Reddit, YouTube)
20
+ * CrUX — Chrome User Experience Report (field data from Google)
21
+ * PSI — PageSpeed Insights (lab data via Lighthouse API)
22
+ */
23
+ export const DEPENDENCY_SOURCES = /** @type {const} */ ({
24
+ RUM: 'RUM',
25
+ AHREFS_IMPORT: 'AHREFSImport',
26
+ GSC: 'GSC',
27
+ SCRAPING: 'scraping',
28
+ EXTERNAL_API: 'ExternalAPI',
29
+ CRUX: 'CrUX',
30
+ PSI: 'PSI',
31
+ });
32
+
33
+ /**
34
+ * Maps each opportunity type to the data sources it requires to be populated.
35
+ * A missing or empty import for any listed source means the opportunity
36
+ * cannot be generated or will have incomplete data.
37
+ *
38
+ * Key: opportunity type string
39
+ * Value: array of DEPENDENCY_SOURCES values
40
+ */
41
+ export const OPPORTUNITY_DEPENDENCY_MAP = {
42
+ // Performance
43
+ cwv: [DEPENDENCY_SOURCES.RUM, DEPENDENCY_SOURCES.CRUX, DEPENDENCY_SOURCES.PSI],
44
+ prerender: [DEPENDENCY_SOURCES.SCRAPING],
45
+
46
+ // SEO — traffic-driven
47
+ 'high-organic-low-ctr': [DEPENDENCY_SOURCES.RUM, DEPENDENCY_SOURCES.GSC],
48
+
49
+ // SEO — link audits
50
+ 'broken-backlinks': [DEPENDENCY_SOURCES.AHREFS_IMPORT, DEPENDENCY_SOURCES.SCRAPING],
51
+ 'broken-internal-links': [DEPENDENCY_SOURCES.RUM, DEPENDENCY_SOURCES.SCRAPING],
52
+
53
+ // SEO — on-page / content
54
+ canonical: [DEPENDENCY_SOURCES.SCRAPING],
55
+ hreflang: [DEPENDENCY_SOURCES.SCRAPING],
56
+ 'meta-tags': [DEPENDENCY_SOURCES.AHREFS_IMPORT, DEPENDENCY_SOURCES.SCRAPING],
57
+ sitemap: [DEPENDENCY_SOURCES.SCRAPING],
58
+ 'sitemap-product-coverage': [DEPENDENCY_SOURCES.AHREFS_IMPORT, DEPENDENCY_SOURCES.SCRAPING],
59
+ 'structured-data': [DEPENDENCY_SOURCES.SCRAPING],
60
+ 'redirect-chains': [DEPENDENCY_SOURCES.SCRAPING],
61
+ headings: [DEPENDENCY_SOURCES.SCRAPING],
62
+
63
+ // Accessibility & content
64
+ accessibility: [DEPENDENCY_SOURCES.SCRAPING],
65
+ 'alt-text': [DEPENDENCY_SOURCES.AHREFS_IMPORT, DEPENDENCY_SOURCES.SCRAPING],
66
+ 'no-cta-above-the-fold': [DEPENDENCY_SOURCES.RUM, DEPENDENCY_SOURCES.SCRAPING],
67
+ readability: [DEPENDENCY_SOURCES.SCRAPING],
68
+
69
+ // Forms
70
+ 'high-form-views-low-conversions': [DEPENDENCY_SOURCES.RUM],
71
+ 'high-page-views-low-form-nav': [DEPENDENCY_SOURCES.RUM],
72
+ 'high-page-views-low-form-views': [DEPENDENCY_SOURCES.RUM],
73
+ 'form-accessibility': [DEPENDENCY_SOURCES.RUM, DEPENDENCY_SOURCES.SCRAPING],
74
+
75
+ // Commerce
76
+ 'product-metatags': [DEPENDENCY_SOURCES.SCRAPING],
77
+
78
+ // Security
79
+ 'security-csp': [DEPENDENCY_SOURCES.SCRAPING],
80
+ 'security-vulnerabilities': [DEPENDENCY_SOURCES.SCRAPING],
81
+ 'security-permissions': [DEPENDENCY_SOURCES.SCRAPING],
82
+ 'security-permissions-redundant': [DEPENDENCY_SOURCES.SCRAPING],
83
+
84
+ // LLMO / content quality
85
+ 'llm-blocked': [DEPENDENCY_SOURCES.SCRAPING],
86
+ 'llm-error-pages': [DEPENDENCY_SOURCES.SCRAPING],
87
+ faqs: [DEPENDENCY_SOURCES.SCRAPING],
88
+ 'related-urls': [DEPENDENCY_SOURCES.AHREFS_IMPORT],
89
+ toc: [DEPENDENCY_SOURCES.SCRAPING],
90
+
91
+ // Experimentation (ESS signals — organic traffic + RUM)
92
+ 'experimentation-ess-daily': [DEPENDENCY_SOURCES.RUM, DEPENDENCY_SOURCES.GSC],
93
+ 'experimentation-ess-monthly': [DEPENDENCY_SOURCES.RUM, DEPENDENCY_SOURCES.GSC],
94
+
95
+ // Offsite / brand analysis
96
+ 'cited-analysis': [DEPENDENCY_SOURCES.EXTERNAL_API],
97
+ 'wikipedia-analysis': [DEPENDENCY_SOURCES.EXTERNAL_API],
98
+ 'reddit-analysis': [DEPENDENCY_SOURCES.EXTERNAL_API],
99
+ 'youtube-analysis': [DEPENDENCY_SOURCES.EXTERNAL_API],
100
+ };
101
+
102
+ // ─── Query helpers ───────────────────────────────────────────────────────────
103
+
104
+ /**
105
+ * Returns the required data sources for a given opportunity type.
106
+ * @param {string} opportunityType
107
+ * @returns {string[]}
108
+ */
109
+ export function getDependenciesForOpportunity(opportunityType) {
110
+ return OPPORTUNITY_DEPENDENCY_MAP[opportunityType] || [];
111
+ }
112
+
113
+ /**
114
+ * Returns all opportunity types that depend on a given data source.
115
+ * @param {string} source - One of DEPENDENCY_SOURCES values
116
+ * @returns {string[]}
117
+ */
118
+ export function getOpportunitiesForSource(source) {
119
+ return Object.entries(OPPORTUNITY_DEPENDENCY_MAP)
120
+ .filter(([, deps]) => deps.includes(source))
121
+ .map(([opportunityType]) => opportunityType);
122
+ }
@@ -0,0 +1,90 @@
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
+ /**
14
+ * Human-readable display titles for each opportunity type.
15
+ *
16
+ * Key: opportunity type string
17
+ * Value: display title string
18
+ */
19
+ export const OPPORTUNITY_TITLES = {
20
+ // Performance
21
+ cwv: 'Core Web Vitals',
22
+ prerender: 'Prerender',
23
+
24
+ // Core SEO
25
+ 'broken-backlinks': 'Broken Backlinks',
26
+ 'broken-internal-links': 'Broken Internal Links',
27
+ canonical: 'Canonical',
28
+ hreflang: 'Hreflang',
29
+ 'meta-tags': 'SEO Meta Tags',
30
+ sitemap: 'Sitemap',
31
+ 'sitemap-product-coverage': 'Sitemap Product Coverage',
32
+ 'structured-data': 'Structured Data',
33
+ 'redirect-chains': 'Redirect Chains',
34
+
35
+ // Accessibility & content
36
+ accessibility: 'Accessibility',
37
+ 'alt-text': 'Alt Text',
38
+ headings: 'Headings',
39
+ 'no-cta-above-the-fold': 'No CTA Above the Fold',
40
+ readability: 'Readability',
41
+
42
+ // Forms
43
+ 'high-form-views-low-conversions': 'High Form Views Low Conversions',
44
+ 'high-page-views-low-form-nav': 'High Page Views Low Form Navigation',
45
+ 'high-page-views-low-form-views': 'High Page Views Low Form Views',
46
+ 'form-accessibility': 'Form Accessibility',
47
+
48
+ // Experimentation
49
+ 'high-organic-low-ctr': 'High Organic Low CTR',
50
+
51
+ // Commerce
52
+ 'product-metatags': 'Product Meta Tags',
53
+
54
+ // Security
55
+ 'security-csp': 'Security CSP',
56
+ 'security-vulnerabilities': 'Security Vulnerabilities',
57
+ 'security-permissions': 'Security Permissions',
58
+ 'security-permissions-redundant': 'Security Permissions Redundant',
59
+
60
+ // LLMO / content quality
61
+ 'llm-blocked': 'LLM Blocked',
62
+ 'llm-error-pages': 'LLM Error Pages',
63
+ faqs: 'FAQs',
64
+ 'related-urls': 'Related URLs',
65
+ toc: 'Table of Contents',
66
+
67
+ // Experimentation (ESS signals)
68
+ 'experimentation-ess-daily': 'Experimentation ESS Daily',
69
+ 'experimentation-ess-monthly': 'Experimentation ESS Monthly',
70
+
71
+ // Offsite / brand analysis
72
+ 'cited-analysis': 'Cited Analysis',
73
+ 'wikipedia-analysis': 'Wikipedia Analysis',
74
+ 'reddit-analysis': 'Reddit Analysis',
75
+ 'youtube-analysis': 'YouTube Analysis',
76
+ };
77
+
78
+ /**
79
+ * Returns the human-readable title for a given opportunity type.
80
+ * Falls back to converting kebab-case to Title Case for unknown types.
81
+ * @param {string} opportunityType
82
+ * @returns {string}
83
+ */
84
+ export function getOpportunityTitle(opportunityType) {
85
+ return OPPORTUNITY_TITLES[opportunityType]
86
+ ?? opportunityType
87
+ .split('-')
88
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
89
+ .join(' ');
90
+ }