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
30 changes: 30 additions & 0 deletions src/cwv-trends-audit/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* 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.
*/

export const AUDIT_TYPE = 'cwv-trends-audit';
export const TREND_DAYS = 28;
export const CURRENT_WEEK_DAYS = 7;
export const S3_BASE_PATH = 'metrics/cwv-trends';
export const MIN_PAGEVIEWS = 1000;
export const DEFAULT_DEVICE_TYPE = 'mobile';

// Core Web Vitals thresholds based on https://web.dev/articles/vitals
export const CWV_THRESHOLDS = {
LCP: { GOOD: 2500, POOR: 4000 },
CLS: { GOOD: 0.1, POOR: 0.25 },
INP: { GOOD: 200, POOR: 500 },
};

export const OPPORTUNITY_TITLES = {
mobile: 'Mobile Web Performance Trends Report',
desktop: 'Desktop Web Performance Trends Report',
};
45 changes: 45 additions & 0 deletions src/cwv-trends-audit/cwv-categorizer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* 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 { CWV_THRESHOLDS } from './constants.js';

/**
* Categorizes a URL based on CWV metrics.
* - Good: all available metrics within good thresholds
* - Poor: any available metric exceeds poor threshold (OR logic)
* - Needs Improvement: everything else
* - null: no metrics available
*
* @param {number|null} lcp - Largest Contentful Paint (ms)
* @param {number|null} cls - Cumulative Layout Shift
* @param {number|null} inp - Interaction to Next Paint (ms)
* @returns {'good'|'needsImprovement'|'poor'|null}
*/
export function categorizeUrl(lcp, cls, inp) {
const hasLcp = lcp !== null && lcp !== undefined;
const hasCls = cls !== null && cls !== undefined;
const hasInp = inp !== null && inp !== undefined;

if (!hasLcp && !hasCls && !hasInp) return null;

const isPoor = (hasLcp && lcp > CWV_THRESHOLDS.LCP.POOR)
|| (hasCls && cls > CWV_THRESHOLDS.CLS.POOR)
|| (hasInp && inp > CWV_THRESHOLDS.INP.POOR);
if (isPoor) return 'poor';

const isGood = (!hasLcp || lcp <= CWV_THRESHOLDS.LCP.GOOD)
&& (!hasCls || cls <= CWV_THRESHOLDS.CLS.GOOD)
&& (!hasInp || inp <= CWV_THRESHOLDS.INP.GOOD);
if (isGood) return 'good';

return 'needsImprovement';
}
75 changes: 75 additions & 0 deletions src/cwv-trends-audit/data-reader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* 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 { getObjectFromKey } from '../utils/s3-utils.js';
import { S3_BASE_PATH } from './constants.js';

export function formatDate(date) {
return date.toISOString().split('T')[0];
}

export function subtractDays(date, days) {
const result = new Date(date);
result.setDate(result.getDate() - days);
return result;
}

function buildS3Key(dateStr) {
return `${S3_BASE_PATH}/cwv-trends-daily-${dateStr}/cwv-trends-daily-${dateStr}.json`;
}

/**
* Reads CWV trend data from S3 for a given number of days ending on endDate.
* Fetches all dates in parallel and skips missing ones gracefully.
*
* @param {object} s3Client - AWS S3 client
* @param {string} bucketName - S3 bucket name
* @param {Date} endDate - End date (inclusive)
* @param {number} days - Number of days to read
* @param {object} log - Logger instance
* @returns {Promise<Array<{date: string, data: Array}>>} Daily data sorted chronologically
*/
export async function readTrendData(s3Client, bucketName, endDate, days, log) {
const promises = [];

for (let i = days - 1; i >= 0; i -= 1) {
const date = subtractDays(endDate, i);
const dateStr = formatDate(date);
const key = buildS3Key(dateStr);

promises.push(
getObjectFromKey(s3Client, bucketName, key, log)
.then((raw) => {
let parsed = raw;
if (typeof raw === 'string') {
try {
parsed = JSON.parse(raw);
} catch {
parsed = null;
}
}
if (parsed && Array.isArray(parsed)) {
return { date: dateStr, data: parsed };
}
log.warn(`Empty or invalid S3 data for date ${dateStr}`);
return null;
})
.catch(() => {
log.warn(`Missing S3 data for date ${dateStr}, skipping`);
return null;
}),
);
}

const results = await Promise.all(promises);
return results.filter(Boolean);
}
22 changes: 22 additions & 0 deletions src/cwv-trends-audit/handler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* 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 { AuditBuilder } from '../common/audit-builder.js';
import { noopUrlResolver } from '../common/index.js';
import cwvTrendsRunner from './lib.js';
import opportunityHandler from './opportunity-handler.js';

export default new AuditBuilder()
.withRunner(cwvTrendsRunner)
.withUrlResolver(noopUrlResolver)
.withPostProcessors([opportunityHandler])
.build();
Loading
Loading