Skip to content
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
75 commits
Select commit Hold shift + click to select a range
aedb6d4
cwv suggestions processor
tkotthakota-adobe Aug 20, 2025
9e7e83a
address some review comments
tkotthakota-adobe Aug 23, 2025
6e9e31e
remove Suggestion object as parameters
tkotthakota-adobe Aug 23, 2025
fda3226
refactor processCWVOpportunity method
tkotthakota-adobe Aug 23, 2025
f028c51
add suggestionUpdated to return statement
tkotthakota-adobe Aug 23, 2025
b8854bb
refactor to move aem best practices to a file
tkotthakota-adobe Aug 23, 2025
a0985f5
Merge branch 'main' of github.com:adobe/spacecat-task-processor into …
tkotthakota-adobe Aug 25, 2025
15d9c9d
Merge branch 'main' of github.com:adobe/spacecat-task-processor into …
tkotthakota-adobe Aug 25, 2025
ed3511c
simplify path logic
tkotthakota-adobe Aug 25, 2025
b15c01d
review comments
tkotthakota-adobe Aug 25, 2025
4ae7121
logs
tkotthakota-adobe Aug 25, 2025
01c5d29
use relative path logic
tkotthakota-adobe Aug 26, 2025
9581f18
tests
tkotthakota-adobe Aug 26, 2025
e396173
path fix
tkotthakota-adobe Aug 26, 2025
41fac8d
path
tkotthakota-adobe Aug 26, 2025
fe5e974
debug
tkotthakota-adobe Aug 26, 2025
d62dc1a
path fix
tkotthakota-adobe Aug 26, 2025
962fbc1
use relative path
tkotthakota-adobe Aug 26, 2025
2bb858e
debug
tkotthakota-adobe Aug 26, 2025
1b05238
debug
tkotthakota-adobe Aug 26, 2025
ad78fec
move fodler
tkotthakota-adobe Aug 26, 2025
de80db4
fix
tkotthakota-adobe Aug 26, 2025
e426a6f
fix
tkotthakota-adobe Aug 26, 2025
0dd14f7
fix
tkotthakota-adobe Aug 26, 2025
f4ddae1
fix
tkotthakota-adobe Aug 26, 2025
d9ef7b4
move
tkotthakota-adobe Aug 26, 2025
f233726
use suggestions with in js file
tkotthakota-adobe Aug 26, 2025
ef44bb6
adjust code
tkotthakota-adobe Aug 26, 2025
d89af16
test using file
tkotthakota-adobe Aug 26, 2025
4b99c75
update package
tkotthakota-adobe Aug 26, 2025
8f67623
add extra slack message
tkotthakota-adobe Aug 26, 2025
68ab01a
update package
tkotthakota-adobe Aug 26, 2025
5901910
adjust condition
tkotthakota-adobe Aug 26, 2025
8b3d247
remove line
tkotthakota-adobe Aug 26, 2025
555ef98
merge main
tkotthakota-adobe Sep 22, 2025
29f3eed
refactor to use seperate files for demo cwv suggestions
tkotthakota-adobe Sep 23, 2025
681f0ea
Merge branch 'main' of github.com:adobe/spacecat-task-processor into …
tkotthakota-adobe Sep 23, 2025
09c56bb
Merge branch 'main' of github.com:adobe/spacecat-task-processor into …
tkotthakota-adobe Sep 23, 2025
a2cf0d9
path fix
tkotthakota-adobe Sep 23, 2025
4a0d585
move static under src
tkotthakota-adobe Sep 23, 2025
feee9fe
refactor
tkotthakota-adobe Sep 23, 2025
55ad2f6
update slack message
tkotthakota-adobe Sep 23, 2025
e1c9220
add debug logs
tkotthakota-adobe Sep 24, 2025
a41fd94
fix path
tkotthakota-adobe Sep 24, 2025
60970b8
change file path
tkotthakota-adobe Sep 24, 2025
f1f06e1
remove unsed files
tkotthakota-adobe Sep 24, 2025
71694cb
adjust path
tkotthakota-adobe Sep 24, 2025
19e853a
add debugs
tkotthakota-adobe Sep 24, 2025
02983f6
rename
tkotthakota-adobe Sep 24, 2025
2d6bd16
logs
tkotthakota-adobe Sep 24, 2025
21609e2
fix
tkotthakota-adobe Sep 24, 2025
9590845
Merge branch 'main' of github.com:adobe/spacecat-task-processor into …
tkotthakota-adobe Sep 24, 2025
1baa7b6
fix
tkotthakota-adobe Sep 24, 2025
6f9625d
Merge branch 'main' of github.com:adobe/spacecat-task-processor into …
tkotthakota-adobe Sep 24, 2025
b8fc316
refactor
tkotthakota-adobe Sep 24, 2025
596c996
refactor readStaticFile
tkotthakota-adobe Sep 24, 2025
71dd2bb
support txt file deployment
tkotthakota-adobe Sep 24, 2025
d701a69
refac
tkotthakota-adobe Sep 24, 2025
812565c
fix
tkotthakota-adobe Sep 24, 2025
927ab55
use load file approach
tkotthakota-adobe Sep 24, 2025
aa6a840
add slack message
tkotthakota-adobe Sep 24, 2025
c2e4af3
slack
tkotthakota-adobe Sep 24, 2025
2ff7de4
debug
tkotthakota-adobe Sep 24, 2025
8ed720c
refactor to use static folder for files
tkotthakota-adobe Sep 24, 2025
e4cf7fb
add generic suggestions to cwv opportunity
tkotthakota-adobe Sep 24, 2025
9dbf1ec
check for non generic suggestions
tkotthakota-adobe Sep 24, 2025
59c3f56
add debugs
tkotthakota-adobe Sep 24, 2025
01972e6
fix
tkotthakota-adobe Sep 24, 2025
3f4d4e4
rafctor
tkotthakota-adobe Sep 24, 2025
5a5f974
refac
tkotthakota-adobe Sep 24, 2025
23045fe
fix
tkotthakota-adobe Sep 24, 2025
80981ca
debug
tkotthakota-adobe Sep 24, 2025
6cb54e9
debug
tkotthakota-adobe Sep 24, 2025
5379e2d
fix
tkotthakota-adobe Sep 24, 2025
86e4db7
refactor
tkotthakota-adobe Sep 24, 2025
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
2 changes: 2 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@ import { imsClientWrapper } from '@adobe/spacecat-shared-ims-client';
import { runOpportunityStatusProcessor as opportunityStatusProcessor } from './tasks/opportunity-status-processor/handler.js';
import { runDisableImportAuditProcessor as disableImportAuditProcessor } from './tasks/disable-import-audit-processor/handler.js';
import { runDemoUrlProcessor as demoUrlProcessor } from './tasks/demo-url-processor/handler.js';
import { runCwvDemoSuggestionsProcessor as cwvDemoSuggestionsProcessor } from './tasks/cwv-demo-suggestions-processor/handler.js';

const HANDLERS = {
'opportunity-status-processor': opportunityStatusProcessor,
'disable-import-audit-processor': disableImportAuditProcessor,
'demo-url-processor': demoUrlProcessor,
'cwv-demo-suggestions-processor': cwvDemoSuggestionsProcessor,
dummy: (message) => ok(message),
};

Expand Down
303 changes: 303 additions & 0 deletions src/tasks/cwv-demo-suggestions-processor/handler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,303 @@
/*
* Copyright 2025 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/

import { isNonEmptyArray } from '@adobe/spacecat-shared-utils';
import { readFileSync } from 'fs';
import path from 'path';
import { say } from '../../utils/slack-utils.js';

const TASK_TYPE = 'cwv-demo-suggestions-processor';
const LCP = 'lcp';
const CLS = 'cls';
const INP = 'inp';
const DEMO = 'demo';
const MAX_CWV_DEMO_SUGGESTIONS = 2;
const CWV_SUGGESTIONS_FILE_PATH = path.resolve(process.cwd(), 'static', 'aem-best-practices.json');

/**
* CWV thresholds for determining if metrics have issues
*/
const CWV_THRESHOLDS = {
lcp: 2500, // 2.5 seconds
cls: 0.1, // 0.1
inp: 200, // 200 milliseconds
};

/**
* Gets metric issues based on CWV thresholds
* @param {object} metrics - The metrics object
* @returns {Array} Array of issue types
*/
function getMetricIssues(metrics) {
const issues = [];

if (metrics?.lcp > CWV_THRESHOLDS[LCP]) {
issues.push(LCP);
}

if (metrics?.cls > CWV_THRESHOLDS[CLS]) {
issues.push(CLS);
}

if (metrics?.inp > CWV_THRESHOLDS[INP]) {
issues.push(INP);
}

return issues;
}

/**
* Checks if a suggestion has existing issues
* @param {object} suggestion - The suggestion object
* @returns {boolean} True if suggestion has existing issues
*/
function hasExistingIssues(suggestion) {
const data = suggestion.getData();
return data.issues && isNonEmptyArray(data.issues);
}

/**
* Gets a random suggestion from the available suggestions for a given issue type
* @param {string} issueType - The type of issue (lcp, cls, inp)
* @param {object} cwvReferenceSuggestions - The CWV reference suggestions object
* @returns {string|null} A random suggestion or null if none available
*/
function getRandomSuggestion(issueType, cwvReferenceSuggestions) {
const suggestions = cwvReferenceSuggestions[issueType];
if (!isNonEmptyArray(suggestions)) {
return null;
}

const randomIndex = Math.floor(Math.random() * suggestions.length);
return suggestions[randomIndex];
}

/**
* Updates a suggestion with generic CWV issues
* @param {object} suggestion - The suggestion object
* @param {Array} metricIssues - Array of metric issue types
* @param {object} logger - The logger object
* @param {object} env - The environment object
* @param {object} slackContext - The Slack context object
* @returns {number} Number of issues successfully added
*/
async function updateSuggestionWithGenericIssues(
suggestion,
metricIssues,
logger,
env,
slackContext,
) {
// Load CWV reference suggestions
let cwvReferenceSuggestions = {};
try {
const jsonContent = readFileSync(CWV_SUGGESTIONS_FILE_PATH, 'utf8');
cwvReferenceSuggestions = JSON.parse(jsonContent);
} catch {
// Fallback to empty object if file loading fails
logger.warn('Failed to load CWV reference suggestions, using empty suggestions');
await say(env, logger, slackContext, 'Failed to load CWV reference suggestions, using empty suggestions');
}

let issuesAdded = 0;

try {
const data = suggestion.getData();

if (!data.issues) {
data.issues = [];
}

for (const issueType of metricIssues) {
const randomSuggestion = getRandomSuggestion(issueType, cwvReferenceSuggestions);
if (randomSuggestion) {
const genericIssue = {
type: issueType,
value: randomSuggestion,
};
data.issues.push(genericIssue);
data.genericSuggestions = true;
issuesAdded += 1;
}
}

suggestion.setData(data);
suggestion.setUpdatedBy('system');
await suggestion.save();

logger.info(`Updated suggestion ${suggestion.getId()} with ${issuesAdded} generic CWV issues: ${metricIssues.join(', ')}`);
} catch (error) {
logger.error(`Error updating suggestion ${suggestion.getId()} with generic issues:`, error);
}

return issuesAdded;
}

/**
* Processes a single opportunity
* @param {object} opportunity - The opportunity object
* @param {object} logger - The logger object
* @param {object} env - The environment object
* @param {object} slackContext - The Slack context object
* @returns {number} Number of suggestions updated
*/
async function processCWVOpportunity(opportunity, logger, env, slackContext) {
try {
const suggestions = await opportunity.getSuggestions();

const hasSuggestionsWithIssues = suggestions.some(hasExistingIssues);

if (hasSuggestionsWithIssues) {
logger.info(`Opportunity ${opportunity.getId()} already has suggestions with issues, skipping generic suggestions`);
return 0;
}

// Sort suggestions by pageviews (descending)
const sortedSuggestions = suggestions
.filter((suggestion) => {
const data = suggestion.getData();
return data?.pageviews > 0;
})
.sort((a, b) => b.getData().pageviews - a.getData().pageviews);

// Find first 2 suggestions with CWV issues
const suggestionsToUpdate = [];

for (const suggestion of sortedSuggestions) {
if (suggestionsToUpdate.length >= MAX_CWV_DEMO_SUGGESTIONS) break;

const data = suggestion.getData();
const metrics = data.metrics || [];

// Check if any device metrics have CWV issues
let hasCWVIssues = false;
let metricIssues = [];

for (const metric of metrics) {
const issues = getMetricIssues(metric);
if (issues.length > 0) {
hasCWVIssues = true;
metricIssues = issues;
break;
}
}

if (hasCWVIssues) {
suggestionsToUpdate.push({ suggestion, metricIssues });
}
}

// Update suggestions with generic recommendations
const updatePromises = suggestionsToUpdate.map(async ({ suggestion, metricIssues }) => {
const issuesAdded = await updateSuggestionWithGenericIssues(
suggestion,
metricIssues,
logger,
env,
slackContext,
);
return issuesAdded;
});

const issuesAddedResults = await Promise.all(updatePromises);
const totalIssuesAdded = issuesAddedResults.reduce((sum, issuesAdded) => sum + issuesAdded, 0);

if (suggestionsToUpdate.length > 0) {
logger.info(`Added ${totalIssuesAdded} generic CWV issues to ${suggestionsToUpdate.length} suggestions for opportunity ${opportunity.getId()}`);
await say(env, logger, slackContext, `🎯 Added ${totalIssuesAdded} generic CWV issues to ${suggestionsToUpdate.length} suggestions for opportunity ${opportunity.getId()}`);
}

return suggestionsToUpdate.length;
} catch (error) {
logger.error(`Error processing opportunity ${opportunity.getId()}:`, error);
return 0;
}
}

/**
* Runs the CWV demo suggestions processor
* @param {object} message - The message object
* @param {object} context - The context object
*/
export async function runCwvDemoSuggestionsProcessor(message, context) {
const { log, env, dataAccess } = context;
const { Site } = dataAccess;
const {
siteId, organizationId, taskContext,
} = message;
const { profile, slackContext } = taskContext || {};

log.info('Processing CWV demo suggestions for site:', {
taskType: TASK_TYPE,
siteId,
organizationId,
profile,
});

try {
if (!profile || profile !== DEMO) {
log.info(`Skipping CWV processing for non-demo profile. Profile: ${profile}`);
return {
message: 'CWV processing skipped - not a demo profile',
reason: 'non-demo-profile',
profile,
suggestionsAdded: 0,
};
}

log.info(`Confirmed demo profile - proceeding with CWV processing for profile: ${profile}`);

const site = await Site.findById(siteId);
if (!site) {
log.error(`Site not found for siteId: ${siteId}`);
return {
message: 'Site not found',
suggestionsAdded: 0,
};
}

const opportunities = await site.getOpportunities();
const cwvOpportunities = opportunities.filter((opp) => opp.getType() === 'cwv');

if (cwvOpportunities.length === 0) {
log.info('No CWV opportunities found for site, skipping generic suggestions');
return {
message: 'No CWV opportunities found',
suggestionsAdded: 0,
};
}

const suggestionsUpdated = await processCWVOpportunity(
cwvOpportunities[0],
log,
env,
slackContext,
);

log.info(`Processed CWV opportunity for generic suggestions. Updated ${suggestionsUpdated} suggestions.`);

return {
message: 'CWV demo suggestions processor completed',
opportunitiesProcessed: 1,
suggestionsAdded: suggestionsUpdated,
};
} catch (error) {
log.error('Error in CWV demo suggestions processor:', error);
return {
message: 'CWV demo suggestions processor completed with errors',
error: error.message,
suggestionsAdded: 0,
};
}
}

export default runCwvDemoSuggestionsProcessor;
14 changes: 14 additions & 0 deletions static/aem-best-practices.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"lcp": [
"## **Title:** Prioritize the LCP Image and Lazy-Load Other Images\n\n* **Description:** The most important image on the page (the LCP element) is competing for network resources with other, less critical images. This delays the LCP and worsens the user experience. By explicitly telling the browser which image to load eagerly and which to load lazily, we can ensure the main content is visible much faster.\n* **Implementation Priority:** High\n* **Implementation Effort:** Easy\n* **Details:**\n * Set `loading=\"eager\"` on the LCP `<img>` element\n * Set `loading=\"lazy\"` on all other non-critical images that appear below the fold\n * This prevents them from being loaded until the user scrolls near them, freeing up bandwidth for the LCP image\n* **Expected Impact:** LCP reduction of 400-800ms",
"## **Title:** Split CSS into Critical and Non-Critical Files to Unblock Rendering\n\n* **Description:** A large, single CSS file is blocking the page from rendering until it is fully downloaded and parsed. Much of this CSS is not needed for the initial view. This \"render-blocking\" behavior significantly delays when users can see content, negatively impacting LCP.\n* **Implementation Priority:** High\n* **Implementation Effort:** Medium\n* **Details:**\n * Separate CSS into \"critical\" and \"non-critical\" parts\n * Critical CSS should contain only minimal styles required for initial viewport (above the fold)\n * Load critical CSS synchronously in the `<head>`\n * Load non-critical CSS asynchronously so it doesn't block initial rendering\n* **Expected Impact:** LCP reduction of 300-600ms",
"## **Title:** Optimize Custom Font Loading to Speed Up Text Rendering\n\n* **Description:** Custom fonts are blocking the display of important text, including the page's headline, until the font files are fully downloaded. This delay contributes to a higher LCP if the LCP element is a block of text.\n* **Implementation Priority:** Medium\n* **Implementation Effort:** Medium\n* **Details:**\n * Host fonts on your own domain to avoid extra connection to third-party domain\n * Preload the most critical font files in the `<head>`\n * Use `font-display: swap;` in `@font-face` declaration to show fallback font immediately\n* **Expected Impact:** LCP reduction of 200-400ms"
],
"cls": [
"## **Title:** Prevent Layout Shifts by Specifying Image Dimensions\n\n* **Description:** Images on the page are loading without their dimensions being specified. This causes content to jump around as images load, creating a jarring user experience and a high Cumulative Layout Shift (CLS) score.\n* **Implementation Priority:** High\n* **Implementation Effort:** Easy\n* **Details:**\n * Add `width` and `height` attributes to all `<img>` elements\n * This allows the browser to reserve the correct amount of space for the image before it loads\n * Use CSS to ensure images remain responsive (e.g., `max-width: 100%; height: auto;`)\n* **Expected Impact:** CLS reduction of 0.1-0.2",
"## **Title:** Stabilize Layout During Font Loading\n\n* **Description:** The switch between the fallback font and the custom web font causes a noticeable shift in layout because the two fonts have different sizes. This contributes to the CLS score and makes the page feel unstable.\n* **Implementation Priority:** Medium\n* **Implementation Effort:** Medium\n* **Details:**\n * Use the `size-adjust` CSS descriptor in your `@font-face` rule\n * Normalize the size of the fallback font to match the custom font\n * This minimizes the layout shift when the custom font loads\n * Use online tools to calculate the correct `size-adjust` value\n* **Expected Impact:** CLS reduction of 0.05-0.1"
],
"inp": [
"## **Title:** Improve Page Interactivity by Deferring Non-Essential JavaScript\n\n* **Description:** A large JavaScript bundle is being downloaded and executed early during page load, which blocks the browser from responding to user interactions like clicks or typing. This leads to a poor Interaction to Next Paint (INP) score and makes the page feel sluggish.\n* **Implementation Priority:** High\n* **Implementation Effort:** Medium\n* **Details:**\n * Split your JavaScript into smaller chunks\n * Load essential, interactive scripts with `defer` so they don't block parsing\n * Load scripts for non-critical features (e.g., social media widgets, analytics) after the page is interactive\n * Use either a delay (`setTimeout`) or load when the user scrolls them into view\n* **Expected Impact:** INP improvement of 100-200ms"
]
}
Loading