-
Notifications
You must be signed in to change notification settings - Fork 0
feat: cwv suggestions processor #48
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
tkotthakota-adobe
wants to merge
75
commits into
main
Choose a base branch
from
SITES-33054
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+1,190
−7
Open
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 9e7e83a
address some review comments
tkotthakota-adobe 6e9e31e
remove Suggestion object as parameters
tkotthakota-adobe fda3226
refactor processCWVOpportunity method
tkotthakota-adobe f028c51
add suggestionUpdated to return statement
tkotthakota-adobe b8854bb
refactor to move aem best practices to a file
tkotthakota-adobe a0985f5
Merge branch 'main' of github.com:adobe/spacecat-task-processor into …
tkotthakota-adobe 15d9c9d
Merge branch 'main' of github.com:adobe/spacecat-task-processor into …
tkotthakota-adobe ed3511c
simplify path logic
tkotthakota-adobe b15c01d
review comments
tkotthakota-adobe 4ae7121
logs
tkotthakota-adobe 01c5d29
use relative path logic
tkotthakota-adobe 9581f18
tests
tkotthakota-adobe e396173
path fix
tkotthakota-adobe 41fac8d
path
tkotthakota-adobe fe5e974
debug
tkotthakota-adobe d62dc1a
path fix
tkotthakota-adobe 962fbc1
use relative path
tkotthakota-adobe 2bb858e
debug
tkotthakota-adobe 1b05238
debug
tkotthakota-adobe ad78fec
move fodler
tkotthakota-adobe de80db4
fix
tkotthakota-adobe e426a6f
fix
tkotthakota-adobe 0dd14f7
fix
tkotthakota-adobe f4ddae1
fix
tkotthakota-adobe d9ef7b4
move
tkotthakota-adobe f233726
use suggestions with in js file
tkotthakota-adobe ef44bb6
adjust code
tkotthakota-adobe d89af16
test using file
tkotthakota-adobe 4b99c75
update package
tkotthakota-adobe 8f67623
add extra slack message
tkotthakota-adobe 68ab01a
update package
tkotthakota-adobe 5901910
adjust condition
tkotthakota-adobe 8b3d247
remove line
tkotthakota-adobe 555ef98
merge main
tkotthakota-adobe 29f3eed
refactor to use seperate files for demo cwv suggestions
tkotthakota-adobe 681f0ea
Merge branch 'main' of github.com:adobe/spacecat-task-processor into …
tkotthakota-adobe 09c56bb
Merge branch 'main' of github.com:adobe/spacecat-task-processor into …
tkotthakota-adobe a2cf0d9
path fix
tkotthakota-adobe 4a0d585
move static under src
tkotthakota-adobe feee9fe
refactor
tkotthakota-adobe 55ad2f6
update slack message
tkotthakota-adobe e1c9220
add debug logs
tkotthakota-adobe a41fd94
fix path
tkotthakota-adobe 60970b8
change file path
tkotthakota-adobe f1f06e1
remove unsed files
tkotthakota-adobe 71694cb
adjust path
tkotthakota-adobe 19e853a
add debugs
tkotthakota-adobe 02983f6
rename
tkotthakota-adobe 2d6bd16
logs
tkotthakota-adobe 21609e2
fix
tkotthakota-adobe 9590845
Merge branch 'main' of github.com:adobe/spacecat-task-processor into …
tkotthakota-adobe 1baa7b6
fix
tkotthakota-adobe 6f9625d
Merge branch 'main' of github.com:adobe/spacecat-task-processor into …
tkotthakota-adobe b8fc316
refactor
tkotthakota-adobe 596c996
refactor readStaticFile
tkotthakota-adobe 71dd2bb
support txt file deployment
tkotthakota-adobe d701a69
refac
tkotthakota-adobe 812565c
fix
tkotthakota-adobe 927ab55
use load file approach
tkotthakota-adobe aa6a840
add slack message
tkotthakota-adobe c2e4af3
slack
tkotthakota-adobe 2ff7de4
debug
tkotthakota-adobe 8ed720c
refactor to use static folder for files
tkotthakota-adobe e4cf7fb
add generic suggestions to cwv opportunity
tkotthakota-adobe 9dbf1ec
check for non generic suggestions
tkotthakota-adobe 59c3f56
add debugs
tkotthakota-adobe 01972e6
fix
tkotthakota-adobe 3f4d4e4
rafctor
tkotthakota-adobe 5a5f974
refac
tkotthakota-adobe 23045fe
fix
tkotthakota-adobe 80981ca
debug
tkotthakota-adobe 6cb54e9
debug
tkotthakota-adobe 5379e2d
fix
tkotthakota-adobe 86e4db7
refactor
tkotthakota-adobe File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
tkotthakota-adobe marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
] | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.