Skip to content
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

feat: SITES-25573 Enable/Disable domains for CWV audit via API/slack command #525

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
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
4 changes: 4 additions & 0 deletions docs/openapi/api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ tags:
description: Site operations
- name: audit
description: Audit operations
- name: site-audit
description: Site-audit links operations
- name: auth
description: Init authentication against 3rd party services
- name: organization
Expand Down Expand Up @@ -99,6 +101,8 @@ paths:
$ref: './audit-api.yaml#/latest-audit-for-site'
/sites/{siteId}/{auditType}:
$ref: './audit-api.yaml#/update-handler-type-config-for-site'
/sites/audits:
$ref: './sites-audits-api.yaml#/update-sites-audits'
/sites/{siteId}/key-events:
$ref: './key-events-api.yaml#/key-events'
/sites/{siteId}/key-events/{keyEventId}:
Expand Down
22 changes: 22 additions & 0 deletions docs/openapi/schemas.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,28 @@ UpdateHandlerTypeConfig:
excludedURLs:
- 'https://www.adobe.com/some-page'
- 'https://www.adobe.com/another-page'
SitesAuditsUpdateResult:
type: array
items:
type: object
properties:
baseURL:
type: string
description: The base URL of the site
response:
type: object
description: The response for operation
properties:
status:
type: string
description: The status of the operation
message:
type: string
description: The message of the operation
site:
$ref: './schemas.yaml#/SiteList'
required:
- status
Audit:
type: object
readOnly: true
Expand Down
47 changes: 47 additions & 0 deletions docs/openapi/sites-audits-api.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
update-sites-audits:
patch:
tags:
- site-audit
summary: Enable/Disable audits for multiple sites
description: |
This endpoint is useful for enabling or disabling audit types for multiple sites.
operationId: update
requestBody:
required: true
content:
application/json:
schema:
type: array
items:
type: object
properties:
baseURL:
$ref: './schemas.yaml#/URL'
auditTypes:
type: array
items:
type: string
examples:
- 404
- broken-backlinks
- organic-traffic
enableAudits:
type: boolean
description: Set to true to enable audits, or false to disable them.
required:
- baseURL
- auditTypes
- enableAudits
responses:
'207':
description: A list of baseURL, the status of the update, and the corresponding site if successful, or the error message if failed.
content:
application/json:
schema:
$ref: './schemas.yaml#/SitesAuditsUpdateResult'
'401':
$ref: './responses.yaml#/401'
'500':
$ref: './responses.yaml#/500'
security:
- admin_key: [ ]
112 changes: 112 additions & 0 deletions src/controllers/sites-audits.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/*
* Copyright 2024 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 {
badRequest,
createResponse,
internalServerError,
} from '@adobe/spacecat-shared-http-utils';
import { isObject, isValidUrl } from '@adobe/spacecat-shared-utils';

import { ConfigurationDto } from '../dto/configuration.js';
import { SiteDto } from '../dto/site.js';

/**
* Sites Audits controller.
* @param {DataAccess} dataAccess - Data access.
* @returns {object} Sites Audits controller.
* @constructor
*/
export default (dataAccess) => {
if (!isObject(dataAccess)) {
throw new Error('Data access required');
}

const validateInput = ({ baseURL, enableAudits, auditTypes }) => {
if (!baseURL) {
throw new Error('Base URL is required');
}

if (!isValidUrl(baseURL)) {
throw new Error(`Invalid Base URL format: ${baseURL}`);
}

if (!Array.isArray(auditTypes) || auditTypes.length === 0) {
throw new Error('Audit types are required');
}

if (typeof enableAudits === 'undefined') {
throw new Error('The "enableAudits" flag is required');
}

if (typeof enableAudits !== 'boolean') {
throw new Error('The "enableAudits" flag should be boolean');
}
};

const update = async (context) => {
const sitesConfigurations = context.data;

try {
for (const siteConfiguration of sitesConfigurations) {
validateInput(siteConfiguration);
}
} catch (error) {
return badRequest(error.message);
}

try {
let hasUpdates = false;
const configuration = await dataAccess.getConfiguration();

const responses = await Promise.all(
sitesConfigurations.map(async ({ baseURL, auditTypes, enableAudits }) => {
const site = await dataAccess.getSiteByBaseURL(baseURL);
if (!site) {
return {
baseURL,
response: {
message: `Site with baseURL: ${baseURL} not found`,
status: 404,
},
};
}
hasUpdates = true;
for (const auditType of auditTypes) {
if (enableAudits === true) {
configuration.enableHandlerForSite(auditType, site);
} else {
configuration.disableHandlerForSite(auditType, site);
}
}

return {
baseURL: site.getBaseURL(),
response: { site: SiteDto.toJSON(site), status: 200 },
};
}),
);

if (hasUpdates === true) {
await dataAccess.updateConfiguration(ConfigurationDto.toJSON(configuration));
}

return createResponse(responses, 207);
} catch (error) {
return internalServerError(error.message || 'Failed to enable/disable audits for all provided sites');
}
};

return {
update,
};
};
2 changes: 2 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import SitesController from './controllers/sites.js';
import ExperimentsController from './controllers/experiments.js';
import HooksController from './controllers/hooks.js';
import SlackController from './controllers/slack.js';
import SitesAuditsController from './controllers/sites-audits.js';
import trigger from './controllers/trigger.js';

// prevents webpack build error
Expand Down Expand Up @@ -97,6 +98,7 @@ async function run(request, context) {
FulfillmentController(context),
ImportController(context),
AssistantController(context),
SitesAuditsController(context),
);

const routeMatch = matchPath(method, suffix, routeHandlers);
Expand Down
3 changes: 3 additions & 0 deletions src/routes/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ function isStaticRoute(routePattern) {
* @param {Object} fulfillmentController - The fulfillment controller.
* @param {Object} importController - The import controller.
* @param {Object} assistantController - The assistant controller.
* @param {Object} sitesAuditsController - The sites audits controller.
* @return {{staticRoutes: {}, dynamicRoutes: {}}} - An object with static and dynamic routes.
*/
export default function getRouteHandlers(
Expand All @@ -67,6 +68,7 @@ export default function getRouteHandlers(
fulfillmentController,
importController,
assistantController,
sitesAuditsController,
) {
const staticRoutes = {};
const dynamicRoutes = {};
Expand Down Expand Up @@ -101,6 +103,7 @@ export default function getRouteHandlers(
'PATCH /sites/:siteId/:auditType': auditsController.patchAuditForSite,
'GET /sites/:siteId/audits/latest': auditsController.getAllLatestForSite,
'GET /sites/:siteId/latest-audit/:auditType': auditsController.getLatestForSite,
'PATCH /sites/audits': sitesAuditsController.update,
'GET /sites/:siteId/experiments': experimentsController.getExperiments,
'GET /sites/:siteId/key-events': sitesController.getKeyEventsBySiteID,
'POST /sites/:siteId/key-events': sitesController.createKeyEvent,
Expand Down
2 changes: 2 additions & 0 deletions src/support/slack/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import runScrape from './commands/run-scrape.js';
import setLiveStatus from './commands/set-live-status.js';
import getGoogleLink from './commands/create-google-link.js';
import help from './commands/help.js';
import updateSitesAudits from './commands/update-sites-audits.js';

/**
* Returns all commands.
Expand All @@ -42,4 +43,5 @@ export default (context) => [
setLiveStatus(context),
getGoogleLink(context),
help(context),
updateSitesAudits(context),
];
112 changes: 112 additions & 0 deletions src/support/slack/commands/update-sites-audits.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/*
* Copyright 2024 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 BaseCommand from './base.js';
import { extractURLFromSlackInput } from '../../../utils/slack/base.js';
import { ConfigurationDto } from '../../../dto/configuration.js';

const PHRASES = ['audits'];

export default (context) => {
const baseCommand = BaseCommand({
id: 'sites--audits',
name: 'Update Sites Audits',
description: 'Enables or disables audits for multiple sites.',
phrases: PHRASES,
usageText: `${PHRASES[0]} {enable/disable} {site1,site2,...} {auditType1,auditType2,...}`,
});

const { log, dataAccess } = context;

const validateInput = ({ baseURLs, enableAudits, auditTypes }) => {
if (!Array.isArray(baseURLs) || baseURLs.length === 0) {
throw new Error('Base URLs are required');
}

if (!Array.isArray(auditTypes) || auditTypes.length === 0) {
throw new Error('Audit types are required');
}

if (enableAudits.length === 0) {
throw new Error('enable/disable value is required');
}

if (['enable', 'disable'].includes(enableAudits) === false) {
throw new Error(`Invalid enable/disable value: ${enableAudits}`);
}
};

const handleExecution = async (args, slackContext) => {
const { say } = slackContext;
const [enableAuditsInput, baseURLsInput, auditTypesInput] = args;

const enableAudits = enableAuditsInput.toLowerCase();
const baseURLs = baseURLsInput.length > 0 ? baseURLsInput.split(',') : [];
if (baseURLs) {
baseURLs.map((baseURL) => extractURLFromSlackInput(baseURL));
}
const auditTypes = auditTypesInput.length > 0 ? auditTypesInput.split(',') : [];

try {
validateInput({ baseURLs, enableAudits, auditTypes });
} catch (error) {
await say(error.message);
return;
}

try {
let hasUpdates = false;
const configuration = await dataAccess.getConfiguration();

const responses = await Promise.all(
baseURLs
.map(async (baseURL) => {
const site = await dataAccess.getSiteByBaseURL(extractURLFromSlackInput(baseURL));

if (!site) {
return { payload: `Cannot update site with baseURL: ${baseURL}, site not found` };
}

hasUpdates = true;
for (const auditType of auditTypes) {
if (enableAudits === 'enable') {
configuration.enableHandlerForSite(auditType, site);
} else {
configuration.disableHandlerForSite(auditType, site);
}
}

return { payload: `${site.getBaseURL()}: successfully updated` };
}),
);

if (hasUpdates === true) {
await dataAccess.updateConfiguration(ConfigurationDto.toJSON(configuration));
}

const message = `Bulk update completed with the following responses:\n${responses
.map((response) => response.payload)
.join('\n')}\n`;

await say(message);
} catch (error) {
log.error(error);
await say(`:nuclear-warning: Failed to enable audits for all provided sites: ${error.message}`);
}
};

baseCommand.init(context);

return {
...baseCommand,
handleExecution,
};
};
Loading
Loading