Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3,468 changes: 2,487 additions & 981 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@
"@adobe/helix-status": "10.1.5",
"@adobe/helix-universal": "5.4.0",
"@adobe/helix-universal-logger": "3.0.28",
"@adobe/spacecat-shared-data-access": "3.31.1",
"@adobe/spacecat-shared-data-access": "https://gist.github.com/tkotthakota-adobe/a882c8a25cbc82cb134ed75e831ef6bd/raw/89cfa44262f3d35bd008abf8240d05605d18cb71/adobe-spacecat-shared-data-access-3.31.0.tgz",
"@adobe/spacecat-shared-data-access-v2": "npm:@adobe/spacecat-shared-data-access@2.109.0",
"@adobe/spacecat-shared-google-client": "1.5.7",
"@adobe/spacecat-shared-gpt-client": "1.6.20",
Expand All @@ -85,7 +85,7 @@
"@adobe/spacecat-shared-rum-api-client": "2.40.10",
"@adobe/spacecat-shared-scrape-client": "2.5.4",
"@adobe/spacecat-shared-slack-client": "1.6.4",
"@adobe/spacecat-shared-utils": "1.106.0",
"@adobe/spacecat-shared-utils": "https://gist.github.com/tkotthakota-adobe/fe9c5d8edb227d23339db99d0293bc81/raw/9483f68938ed65df6d3dab50cf55c4d5c4cff113/adobe-spacecat-shared-utils-1.106.0.tgz",
"@aws-sdk/client-lambda": "3.1014.0",
"@aws-sdk/client-sqs": "3.1014.0",
"@aws-sdk/client-s3": "3.1014.0",
Expand Down
67 changes: 0 additions & 67 deletions src/tasks/opportunity-status-processor/audit-opportunity-map.js

This file was deleted.

161 changes: 111 additions & 50 deletions src/tasks/opportunity-status-processor/handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,16 @@ import { ok } from '@adobe/spacecat-shared-http-utils';
import RUMAPIClient from '@adobe/spacecat-shared-rum-api-client';
import GoogleClient from '@adobe/spacecat-shared-google-client';
import { ScrapeClient } from '@adobe/spacecat-shared-scrape-client';
import { resolveCanonicalUrl } from '@adobe/spacecat-shared-utils';
import {
resolveCanonicalUrl,
getAuditsForOpportunity,
getOpportunityTitle,
OPPORTUNITY_DEPENDENCY_MAP,
getOpportunitiesForAudit,
} from '@adobe/spacecat-shared-utils';
import { getAuditStatus } from '../../utils/cloudwatch-utils.js';
import { checkAndAlertBotProtection } from '../../utils/bot-detection.js';
import { say } from '../../utils/slack-utils.js';
import { getOpportunitiesForAudit } from './audit-opportunity-map.js';
import { OPPORTUNITY_DEPENDENCY_MAP } from './opportunity-dependency-map.js';

const TASK_TYPE = 'opportunity-status-processor';

Expand Down Expand Up @@ -94,33 +98,6 @@ async function isGSCConfigured(siteUrl, context) {
}
}

/**
* Gets the opportunity title from the opportunity type
* @param {string} opportunityType - The opportunity type
* @returns {string} The opportunity title
*/
function getOpportunityTitle(opportunityType) {
const opportunityTitles = {
cwv: 'Core Web Vitals',
'meta-tags': 'SEO Meta Tags',
'broken-backlinks': 'Broken Backlinks',
'broken-internal-links': 'Broken Internal Links',
'alt-text': 'Alt Text',
sitemap: 'Sitemap',
};

// Check if the opportunity type exists in our map
if (opportunityTitles[opportunityType]) {
return opportunityTitles[opportunityType];
}

// Convert kebab-case to Title Case (e.g., "first-second" -> "First Second")
return opportunityType
.split('-')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
}

/**
* Filters scrape jobs to only include those created after onboardStartTime
* This ensures we only check jobs from the CURRENT onboarding session,
Expand Down Expand Up @@ -241,11 +218,51 @@ async function isScrapingAvailable(baseUrl, context, onboardStartTime) {
}

/**
* Checks scrape results for bot protection blocking
* @param {Array} scrapeResults - Array of scrape URL results
* @param {object} context - The context object with log
* @returns {object|null} Bot protection details if detected, null otherwise
* Checks which audit types have completed since onboardStartTime by querying the database.
* An audit is considered completed if a record exists with auditedAt >= onboardStartTime.
* Falls back conservatively (all pending) if the DB query fails.
*
* @param {string} siteId - The site ID
* @param {Array<string>} auditTypes - Audit types expected for this onboard session
* @param {number} onboardStartTime - Onboarding start timestamp in ms
* @param {object} dataAccess - Data access object
* @param {object} log - Logger
* @returns {Promise<{pendingAuditTypes: Array<string>, completedAuditTypes: Array<string>}>}
*/
async function checkAuditCompletionFromDB(siteId, auditTypes, onboardStartTime, dataAccess, log) {
const pendingAuditTypes = [];
const completedAuditTypes = [];
try {
const { Audit } = dataAccess;
const latestAudits = await Audit.allLatestForSite(siteId);
const auditsByType = {};
if (latestAudits) {
for (const audit of latestAudits) {
auditsByType[audit.getAuditType()] = audit;
}
}
for (const auditType of auditTypes) {
const audit = auditsByType[auditType];
if (!audit) {
pendingAuditTypes.push(auditType);
} else {
const auditedAt = new Date(audit.getAuditedAt()).getTime();
if (onboardStartTime && auditedAt < onboardStartTime) {
// Record exists but predates this onboard session — treat as pending
pendingAuditTypes.push(auditType);
} else {
completedAuditTypes.push(auditType);
}
}
}
} catch (error) {
log.warn(`Could not check audit completion from DB for site ${siteId}: ${error.message}`);
// Conservative fallback: mark all as pending so disclaimer is always shown on error
pendingAuditTypes.push(...auditTypes.filter((t) => !completedAuditTypes.includes(t)));
}
return { pendingAuditTypes, completedAuditTypes };
}

/**
* Analyzes missing opportunities and determines the root cause
* @param {Array<string>} missingOpportunities - Array of missing opportunity types
Expand Down Expand Up @@ -558,6 +575,15 @@ export async function runOpportunityStatusProcessor(message, context) {
statusMessages.push(`GSC ${gscStatus}`);
statusMessages.push(`Scraping ${scrapingStatus}`);

// Determine which audits are still pending so opportunity statuses can reflect
// in-progress state (⏳) rather than showing stale data as ✅/❌.
// Only meaningful when we have an onboardStartTime anchor to compare against.
let pendingAuditTypes = [];
if (auditTypes && auditTypes.length > 0 && onboardStartTime) {
// eslint-disable-next-line max-len
({ pendingAuditTypes } = await checkAuditCompletionFromDB(siteId, auditTypes, onboardStartTime, dataAccess, log));
}

// Process opportunities by type to avoid duplicates
// Only process opportunities that are expected based on the profile's audit types
const processedTypes = new Set();
Expand Down Expand Up @@ -586,23 +612,28 @@ export async function runOpportunityStatusProcessor(message, context) {
}
processedTypes.add(opportunityType);

// eslint-disable-next-line no-await-in-loop
const suggestions = await opportunity.getSuggestions();

const opportunityTitle = getOpportunityTitle(opportunityType);
const hasSuggestions = suggestions && suggestions.length > 0;
const status = hasSuggestions ? ':white_check_mark:' : ':x:';
statusMessages.push(`${opportunityTitle} ${status}`);

// Track failed opportunities (no suggestions)
if (!hasSuggestions) {
// Use informational message for opportunities with zero suggestions
const reason = 'Audit executed successfully, opportunity added, but found no suggestions';

failedOpportunities.push({
title: opportunityTitle,
reason,
});

// If the source audit is still running, show ⏳ instead of stale ✅/❌
const sourceAuditIsPending = getAuditsForOpportunity(opportunityType)
.some((auditType) => pendingAuditTypes.includes(auditType));

if (sourceAuditIsPending) {
statusMessages.push(`${opportunityTitle} :hourglass_flowing_sand:`);
} else {
// eslint-disable-next-line no-await-in-loop
const suggestions = await opportunity.getSuggestions();
const hasSuggestions = suggestions && suggestions.length > 0;
const status = hasSuggestions ? ':white_check_mark:' : ':x:';
statusMessages.push(`${opportunityTitle} ${status}`);

// Track failed opportunities (no suggestions)
if (!hasSuggestions) {
failedOpportunities.push({
title: opportunityTitle,
reason: 'Audit executed successfully, opportunity added, but found no suggestions',
});
}
}
}

Expand Down Expand Up @@ -680,6 +711,36 @@ export async function runOpportunityStatusProcessor(message, context) {
} else {
await say(env, log, slackContext, 'No audit errors found');
}

// Audit completion disclaimer — reuse pendingAuditTypes already computed above.
// Only list audit types that have known opportunity mappings; infrastructure audits
// (auto-suggest, auto-fix, scrape, etc.) are not shown since they don't affect
// the displayed opportunity statuses.
if (auditTypes.length > 0) {
const isRecheck = taskContext?.isRecheck === true;
const relevantPendingTypes = pendingAuditTypes.filter(
(t) => getOpportunitiesForAudit(t).length > 0,
);
if (relevantPendingTypes.length > 0) {
const pendingList = relevantPendingTypes.map(getOpportunityTitle).join(', ');
await say(
env,
log,
slackContext,
`:warning: *Heads-up:* The following audit${relevantPendingTypes.length > 1 ? 's' : ''} `
+ `may still be in progress: *${pendingList}*.\n`
+ 'The statuses above reflect data available at this moment and may be incomplete. '
+ `Run \`onboard status ${siteUrl}\` to re-check once all audits have completed.`,
);
} else if (isRecheck) {
await say(
env,
log,
slackContext,
':white_check_mark: All audits have completed. The statuses above are up to date.',
);
}
}
}

log.info(`Processed ${opportunities.length} opportunities for site ${siteId}`);
Expand Down

This file was deleted.

Loading
Loading