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
7 changes: 7 additions & 0 deletions packages/spacecat-shared-utils/src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,13 @@ const OPPORTUNITY_TYPES = {
PAID_COOKIE_CONSENT: 'paid-cookie-consent',
};

/**
* Default CPC (Cost Per Click) value in dollars used when Ahrefs organic traffic data
* is not available or invalid.
*/
const DEFAULT_CPC_VALUE = 1.5;

export {
OPPORTUNITY_TYPES,
DEFAULT_CPC_VALUE,
};
35 changes: 35 additions & 0 deletions packages/spacecat-shared-utils/src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,9 +253,44 @@ export function getStoredMetrics(config: object, context: object):
*/
export function storeMetrics(content: object, config: object, context: object): Promise<string>;

/**
* Retrieves an object from S3 by its key and returns its JSON parsed content.
* If the object is not JSON, returns the raw body.
* If the object is not found, returns null.
* @param s3Client - The S3 client
* @param bucketName - The name of the S3 bucket
* @param key - The key of the S3 object
* @param log - A logger instance
* @returns The content of the S3 object or null if not found
*/
export function getObjectFromKey(
s3Client: any,
bucketName: string,
key: string,
log: any
): Promise<any | null>;

/**
* Fetches the organic traffic data for a site from S3 and calculates the CPC value
* @param context - Context object
* @param context.env - Environment variables
* @param context.env.S3_IMPORTER_BUCKET_NAME - S3 importer bucket name
* @param context.s3Client - S3 client
* @param context.log - Logger
* @param siteId - The site ID
* @returns CPC value in dollars
*/
export function calculateCPCValue(context: object, siteId: string): Promise<number>;

export function s3Wrapper(fn: (request: object, context: object) => Promise<Response>):
(request: object, context: object) => Promise<Response>;

/**
* Default CPC (Cost Per Click) value in dollars used when Ahrefs organic traffic data
* is not available or invalid.
*/
export const DEFAULT_CPC_VALUE: number;

export function fetch(url: string | Request, options?: RequestOptions): Promise<Response>;

export function tracingFetch(url: string | Request, options?: RequestOptions): Promise<Response>;
Expand Down
6 changes: 4 additions & 2 deletions packages/spacecat-shared-utils/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,11 @@ export {
extractUrlsFromSuggestion,
} from './url-extractors.js';

export { getStoredMetrics, storeMetrics } from './metrics-store.js';
export { getStoredMetrics, storeMetrics, calculateCPCValue } from './metrics-store.js';

export { s3Wrapper } from './s3.js';
export { s3Wrapper, getObjectFromKey } from './s3.js';

export { DEFAULT_CPC_VALUE } from './constants.js';

export { fetch } from './adobe-fetch.js';
export { tracingFetch, SPACECAT_USER_AGENT } from './tracing-fetch.js';
Expand Down
44 changes: 44 additions & 0 deletions packages/spacecat-shared-utils/src/metrics-store.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
* governing permissions and limitations under the License.
*/
import { GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3';
import { DEFAULT_CPC_VALUE } from './constants.js';
import { getObjectFromKey } from './s3.js';

function createFilePath({ siteId, source, metric }) {
if (!siteId) {
Expand Down Expand Up @@ -80,3 +82,45 @@ export async function storeMetrics(content, config, context) {
throw new Error(`Failed to upload metrics to ${filePath}, error: ${e.message}`);
}
}

/**
* Fetches the organic traffic data for a site from S3 and calculate the CPC value as per
* https://wiki.corp.adobe.com/pages/viewpage.action?spaceKey=AEMSites&title=Success+Studio+Projected+Business+Impact+Metrics#SuccessStudioProjectedBusinessImpactMetrics-IdentifyingCPCvalueforadomain
* @param context
* @param siteId
* @returns {number} CPC value
*/
export async function calculateCPCValue(context, siteId) {
if (!context?.env?.S3_IMPORTER_BUCKET_NAME) {
throw new Error('S3 importer bucket name is required');
}
if (!context.s3Client) {
throw new Error('S3 client is required');
}
if (!context.log) {
throw new Error('Logger is required');
}
if (!siteId) {
throw new Error('SiteId is required');
}
const { s3Client, log } = context;
const bucketName = context.env.S3_IMPORTER_BUCKET_NAME;
const key = `metrics/${siteId}/ahrefs/organic-traffic.json`;
try {
const organicTrafficData = await getObjectFromKey(s3Client, bucketName, key, log);
if (!Array.isArray(organicTrafficData) || organicTrafficData.length === 0) {
log.warn(`Organic traffic data not available for ${siteId}. Using Default CPC value.`);
return DEFAULT_CPC_VALUE;
}
const lastTraffic = organicTrafficData.at(-1);
if (!lastTraffic.cost || !lastTraffic.value) {
log.warn(`Invalid organic traffic data present for ${siteId} - cost:${lastTraffic.cost} value:${lastTraffic.value}, Using Default CPC value.`);
return DEFAULT_CPC_VALUE;
}
// dividing by 100 for cents to dollar conversion
return lastTraffic.cost / lastTraffic.value / 100;
} catch (err) {
log.error(`Error fetching organic traffic data for site ${siteId}. Using Default CPC value.`, err);
return DEFAULT_CPC_VALUE;
}
}
48 changes: 47 additions & 1 deletion packages/spacecat-shared-utils/src/s3.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,55 @@
* governing permissions and limitations under the License.
*/

import { S3Client } from '@aws-sdk/client-s3';
import { GetObjectCommand, S3Client } from '@aws-sdk/client-s3';
import { instrumentAWSClient } from './xray.js';

/**
* Retrieves an object from S3 by its key and returns its JSON parsed content.
* If the object is not JSON, returns the raw body.
* If the object is not found, returns null.
* @param {import('@aws-sdk/client-s3').S3Client} s3Client - an S3 client
* @param {string} bucketName - the name of the S3 bucket
* @param {string} key - the key of the S3 object
* @param {import('@azure/logger').Logger} log - a logger instance
* @returns {Promise<import('@aws-sdk/client-s3').GetObjectOutput['Body'] | null>}
* - the content of the S3 object
*/
export async function getObjectFromKey(s3Client, bucketName, key, log) {
if (!s3Client || !bucketName || !key) {
log.error(
'Invalid input parameters in getObjectFromKey: ensure s3Client, bucketName, and key are provided.',
);
return null;
}
const command = new GetObjectCommand({
Bucket: bucketName,
Key: key,
});
try {
const response = await s3Client.send(command);
const contentType = response.ContentType;
const body = await response.Body.transformToString();

if (contentType && contentType.includes('application/json')) {
try {
return JSON.parse(body);
} catch (parseError) {
log.error(`Unable to parse content for key ${key}`, parseError);
return null;
}
}
// Always return body for non-JSON content types
return body;
} catch (err) {
log.error(
`Error while fetching S3 object from bucket ${bucketName} using key ${key}`,
err,
);
return null;
}
}

/**
* Adds an S3Client instance and bucket to the context.
*
Expand Down
73 changes: 38 additions & 35 deletions packages/spacecat-shared-utils/test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,42 @@ import * as allExports from '../src/index.js';
describe('Index Exports', () => {
const expectedExports = [
'arrayEquals',
'calculateCPCValue',
'composeAuditURL',
'composeBaseURL',
'dateAfterDays',
'deepEqual',
'DEFAULT_CPC_VALUE',
'DELIVERY_TYPES',
'detectAEMVersion',
'detectLocale',
'determineAEMCSPageId',
'ensureHttps',
'extractUrlsFromOpportunity',
'extractUrlsFromSuggestion',
'fetch',
'FORMS_AUDIT_INTERVAL',
'generateCSVFile',
'getStoredMetrics',
'replacePlaceholders',
'getStaticContent',
'getAccessToken',
'getDateRanges',
'getHighFormViewsLowConversionMetrics',
'getHighPageViewsLowFormCtrMetrics',
'getHighPageViewsLowFormViewsMetrics',
'getLastNumberOfWeeks',
'getMonthInfo',
'getObjectFromKey',
'getPageEditUrl',
'getPrompt',
'getQuery',
'getSpacecatRequestHeaders',
'getStaticContent',
'getStoredMetrics',
'getTemporalCondition',
'getWeekInfo',
'hasText',
'instrumentAWSClient',
'isArray',
'isAWSLambda',
'isBoolean',
'isInteger',
'isIsoDate',
Expand All @@ -42,16 +65,25 @@ describe('Index Exports', () => {
'isString',
'isValidDate',
'isValidEmail',
'isValidHelixPreviewUrl',
'isValidIMSOrgId',
'isValidUrl',
'isValidUUID',
'isValidIMSOrgId',
'isValidHelixPreviewUrl',
'isoCalendarWeek',
'isoCalendarWeekMonday',
'isoCalendarWeekSunday',
'llmoConfig',
'logWrapper',
'prependSchema',
'getAccessToken',
'prettifyLogForwardingConfig',
'replacePlaceholders',
'resolveCanonicalUrl',
'resolveCustomerSecretsName',
'resolveSecretsName',
'retrievePageAuthentication',
's3Wrapper',
'schemas',
'SPACECAT_USER_AGENT',
'sqsEventAdapter',
'sqsWrapper',
'storeMetrics',
Expand All @@ -61,36 +93,7 @@ describe('Index Exports', () => {
'stripWWW',
'toBoolean',
'tracingFetch',
'getHighFormViewsLowConversionMetrics',
'getHighPageViewsLowFormViewsMetrics',
'getHighPageViewsLowFormCtrMetrics',
'FORMS_AUDIT_INTERVAL',
'SPACECAT_USER_AGENT',
'isAWSLambda',
'instrumentAWSClient',
'retrievePageAuthentication',
'getDateRanges',
'getLastNumberOfWeeks',
'resolveCanonicalUrl',
'getSpacecatRequestHeaders',
'ensureHttps',
'getWeekInfo',
'getMonthInfo',
'getTemporalCondition',
'urlMatchesFilter',
'extractUrlsFromOpportunity',
'extractUrlsFromSuggestion',
'detectAEMVersion',
'determineAEMCSPageId',
'DELIVERY_TYPES',
'getPageEditUrl',
'llmoConfig',
'schemas',
'detectLocale',
'prettifyLogForwardingConfig',
'isoCalendarWeek',
'isoCalendarWeekSunday',
'isoCalendarWeekMonday',
];

it('exports all expected functions', () => {
Expand Down
Loading