-
Notifications
You must be signed in to change notification settings - Fork 13
feat: codefix handler to update suggestions with patch content #1343
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
dmaurya929
wants to merge
20
commits into
main
Choose a base branch
from
feature/send-autofix-message
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.
Open
Changes from all commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
541f0ba
fix: align form accessibility suggestion creation with sites
dmaurya929 de62758
fix: source extraction
dmaurya929 95eab58
fix: add understanding url
dmaurya929 76e77e6
fix: failing test
dmaurya929 7021d5c
fix: add form specific rules
dmaurya929 8563fd7
feat: add AccessibilityCodeChangeHandler to process code change updates
dmaurya929 d26e7e4
fix: mystique message handling
dmaurya929 fc19a67
Merge branch 'main' of https://github.com/adobe/spacecat-audit-worker…
dmaurya929 88be018
fix: failing test
dmaurya929 2af79e7
fix: update handler name
dmaurya929 4bc3a88
fix: move code update logic to separate Pr
dmaurya929 583bf44
Merge branch 'main' of https://github.com/adobe/spacecat-audit-worker…
dmaurya929 cab7534
refactor: update accessibility opportunities handling
dmaurya929 e0a428c
refactor: revert unnecessary changes
dmaurya929 832ee78
refactor: separate method to send message to mystique for extensibility
dmaurya929 e134cd9
feat: codefix handler to update suggestions with patch content
dmaurya929 93cf961
Merge branch 'main' of https://github.com/adobe/spacecat-audit-worker…
dmaurya929 83d6830
feat: make codefix handlers generic
dmaurya929 9652f0d
fix: add handler
dmaurya929 9414f4e
fix: add deliverytype
dmaurya929 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
There are no files selected for viewing
254 changes: 254 additions & 0 deletions
254
src/accessibility/auto-optimization-handlers/codefix-handler.js
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,254 @@ | ||
| /* | ||
| * 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 { createHash } from 'crypto'; | ||
| import { | ||
| ok, badRequest, notFound, internalServerError, | ||
| } from '@adobe/spacecat-shared-http-utils'; | ||
| import { isNonEmptyArray } from '@adobe/spacecat-shared-utils'; | ||
| import { getObjectFromKey } from '../../utils/s3-utils.js'; | ||
|
|
||
| /** | ||
| * Generates a hash for the given URL and source combination. | ||
| * @param {string} url - The URL to hash | ||
| * @param {string} source - The source to hash | ||
| * @returns {string} - The generated hash (first 16 characters of MD5) | ||
| */ | ||
| function generateUrlSourceHash(url, source) { | ||
| const combined = `${url}_${source}`; | ||
| return createHash('md5').update(combined).digest('hex').substring(0, 16); | ||
| } | ||
|
|
||
| /** | ||
| * Reads code change report from S3 bucket | ||
| * @param {Object} s3Client - The S3 client instance | ||
| * @param {string} bucketName - The S3 bucket name | ||
| * @param {string} siteId - The site ID | ||
| * @param {string} url - The page URL | ||
| * @param {string} source - The source (optional) | ||
| * @param {string} type - The issue type (e.g., 'color-contrast') | ||
| * @param {Object} log - Logger instance | ||
| * @returns {Promise<Object|null>} - The report data or null if not found | ||
| */ | ||
| async function readCodeChangeReport(s3Client, bucketName, siteId, url, source, type, log) { | ||
| try { | ||
| const urlSourceHash = generateUrlSourceHash(url, source || ''); | ||
| const reportKey = `fixes/${siteId}/${urlSourceHash}/${type}/report.json`; | ||
|
|
||
| log.info(`Reading code change report from S3: ${reportKey}`); | ||
|
|
||
| const reportData = await getObjectFromKey(s3Client, bucketName, reportKey, log); | ||
|
|
||
| if (!reportData) { | ||
| log.warn(`No code change report found for key: ${reportKey}`); | ||
| return null; | ||
| } | ||
|
|
||
| log.info(`Successfully read code change report from S3: ${reportKey}`); | ||
| return reportData; | ||
| } catch (error) { | ||
| log.error(`Error reading code change report from S3: ${error.message}`, error); | ||
| return null; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Updates suggestions with code change data | ||
| * @param {Array} suggestions - Array of suggestion objects | ||
| * @param {string} url - The page URL to match | ||
| * @param {string} source - The source to match (optional) | ||
| * @param {string} ruleId - The WCAG rule ID to match | ||
| * @param {Object} reportData - The code change report data | ||
| * @param {Object} log - Logger instance | ||
| * @returns {Promise<Array>} - Array of updated suggestions | ||
| */ | ||
| async function updateSuggestionsWithCodeChange(suggestions, url, source, ruleId, reportData, log) { | ||
| const updatedSuggestions = []; | ||
|
|
||
| try { | ||
| const promises = []; | ||
| for (const suggestion of suggestions) { | ||
| const suggestionData = suggestion.getData(); | ||
|
|
||
| // Check if this suggestion matches the criteria | ||
| const suggestionUrl = suggestionData.url; | ||
| const suggestionSource = suggestionData.source; | ||
| const suggestionRuleId = suggestionData.issues[0]?.type; | ||
|
|
||
| if (suggestionUrl === url | ||
| && (!source || suggestionSource === source) | ||
| && suggestionRuleId === ruleId | ||
| && !!reportData.diff) { | ||
| log.info(`Updating suggestion ${suggestion.getId()} with code change data`); | ||
|
|
||
| // Update suggestion data with diff content and availability flag | ||
| const updatedData = { | ||
| ...suggestionData, | ||
| patchContent: reportData.diff, | ||
| isCodeChangeAvailable: true, | ||
| }; | ||
|
|
||
| suggestion.setData(updatedData); | ||
| suggestion.setUpdatedBy('system'); | ||
|
|
||
| promises.push(suggestion.save()); | ||
| updatedSuggestions.push(suggestion); | ||
|
|
||
| log.info(`Successfully updated suggestion ${suggestion.getId()}`); | ||
| } | ||
| } | ||
| await Promise.all(promises); | ||
| } catch (error) { | ||
| log.error(`Error updating suggestions with code change data: ${error.message}`, error); | ||
| throw error; | ||
| } | ||
|
|
||
| return updatedSuggestions; | ||
| } | ||
|
|
||
| /** | ||
| * AccessibilityCodeFixHandler - Updates suggestions with code changes from S3 | ||
| * | ||
| * Expected message format: | ||
| * { | ||
| * "siteId": "<site-id>", | ||
| * "type": "codefix:accessibility", | ||
| * "data": { | ||
| * "opportunityId": "<uuid>", | ||
| * "updates": [ | ||
| * { | ||
| * "url": "<page url>", | ||
| * "source": "<source>", // optional | ||
| * "type": ["color-contrast", "select-name"] | ||
| * } | ||
| * ] | ||
| * } | ||
| * } | ||
| * | ||
| * @param {Object} message - The SQS message | ||
| * @param {Object} context - The context object containing dataAccess, log, s3Client, etc. | ||
| * @returns {Promise<Response>} - HTTP response | ||
| */ | ||
| export default async function accessibilityCodeFixHandler(message, context) { | ||
| const { | ||
| log, dataAccess, s3Client, env, | ||
| } = context; | ||
| const { Opportunity } = dataAccess; | ||
| const { siteId, data } = message; | ||
|
|
||
| if (!data) { | ||
| log.error('AccessibilityCodeFixHandler: No data provided in message'); | ||
| return badRequest('No data provided in message'); | ||
| } | ||
|
|
||
| const { opportunityId, updates } = data; | ||
|
|
||
| if (!opportunityId) { | ||
| log.error('[AccessibilityCodeFixHandler] No opportunityId provided'); | ||
| return badRequest('No opportunityId provided'); | ||
| } | ||
|
|
||
| if (!isNonEmptyArray(updates)) { | ||
| log.error('[AccessibilityCodeFixHandler] No updates provided or updates is empty'); | ||
| return badRequest('No updates provided or updates is empty'); | ||
| } | ||
|
|
||
| log.info(`[AccessibilityCodeFixHandler] Processing message for siteId: ${siteId}, opportunityId: ${opportunityId}`); | ||
|
|
||
| try { | ||
| // Find the opportunity | ||
| const opportunity = await Opportunity.findById(opportunityId); | ||
|
|
||
| if (!opportunity) { | ||
| log.error(`[AccessibilityCodeFixHandler] Opportunity not found for ID: ${opportunityId}`); | ||
| return notFound('Opportunity not found'); | ||
| } | ||
|
|
||
| // Verify the opportunity belongs to the correct site | ||
| if (opportunity.getSiteId() !== siteId) { | ||
| const errorMsg = `[AccessibilityCodeFixHandler] Site ID mismatch. Expected: ${siteId}, Found: ${opportunity.getSiteId()}`; | ||
| log.error(errorMsg); | ||
| return badRequest('Site ID mismatch'); | ||
| } | ||
|
|
||
| // Get all suggestions for the opportunity | ||
| const suggestions = await opportunity.getSuggestions(); | ||
|
|
||
| if (!isNonEmptyArray(suggestions)) { | ||
| log.warn(`[AccessibilityCodeFixHandler] No suggestions found for opportunity: ${opportunityId}`); | ||
| return ok('No suggestions found for opportunity'); | ||
| } | ||
|
|
||
| const bucketName = env.S3_MYSTIQUE_BUCKET_NAME; | ||
|
|
||
| if (!bucketName) { | ||
| log.error('AccessibilityCodeFixHandler: S3_MYSTIQUE_BUCKET_NAME environment variable not set'); | ||
| return internalServerError('S3 bucket name not configured'); | ||
| } | ||
|
|
||
| let totalUpdatedSuggestions = 0; | ||
|
|
||
| // Process each update | ||
| await Promise.all(updates.map(async (update) => { | ||
| const { url, source, type: types } = update; | ||
|
|
||
| if (!url) { | ||
| log.warn('[AccessibilityCodeFixHandler] Skipping update without URL'); | ||
| return; | ||
| } | ||
|
|
||
| if (!isNonEmptyArray(types)) { | ||
| log.warn(`[AccessibilityCodeFixHandler] Skipping update for URL ${url} without types`); | ||
| return; | ||
| } | ||
|
|
||
| log.info(`[AccessibilityCodeFixHandler] Processing update for URL: ${url}, source: ${source || 'N/A'}, types: ${types.join(', ')}`); | ||
|
|
||
| // For each type in the update, try to read the code change report | ||
| await Promise.all(types.map(async (ruleId) => { | ||
| let reportData = await readCodeChangeReport( | ||
| s3Client, | ||
| bucketName, | ||
| siteId, | ||
| url, | ||
| source, | ||
| ruleId, | ||
| log, | ||
| ); | ||
|
|
||
| if (!reportData) { | ||
| log.warn(`[AccessibilityCodeFixHandler] No code change report found for URL: ${url}, source: ${source}, type: ${ruleId}`); | ||
| return; | ||
| } | ||
|
|
||
| reportData = JSON.parse(reportData); | ||
|
|
||
| // Update matching suggestions with the code change data | ||
| const updatedSuggestions = await updateSuggestionsWithCodeChange( | ||
| suggestions, | ||
| url, | ||
| source, | ||
| ruleId, | ||
| reportData, | ||
| log, | ||
| ); | ||
| totalUpdatedSuggestions += updatedSuggestions.length; | ||
| })); | ||
| })); | ||
|
|
||
| log.info(`[AccessibilityCodeFixHandler] Successfully processed all updates. Total suggestions updated: ${totalUpdatedSuggestions}`); | ||
| return ok(); | ||
| } catch (error) { | ||
| log.error(`[AccessibilityCodeFixHandler] Error processing message: ${error.message}`, error); | ||
| return internalServerError(`Error processing message: ${error.message}`); | ||
| } | ||
| } | ||
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 |
|---|---|---|
|
|
@@ -754,3 +754,89 @@ export async function sendRunImportMessage( | |
| ...(data && { data }), | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * Groups suggestions by URL, source, and issue type, then sends messages | ||
| * to the importer worker for code-fix generation | ||
| * | ||
| * @param {Object} opportunity - The opportunity object containing suggestions | ||
| * @param {string} auditId - The audit ID | ||
| * @param {Object} context - The context object containing log, sqs, env, and site | ||
| * @returns {Promise<void>} | ||
| */ | ||
| export async function sendCodeFixMessagesToImporter(opportunity, auditId, context) { | ||
| const { | ||
| log, sqs, env, site, | ||
| } = context; | ||
|
|
||
| const siteId = opportunity.getSiteId(); | ||
| const baseUrl = site.getBaseURL(); | ||
| const opportunityType = opportunity.getType(); | ||
|
|
||
| try { | ||
| // Get all suggestions from the opportunity | ||
| const suggestions = await opportunity.getSuggestions(); | ||
| if (!suggestions || suggestions.length === 0) { | ||
| log.info(`[${opportunityType}] [Site Id: ${siteId}] No suggestions found for code-fix generation`); | ||
| return; | ||
| } | ||
|
|
||
| // Group suggestions by URL, source, and issueType | ||
| const groupedSuggestions = new Map(); | ||
|
|
||
| suggestions.forEach((suggestion) => { | ||
| const suggestionData = suggestion.getData(); | ||
| const { url, source = 'default', issues } = suggestionData; | ||
|
|
||
| // By design, data.issues will always have length 1 | ||
| if (issues && issues.length > 0) { | ||
| const issueType = issues[0].type; | ||
| const groupKey = `${url}|${source}|${issueType}`; | ||
| if (!groupedSuggestions.has(groupKey)) { | ||
| groupedSuggestions.set(groupKey, { | ||
| url, | ||
| source, | ||
| issueType, | ||
| suggestionIds: [], | ||
| }); | ||
| } | ||
|
|
||
| // Add the suggestion ID to the group | ||
| groupedSuggestions.get(groupKey).suggestionIds.push(suggestion.getId()); | ||
| } | ||
| }); | ||
|
|
||
| log.info(`[${opportunityType}] [Site Id: ${siteId}] Grouped suggestions into ${groupedSuggestions.size} groups for code-fix generation`); | ||
|
|
||
| const messagePromises = Array.from(groupedSuggestions.values()).map(async (group) => { | ||
| const message = { | ||
| type: 'code', | ||
| siteId, | ||
| forward: { | ||
| queue: env.QUEUE_SPACECAT_TO_MYSTIQUE, | ||
| type: `codefix:${opportunityType}`, | ||
| siteId, | ||
| auditId, | ||
| url: baseUrl, | ||
| deliveryType: site.getDeliveryType(), | ||
| data: { | ||
| opportunityId: opportunity.getId(), | ||
| suggestionIds: group.suggestionIds, | ||
| }, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I thought the forward message has the structure There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No |
||
| }, | ||
| }; | ||
|
|
||
| try { | ||
| await sqs.sendMessage(env.IMPORT_WORKER_QUEUE_URL, message); | ||
| log.info(`[${opportunityType}] [Site Id: ${siteId}] Sent code-fix message to importer for URL: ${group.url}, source: ${group.source}, issueType: ${group.issueType}, suggestions: ${group.suggestionIds.length}`); | ||
| } catch (error) { | ||
| log.error(`[${opportunityType}] [Site Id: ${siteId}] Failed to send code-fix message for URL: ${group.url}, error: ${error.message}`); | ||
| } | ||
| }); | ||
|
|
||
| await Promise.all(messagePromises); | ||
| log.info(`[${opportunityType}] [Site Id: ${siteId}] Completed sending ${messagePromises.length} code-fix messages to importer`); | ||
| } catch (error) { | ||
| log.error(`[${opportunityType}] [Site Id: ${siteId}] Error in sendCodeFixMessagesToImporter: ${error.message}`); | ||
| } | ||
| } | ||
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
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.