@lsendel/claude-agents
Version:
Supercharge Claude Code with specialized AI sub-agents for code review, testing, debugging, documentation & more. Now with process & standards management! Easy CLI tool to install, manage & create custom AI agents for enhanced development workflow
1,833 lines (1,600 loc) • 53.7 kB
Markdown
---
name: post-deployment-automation
type: process
version: 1.0.0
description: Fully automated post-deployment validation process with Lighthouse, Puppeteer, GPT analysis, and Slack reporting
author: Claude
tags: [deployment, automation, testing, monitoring, performance]
related_commands: [/deploy-validate, /performance-check]
---
# Post-Deployment Automation Process
> Version: 1.0.0
> Last updated: 2025-07-29
> Purpose: Automated quality assurance pipeline triggered after every deployment
> Output: Comprehensive Slack dashboard with performance, UX, and accessibility metrics
## Overview
This process automatically validates deployments through multiple quality checks:
1. Lighthouse performance audits
2. Puppeteer functional tests
3. GPT-powered visual analysis
4. Comprehensive error detection
5. Aggregated Slack reporting
## Related Standards
This automation process validates against:
- **[testing-standards.md](../standards/testing-standards.md)** - For automated test coverage requirements
- **[ui-design-guide.md](../standards/ui-design-guide.md)** - For UI/UX validation criteria
- **[best-practices.md](../standards/best-practices.md)** - For performance benchmarks
- **[api-design.md](../standards/api-design.md)** - For API endpoint testing
## Architecture
```mermaid
graph TD
A[Deployment Complete] --> B[Trigger Webhook]
B --> C[Post-Deploy Pipeline]
C --> D[Lighthouse Audit]
C --> E[Puppeteer Tests]
C --> F[Visual Analysis]
C --> G[Error Scanning]
D --> H[Performance Metrics]
E --> I[Functional Status]
F --> J[UX Insights]
G --> K[Error Report]
H --> L[Report Aggregator]
I --> L
J --> L
K --> L
L --> M[Slack Dashboard]
L --> N[Historical Database]
```
## Implementation
### 1. Pipeline Orchestrator
```typescript
// post-deploy-pipeline.ts
import { LighthouseAuditor } from './auditors/lighthouse';
import { PuppeteerTester } from './testers/puppeteer';
import { VisualAnalyzer } from './analyzers/visual';
import { ErrorScanner } from './scanners/errors';
import { SlackReporter } from './reporters/slack';
import { MetricsDatabase } from './storage/metrics';
interface DeploymentInfo {
deploymentId: string;
environment: 'production' | 'staging' | 'preview';
deployedAt: Date;
commitHash: string;
deployedBy: string;
baseUrl: string;
}
export class PostDeploymentPipeline {
private lighthouse: LighthouseAuditor;
private puppeteer: PuppeteerTester;
private visual: VisualAnalyzer;
private errorScanner: ErrorScanner;
private slack: SlackReporter;
private db: MetricsDatabase;
constructor(config: PipelineConfig) {
this.lighthouse = new LighthouseAuditor(config.lighthouse);
this.puppeteer = new PuppeteerTester(config.puppeteer);
this.visual = new VisualAnalyzer(config.visual);
this.errorScanner = new ErrorScanner(config.errors);
this.slack = new SlackReporter(config.slack);
this.db = new MetricsDatabase(config.database);
}
async execute(deployment: DeploymentInfo): Promise<void> {
console.log(`🚀 Starting post-deployment validation for ${deployment.deploymentId}`);
const startTime = Date.now();
const results = {
deployment,
lighthouse: null,
puppeteer: null,
visual: null,
errors: null,
duration: 0
};
try {
// Run all checks in parallel for speed
const [lighthouseResults, puppeteerResults, visualResults, errorResults] =
await Promise.allSettled([
this.lighthouse.audit(deployment.baseUrl),
this.puppeteer.test(deployment.baseUrl),
this.visual.analyze(deployment.baseUrl),
this.errorScanner.scan(deployment.baseUrl)
]);
// Process results
results.lighthouse = this.processResult(lighthouseResults);
results.puppeteer = this.processResult(puppeteerResults);
results.visual = this.processResult(visualResults);
results.errors = this.processResult(errorResults);
results.duration = Date.now() - startTime;
// Store in database
await this.db.store(results);
// Send to Slack
await this.slack.report(results);
console.log(`✅ Post-deployment validation completed in ${results.duration}ms`);
} catch (error) {
console.error('❌ Pipeline failed:', error);
await this.slack.reportError(deployment, error);
}
}
private processResult(result: PromiseSettledResult<any>): any {
if (result.status === 'fulfilled') {
return result.value;
} else {
return { error: result.reason.message };
}
}
}
```
### 2. Lighthouse Auditor
```typescript
// auditors/lighthouse.ts
import lighthouse from 'lighthouse';
import * as chromeLauncher from 'chrome-launcher';
export interface LighthouseConfig {
categories: string[];
throttling: 'mobile' | 'desktop';
pages: PageConfig[];
}
export interface PageConfig {
path: string;
name: string;
waitForSelector?: string;
}
export interface LighthouseResults {
summary: {
performance: number;
accessibility: number;
bestPractices: number;
seo: number;
pwa: number;
};
pages: PageAudit[];
insights: string[];
}
interface PageAudit {
path: string;
name: string;
scores: {
performance: number;
accessibility: number;
bestPractices: number;
seo: number;
pwa: number;
};
metrics: {
firstContentfulPaint: number;
largestContentfulPaint: number;
totalBlockingTime: number;
cumulativeLayoutShift: number;
speedIndex: number;
};
opportunities: Opportunity[];
diagnostics: Diagnostic[];
}
export class LighthouseAuditor {
constructor(private config: LighthouseConfig) {}
async audit(baseUrl: string): Promise<LighthouseResults> {
const chrome = await chromeLauncher.launch({
chromeFlags: ['--headless', '--no-sandbox']
});
try {
const results: PageAudit[] = [];
for (const page of this.config.pages) {
const url = `${baseUrl}${page.path}`;
console.log(`🔍 Auditing ${page.name} at ${url}`);
const options = {
logLevel: 'error',
output: 'json',
onlyCategories: this.config.categories,
port: chrome.port,
throttling: this.getThrottlingConfig()
};
const runnerResult = await lighthouse(url, options);
const audit = this.processAuditResult(page, runnerResult.lhr);
results.push(audit);
}
return {
summary: this.calculateSummary(results),
pages: results,
insights: this.generateInsights(results)
};
} finally {
await chrome.kill();
}
}
private processAuditResult(page: PageConfig, lhr: any): PageAudit {
return {
path: page.path,
name: page.name,
scores: {
performance: Math.round(lhr.categories.performance.score * 100),
accessibility: Math.round(lhr.categories.accessibility.score * 100),
bestPractices: Math.round(lhr.categories['best-practices'].score * 100),
seo: Math.round(lhr.categories.seo.score * 100),
pwa: Math.round(lhr.categories.pwa?.score * 100 || 0)
},
metrics: {
firstContentfulPaint: lhr.audits['first-contentful-paint'].numericValue,
largestContentfulPaint: lhr.audits['largest-contentful-paint'].numericValue,
totalBlockingTime: lhr.audits['total-blocking-time'].numericValue,
cumulativeLayoutShift: lhr.audits['cumulative-layout-shift'].numericValue,
speedIndex: lhr.audits['speed-index'].numericValue
},
opportunities: this.extractOpportunities(lhr),
diagnostics: this.extractDiagnostics(lhr)
};
}
private extractOpportunities(lhr: any): Opportunity[] {
return Object.values(lhr.audits)
.filter((audit: any) =>
audit.details?.type === 'opportunity' &&
audit.score !== null &&
audit.score < 0.9
)
.map((audit: any) => ({
title: audit.title,
description: audit.description,
savings: audit.details.overallSavingsMs
}))
.sort((a, b) => b.savings - a.savings)
.slice(0, 5);
}
private getThrottlingConfig() {
return this.config.throttling === 'mobile'
? lighthouse.constants.throttling.mobileSlow4G
: lighthouse.constants.throttling.desktopDense4G;
}
private calculateSummary(results: PageAudit[]): LighthouseResults['summary'] {
const avg = (key: keyof PageAudit['scores']) =>
Math.round(results.reduce((sum, r) => sum + r.scores[key], 0) / results.length);
return {
performance: avg('performance'),
accessibility: avg('accessibility'),
bestPractices: avg('bestPractices'),
seo: avg('seo'),
pwa: avg('pwa')
};
}
private generateInsights(results: PageAudit[]): string[] {
const insights: string[] = [];
// Performance insights
const avgPerf = results.reduce((sum, r) => sum + r.scores.performance, 0) / results.length;
if (avgPerf < 50) {
insights.push('⚠️ Critical performance issues detected across multiple pages');
} else if (avgPerf < 75) {
insights.push('🟡 Performance could be improved for better user experience');
} else {
insights.push('✅ Performance scores are good across all pages');
}
// Accessibility insights
const a11yIssues = results.filter(r => r.scores.accessibility < 90);
if (a11yIssues.length > 0) {
insights.push(`🔴 ${a11yIssues.length} pages have accessibility issues`);
}
return insights;
}
}
```
### 3. Puppeteer Test Suite
```typescript
// testers/puppeteer.ts
import puppeteer, { Browser, Page } from 'puppeteer';
export interface PuppeteerConfig {
viewport: { width: number; height: number };
timeout: number;
tests: TestScenario[];
}
export interface TestScenario {
name: string;
path: string;
actions: TestAction[];
assertions: TestAssertion[];
screenshot: boolean;
}
type TestAction =
| { type: 'click'; selector: string }
| { type: 'type'; selector: string; text: string }
| { type: 'wait'; selector: string }
| { type: 'navigate'; path: string }
| { type: 'scroll'; x: number; y: number };
type TestAssertion =
| { type: 'element-exists'; selector: string }
| { type: 'text-contains'; selector: string; text: string }
| { type: 'url-matches'; pattern: string }
| { type: 'no-console-errors' };
export interface PuppeteerResults {
passed: number;
failed: number;
tests: TestResult[];
screenshots: Screenshot[];
criticalPaths: CriticalPathStatus[];
}
interface TestResult {
name: string;
status: 'passed' | 'failed';
duration: number;
error?: string;
screenshot?: string;
}
interface Screenshot {
name: string;
path: string;
base64: string;
}
interface CriticalPathStatus {
name: string;
path: string;
status: 'working' | 'broken';
issue?: string;
}
export class PuppeteerTester {
private browser: Browser | null = null;
constructor(private config: PuppeteerConfig) {}
async test(baseUrl: string): Promise<PuppeteerResults> {
this.browser = await puppeteer.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox']
});
const results: TestResult[] = [];
const screenshots: Screenshot[] = [];
const criticalPaths: CriticalPathStatus[] = [];
try {
for (const scenario of this.config.tests) {
const result = await this.runScenario(baseUrl, scenario, screenshots);
results.push(result);
// Track critical paths
if (scenario.name.includes('critical')) {
criticalPaths.push({
name: scenario.name,
path: scenario.path,
status: result.status === 'passed' ? 'working' : 'broken',
issue: result.error
});
}
}
return {
passed: results.filter(r => r.status === 'passed').length,
failed: results.filter(r => r.status === 'failed').length,
tests: results,
screenshots,
criticalPaths
};
} finally {
await this.browser.close();
}
}
private async runScenario(
baseUrl: string,
scenario: TestScenario,
screenshots: Screenshot[]
): Promise<TestResult> {
const page = await this.browser!.newPage();
await page.setViewport(this.config.viewport);
const startTime = Date.now();
const consoleErrors: string[] = [];
// Track console errors
page.on('console', msg => {
if (msg.type() === 'error') {
consoleErrors.push(msg.text());
}
});
try {
// Navigate to starting page
await page.goto(`${baseUrl}${scenario.path}`, {
waitUntil: 'networkidle2',
timeout: this.config.timeout
});
// Execute actions
for (const action of scenario.actions) {
await this.executeAction(page, action);
}
// Run assertions
for (const assertion of scenario.assertions) {
await this.runAssertion(page, assertion, consoleErrors);
}
// Take screenshot if requested
if (scenario.screenshot) {
const screenshotBuffer = await page.screenshot({ fullPage: true });
screenshots.push({
name: scenario.name,
path: scenario.path,
base64: screenshotBuffer.toString('base64')
});
}
return {
name: scenario.name,
status: 'passed',
duration: Date.now() - startTime
};
} catch (error) {
// Take error screenshot
try {
const errorScreenshot = await page.screenshot({ fullPage: true });
return {
name: scenario.name,
status: 'failed',
duration: Date.now() - startTime,
error: error.message,
screenshot: errorScreenshot.toString('base64')
};
} catch {
return {
name: scenario.name,
status: 'failed',
duration: Date.now() - startTime,
error: error.message
};
}
} finally {
await page.close();
}
}
private async executeAction(page: Page, action: TestAction): Promise<void> {
switch (action.type) {
case 'click':
await page.waitForSelector(action.selector, { timeout: this.config.timeout });
await page.click(action.selector);
break;
case 'type':
await page.waitForSelector(action.selector, { timeout: this.config.timeout });
await page.type(action.selector, action.text);
break;
case 'wait':
await page.waitForSelector(action.selector, { timeout: this.config.timeout });
break;
case 'navigate':
await page.goto(action.path, { waitUntil: 'networkidle2' });
break;
case 'scroll':
await page.evaluate((x, y) => window.scrollTo(x, y), action.x, action.y);
break;
}
}
private async runAssertion(
page: Page,
assertion: TestAssertion,
consoleErrors: string[]
): Promise<void> {
switch (assertion.type) {
case 'element-exists':
const element = await page.$(assertion.selector);
if (!element) {
throw new Error(`Element not found: ${assertion.selector}`);
}
break;
case 'text-contains':
const text = await page.$eval(assertion.selector, el => el.textContent);
if (!text?.includes(assertion.text)) {
throw new Error(`Text "${assertion.text}" not found in ${assertion.selector}`);
}
break;
case 'url-matches':
const url = page.url();
if (!url.match(new RegExp(assertion.pattern))) {
throw new Error(`URL ${url} doesn't match pattern ${assertion.pattern}`);
}
break;
case 'no-console-errors':
if (consoleErrors.length > 0) {
throw new Error(`Console errors found: ${consoleErrors.join(', ')}`);
}
break;
}
}
}
```
### 4. GPT Visual Analyzer
```typescript
// analyzers/visual.ts
import { OpenAI } from 'openai';
import puppeteer from 'puppeteer';
export interface VisualAnalyzerConfig {
apiKey: string;
model: string;
pages: VisualPage[];
aspectsToAnalyze: AnalysisAspect[];
}
interface VisualPage {
path: string;
name: string;
waitForSelector?: string;
}
type AnalysisAspect =
| 'layout-consistency'
| 'color-contrast'
| 'visual-hierarchy'
| 'mobile-responsiveness'
| 'loading-state'
| 'error-states'
| 'accessibility-visual';
export interface VisualAnalysisResults {
overall: {
score: number;
grade: 'A' | 'B' | 'C' | 'D' | 'F';
summary: string;
};
pages: PageAnalysis[];
recommendations: UXRecommendation[];
accessibilityIssues: AccessibilityIssue[];
}
interface PageAnalysis {
path: string;
name: string;
screenshot: string;
analysis: {
layoutScore: number;
visualHierarchy: string;
colorContrast: string;
responsiveness: string;
issues: string[];
suggestions: string[];
};
}
interface UXRecommendation {
priority: 'high' | 'medium' | 'low';
category: string;
issue: string;
suggestion: string;
affectedPages: string[];
}
interface AccessibilityIssue {
severity: 'critical' | 'major' | 'minor';
type: string;
description: string;
pages: string[];
}
export class VisualAnalyzer {
private openai: OpenAI;
constructor(private config: VisualAnalyzerConfig) {
this.openai = new OpenAI({ apiKey: config.apiKey });
}
async analyze(baseUrl: string): Promise<VisualAnalysisResults> {
const browser = await puppeteer.launch({
headless: true,
args: ['--no-sandbox']
});
try {
const pageAnalyses: PageAnalysis[] = [];
// Capture screenshots and analyze each page
for (const pageConfig of this.config.pages) {
const analysis = await this.analyzePage(browser, baseUrl, pageConfig);
pageAnalyses.push(analysis);
}
// Generate overall analysis
const overallAnalysis = await this.generateOverallAnalysis(pageAnalyses);
return overallAnalysis;
} finally {
await browser.close();
}
}
private async analyzePage(
browser: any,
baseUrl: string,
pageConfig: VisualPage
): Promise<PageAnalysis> {
const page = await browser.newPage();
// Set multiple viewport sizes for responsive analysis
const viewports = [
{ width: 1920, height: 1080, name: 'desktop' },
{ width: 768, height: 1024, name: 'tablet' },
{ width: 375, height: 667, name: 'mobile' }
];
const screenshots: { [key: string]: string } = {};
for (const viewport of viewports) {
await page.setViewport(viewport);
await page.goto(`${baseUrl}${pageConfig.path}`, {
waitUntil: 'networkidle2'
});
if (pageConfig.waitForSelector) {
await page.waitForSelector(pageConfig.waitForSelector);
}
const screenshot = await page.screenshot({
fullPage: true,
encoding: 'base64'
});
screenshots[viewport.name] = screenshot;
}
await page.close();
// Analyze with GPT-4 Vision
const analysis = await this.analyzeWithGPT(pageConfig, screenshots);
return {
path: pageConfig.path,
name: pageConfig.name,
screenshot: screenshots.desktop, // Primary screenshot
analysis
};
}
private async analyzeWithGPT(
pageConfig: VisualPage,
screenshots: { [key: string]: string }
): Promise<PageAnalysis['analysis']> {
const prompt = `
Analyze these screenshots of a web page across different devices.
Page: ${pageConfig.name}
Please evaluate:
1. Layout consistency across devices
2. Visual hierarchy and information flow
3. Color contrast and readability
4. Mobile responsiveness issues
5. Any visual bugs or misalignments
6. Accessibility concerns visible in the UI
Provide:
- Layout score (0-100)
- Specific issues found
- Actionable suggestions for improvement
Format your response as JSON.
`;
const messages = [
{
role: 'user' as const,
content: [
{ type: 'text' as const, text: prompt },
...Object.entries(screenshots).map(([device, screenshot]) => ({
type: 'image_url' as const,
image_url: {
url: `data:image/png;base64,${screenshot}`,
detail: 'high' as const
}
}))
]
}
];
const response = await this.openai.chat.completions.create({
model: this.config.model,
messages,
max_tokens: 1000,
response_format: { type: 'json_object' }
});
const result = JSON.parse(response.choices[0].message.content || '{}');
return {
layoutScore: result.layoutScore || 0,
visualHierarchy: result.visualHierarchy || 'Not analyzed',
colorContrast: result.colorContrast || 'Not analyzed',
responsiveness: result.responsiveness || 'Not analyzed',
issues: result.issues || [],
suggestions: result.suggestions || []
};
}
private async generateOverallAnalysis(
pageAnalyses: PageAnalysis[]
): Promise<VisualAnalysisResults> {
// Aggregate scores
const avgScore = pageAnalyses.reduce(
(sum, p) => sum + p.analysis.layoutScore, 0
) / pageAnalyses.length;
const grade = this.scoreToGrade(avgScore);
// Collect all issues and suggestions
const allIssues = pageAnalyses.flatMap(p =>
p.analysis.issues.map(issue => ({ issue, page: p.name }))
);
const recommendations = this.generateRecommendations(pageAnalyses);
const accessibilityIssues = this.extractAccessibilityIssues(pageAnalyses);
return {
overall: {
score: Math.round(avgScore),
grade,
summary: this.generateSummary(avgScore, pageAnalyses)
},
pages: pageAnalyses,
recommendations,
accessibilityIssues
};
}
private scoreToGrade(score: number): 'A' | 'B' | 'C' | 'D' | 'F' {
if (score >= 90) return 'A';
if (score >= 80) return 'B';
if (score >= 70) return 'C';
if (score >= 60) return 'D';
return 'F';
}
private generateSummary(score: number, analyses: PageAnalysis[]): string {
const issueCount = analyses.reduce(
(sum, p) => sum + p.analysis.issues.length, 0
);
if (score >= 90) {
return `Excellent visual quality across all ${analyses.length} pages with minimal issues.`;
} else if (score >= 70) {
return `Good visual quality with ${issueCount} minor issues to address.`;
} else {
return `Significant visual issues detected. ${issueCount} problems need attention.`;
}
}
private generateRecommendations(analyses: PageAnalysis[]): UXRecommendation[] {
const recommendations: UXRecommendation[] = [];
// Analyze common issues across pages
const issueFrequency = new Map<string, string[]>();
analyses.forEach(page => {
page.analysis.issues.forEach(issue => {
if (!issueFrequency.has(issue)) {
issueFrequency.set(issue, []);
}
issueFrequency.get(issue)!.push(page.name);
});
});
// Create recommendations for frequent issues
issueFrequency.forEach((pages, issue) => {
const priority = pages.length > analyses.length / 2 ? 'high' :
pages.length > 1 ? 'medium' : 'low';
recommendations.push({
priority,
category: this.categorizeIssue(issue),
issue,
suggestion: this.generateSuggestion(issue),
affectedPages: pages
});
});
return recommendations.sort((a, b) => {
const priorityOrder = { high: 0, medium: 1, low: 2 };
return priorityOrder[a.priority] - priorityOrder[b.priority];
});
}
private categorizeIssue(issue: string): string {
if (issue.includes('contrast') || issue.includes('color')) return 'Color & Contrast';
if (issue.includes('mobile') || issue.includes('responsive')) return 'Responsiveness';
if (issue.includes('layout') || issue.includes('alignment')) return 'Layout';
if (issue.includes('text') || issue.includes('font')) return 'Typography';
return 'General';
}
private generateSuggestion(issue: string): string {
// Map common issues to suggestions
const suggestionMap: { [key: string]: string } = {
'contrast': 'Increase color contrast to meet WCAG AA standards (4.5:1 for normal text)',
'mobile': 'Adjust layout for mobile devices using responsive design techniques',
'alignment': 'Use consistent spacing and alignment grid system',
'hierarchy': 'Establish clear visual hierarchy with size, weight, and spacing'
};
for (const [key, suggestion] of Object.entries(suggestionMap)) {
if (issue.toLowerCase().includes(key)) {
return suggestion;
}
}
return 'Review and fix the identified issue';
}
private extractAccessibilityIssues(analyses: PageAnalysis[]): AccessibilityIssue[] {
const issues: AccessibilityIssue[] = [];
analyses.forEach(page => {
page.analysis.issues.forEach(issue => {
if (this.isAccessibilityIssue(issue)) {
const existingIssue = issues.find(i => i.description === issue);
if (existingIssue) {
existingIssue.pages.push(page.name);
} else {
issues.push({
severity: this.getAccessibilitySeverity(issue),
type: this.getAccessibilityType(issue),
description: issue,
pages: [page.name]
});
}
}
});
});
return issues;
}
private isAccessibilityIssue(issue: string): boolean {
const a11yKeywords = ['contrast', 'aria', 'alt', 'focus', 'keyboard', 'screen reader'];
return a11yKeywords.some(keyword => issue.toLowerCase().includes(keyword));
}
private getAccessibilitySeverity(issue: string): 'critical' | 'major' | 'minor' {
if (issue.includes('contrast') && issue.includes('fail')) return 'critical';
if (issue.includes('keyboard') || issue.includes('screen reader')) return 'major';
return 'minor';
}
private getAccessibilityType(issue: string): string {
if (issue.includes('contrast')) return 'Color Contrast';
if (issue.includes('keyboard')) return 'Keyboard Navigation';
if (issue.includes('aria')) return 'ARIA Labels';
if (issue.includes('alt')) return 'Alternative Text';
return 'General Accessibility';
}
}
```
### 5. Error Scanner
```typescript
// scanners/errors.ts
import puppeteer from 'puppeteer';
import { SourceMapConsumer } from 'source-map';
export interface ErrorScannerConfig {
pages: string[];
errorTypes: ErrorType[];
timeout: number;
}
type ErrorType =
| 'javascript-errors'
| 'network-errors'
| '404-errors'
| 'mixed-content'
| 'cors-errors'
| 'csp-violations';
export interface ErrorScanResults {
summary: {
totalErrors: number;
criticalErrors: number;
byType: { [key: string]: number };
};
errors: DetailedError[];
brokenLinks: BrokenLink[];
securityIssues: SecurityIssue[];
}
interface DetailedError {
type: ErrorType;
severity: 'critical' | 'warning' | 'info';
page: string;
message: string;
stack?: string;
timestamp: string;
context?: any;
}
interface BrokenLink {
page: string;
url: string;
statusCode: number;
element: string;
}
interface SecurityIssue {
type: string;
severity: 'high' | 'medium' | 'low';
description: string;
pages: string[];
}
export class ErrorScanner {
constructor(private config: ErrorScannerConfig) {}
async scan(baseUrl: string): Promise<ErrorScanResults> {
const browser = await puppeteer.launch({
headless: true,
args: ['--no-sandbox']
});
const errors: DetailedError[] = [];
const brokenLinks: BrokenLink[] = [];
const securityIssues: SecurityIssue[] = [];
try {
for (const pagePath of this.config.pages) {
await this.scanPage(
browser,
`${baseUrl}${pagePath}`,
pagePath,
errors,
brokenLinks,
securityIssues
);
}
return {
summary: this.generateSummary(errors),
errors,
brokenLinks,
securityIssues
};
} finally {
await browser.close();
}
}
private async scanPage(
browser: any,
url: string,
pagePath: string,
errors: DetailedError[],
brokenLinks: BrokenLink[],
securityIssues: SecurityIssue[]
): Promise<void> {
const page = await browser.newPage();
// Set up error tracking
const pageErrors: DetailedError[] = [];
const networkErrors: Map<string, number> = new Map();
// JavaScript errors
page.on('pageerror', (error: Error) => {
pageErrors.push({
type: 'javascript-errors',
severity: 'critical',
page: pagePath,
message: error.message,
stack: error.stack,
timestamp: new Date().toISOString()
});
});
// Console messages
page.on('console', (msg: any) => {
if (msg.type() === 'error') {
pageErrors.push({
type: 'javascript-errors',
severity: 'warning',
page: pagePath,
message: msg.text(),
timestamp: new Date().toISOString()
});
}
});
// Network errors
page.on('response', (response: any) => {
const status = response.status();
const url = response.url();
if (status >= 400) {
networkErrors.set(url, status);
if (status === 404) {
pageErrors.push({
type: '404-errors',
severity: 'warning',
page: pagePath,
message: `404 Not Found: ${url}`,
timestamp: new Date().toISOString()
});
}
}
// Check for mixed content
if (url.startsWith('http://') && !url.startsWith('http://localhost')) {
pageErrors.push({
type: 'mixed-content',
severity: 'critical',
page: pagePath,
message: `Mixed content: Loading HTTP resource on HTTPS page: ${url}`,
timestamp: new Date().toISOString()
});
}
});
// CORS errors
page.on('requestfailed', (request: any) => {
const failure = request.failure();
if (failure && failure.errorText.includes('CORS')) {
pageErrors.push({
type: 'cors-errors',
severity: 'critical',
page: pagePath,
message: `CORS error: ${request.url()}`,
timestamp: new Date().toISOString()
});
}
});
try {
await page.goto(url, {
waitUntil: 'networkidle2',
timeout: this.config.timeout
});
// Scan for broken links
const links = await page.$$eval('a[href]', (links: any[]) =>
links.map(link => ({
href: link.href,
text: link.textContent,
selector: link.tagName.toLowerCase() +
(link.id ? `#${link.id}` : '') +
(link.className ? `.${link.className.split(' ').join('.')}` : '')
}))
);
// Check each link
for (const link of links) {
if (networkErrors.has(link.href)) {
brokenLinks.push({
page: pagePath,
url: link.href,
statusCode: networkErrors.get(link.href)!,
element: link.selector
});
}
}
// Security checks
await this.performSecurityChecks(page, pagePath, securityIssues);
// Add collected errors
errors.push(...pageErrors);
} catch (error) {
errors.push({
type: 'javascript-errors',
severity: 'critical',
page: pagePath,
message: `Page load failed: ${error.message}`,
timestamp: new Date().toISOString()
});
} finally {
await page.close();
}
}
private async performSecurityChecks(
page: any,
pagePath: string,
securityIssues: SecurityIssue[]
): Promise<void> {
// Check for exposed sensitive data
const pageContent = await page.content();
// API keys
const apiKeyPattern = /(?:api[_-]?key|apikey)["\s]*[:=]["\s]*["']([^"']+)["']/gi;
if (apiKeyPattern.test(pageContent)) {
this.addSecurityIssue(securityIssues, {
type: 'exposed-api-key',
severity: 'high',
description: 'Potential API key exposed in client-side code',
pages: [pagePath]
});
}
// Check for insecure forms
const forms = await page.$$eval('form', (forms: any[]) =>
forms.map(form => ({
action: form.action,
method: form.method,
hasPassword: !!form.querySelector('input[type="password"]')
}))
);
forms.forEach(form => {
if (form.hasPassword && !form.action.startsWith('https://')) {
this.addSecurityIssue(securityIssues, {
type: 'insecure-form',
severity: 'high',
description: 'Password form submitted over non-HTTPS connection',
pages: [pagePath]
});
}
});
// Check CSP
const cspHeader = await page.evaluate(() => {
const meta = document.querySelector('meta[http-equiv="Content-Security-Policy"]');
return meta?.getAttribute('content');
});
if (!cspHeader) {
this.addSecurityIssue(securityIssues, {
type: 'missing-csp',
severity: 'medium',
description: 'Content Security Policy not implemented',
pages: [pagePath]
});
}
}
private addSecurityIssue(
issues: SecurityIssue[],
newIssue: SecurityIssue
): void {
const existing = issues.find(i =>
i.type === newIssue.type &&
i.description === newIssue.description
);
if (existing) {
existing.pages.push(...newIssue.pages);
} else {
issues.push(newIssue);
}
}
private generateSummary(errors: DetailedError[]): ErrorScanResults['summary'] {
const byType: { [key: string]: number } = {};
errors.forEach(error => {
byType[error.type] = (byType[error.type] || 0) + 1;
});
return {
totalErrors: errors.length,
criticalErrors: errors.filter(e => e.severity === 'critical').length,
byType
};
}
}
```
### 6. Slack Reporter
```typescript
// reporters/slack.ts
import { WebClient } from '@slack/web-api';
export interface SlackConfig {
token: string;
channel: string;
mentionOnFailure: string[];
webhookUrl?: string;
}
export class SlackReporter {
private client: WebClient;
constructor(private config: SlackConfig) {
this.client = new WebClient(config.token);
}
async report(results: any): Promise<void> {
const message = this.buildDashboardMessage(results);
try {
await this.client.chat.postMessage({
channel: this.config.channel,
blocks: message.blocks,
attachments: message.attachments,
text: message.text
});
} catch (error) {
console.error('Failed to send Slack message:', error);
}
}
private buildDashboardMessage(results: any): any {
const { deployment, lighthouse, puppeteer, visual, errors } = results;
const overallStatus = this.calculateOverallStatus(results);
const blocks = [
// Header
{
type: 'header',
text: {
type: 'plain_text',
text: `🚀 Deployment Report: ${deployment.environment}`,
emoji: true
}
},
// Deployment info
{
type: 'section',
fields: [
{
type: 'mrkdwn',
text: `*Environment:*\n${deployment.environment}`
},
{
type: 'mrkdwn',
text: `*Deploy Time:*\n${new Date(deployment.deployedAt).toLocaleString()}`
},
{
type: 'mrkdwn',
text: `*Deployed By:*\n${deployment.deployedBy}`
},
{
type: 'mrkdwn',
text: `*Commit:*\n${deployment.commitHash.substring(0, 8)}`
}
]
},
// Overall status
{
type: 'section',
text: {
type: 'mrkdwn',
text: `*Overall Status:* ${overallStatus.emoji} ${overallStatus.text}`
}
},
// Lighthouse results
this.buildLighthouseBlock(lighthouse),
// Puppeteer results
this.buildPuppeteerBlock(puppeteer),
// Visual analysis results
this.buildVisualBlock(visual),
// Error scan results
this.buildErrorBlock(errors),
// Actions
{
type: 'actions',
elements: [
{
type: 'button',
text: {
type: 'plain_text',
text: 'View Full Report'
},
url: `${deployment.baseUrl}/deployment-report/${deployment.deploymentId}`,
style: 'primary'
},
{
type: 'button',
text: {
type: 'plain_text',
text: 'View Lighthouse Details'
},
url: `${deployment.baseUrl}/lighthouse-report/${deployment.deploymentId}`
},
{
type: 'button',
text: {
type: 'plain_text',
text: 'Rollback'
},
style: 'danger',
confirm: {
title: {
type: 'plain_text',
text: 'Confirm Rollback'
},
text: {
type: 'plain_text',
text: 'Are you sure you want to rollback this deployment?'
},
confirm: {
type: 'plain_text',
text: 'Rollback'
},
deny: {
type: 'plain_text',
text: 'Cancel'
}
}
}
]
}
];
// Add mention if critical issues
if (overallStatus.severity === 'critical') {
blocks.push({
type: 'section',
text: {
type: 'mrkdwn',
text: `🚨 ${this.config.mentionOnFailure.map(u => `<@${u}>`).join(' ')} Critical issues detected!`
}
});
}
return {
blocks,
text: `Deployment Report: ${overallStatus.text}`,
attachments: this.buildAttachments(results)
};
}
private buildLighthouseBlock(lighthouse: any): any {
if (!lighthouse || lighthouse.error) {
return {
type: 'section',
text: {
type: 'mrkdwn',
text: `*🏗 Lighthouse:* ❌ Failed to run audits`
}
};
}
const { summary } = lighthouse;
const getScoreEmoji = (score: number) => {
if (score >= 90) return '🟢';
if (score >= 70) return '🟡';
return '🔴';
};
return {
type: 'section',
text: {
type: 'mrkdwn',
text: `*🏗 Lighthouse Performance Audit*`
},
fields: [
{
type: 'mrkdwn',
text: `${getScoreEmoji(summary.performance)} Performance: ${summary.performance}`
},
{
type: 'mrkdwn',
text: `${getScoreEmoji(summary.accessibility)} Accessibility: ${summary.accessibility}`
},
{
type: 'mrkdwn',
text: `${getScoreEmoji(summary.bestPractices)} Best Practices: ${summary.bestPractices}`
},
{
type: 'mrkdwn',
text: `${getScoreEmoji(summary.seo)} SEO: ${summary.seo}`
}
],
accessory: {
type: 'image',
image_url: this.generateScoreChart(summary),
alt_text: 'Lighthouse scores'
}
};
}
private buildPuppeteerBlock(puppeteer: any): any {
if (!puppeteer || puppeteer.error) {
return {
type: 'section',
text: {
type: 'mrkdwn',
text: `*🎭 Functional Tests:* ❌ Failed to run tests`
}
};
}
const criticalFailing = puppeteer.criticalPaths.filter((p: any) => p.status === 'broken');
const statusEmoji = puppeteer.failed === 0 ? '✅' :
criticalFailing.length > 0 ? '🔴' : '🟡';
return {
type: 'section',
text: {
type: 'mrkdwn',
text: `*🎭 Functional Tests:* ${statusEmoji} ${puppeteer.passed}/${puppeteer.passed + puppeteer.failed} passed`
},
fields: criticalFailing.length > 0 ? [
{
type: 'mrkdwn',
text: `*Critical Paths Broken:*\n${criticalFailing.map((p: any) => `• ${p.name}`).join('\n')}`
}
] : undefined
};
}
private buildVisualBlock(visual: any): any {
if (!visual || visual.error) {
return {
type: 'section',
text: {
type: 'mrkdwn',
text: `*👁 Visual Analysis:* ❌ Failed to analyze`
}
};
}
const { overall, recommendations, accessibilityIssues } = visual;
const gradeEmoji = {
'A': '🟢',
'B': '🟡',
'C': '🟠',
'D': '🔴',
'F': '🔴'
};
const highPriorityRecs = recommendations.filter((r: any) => r.priority === 'high');
const criticalA11y = accessibilityIssues.filter((i: any) => i.severity === 'critical');
return {
type: 'section',
text: {
type: 'mrkdwn',
text: `*👁 Visual Analysis:* ${gradeEmoji[overall.grade]} Grade ${overall.grade} (${overall.score}/100)\n${overall.summary}`
},
fields: [
highPriorityRecs.length > 0 ? {
type: 'mrkdwn',
text: `*🔺 High Priority Issues:*\n${highPriorityRecs.slice(0, 3).map((r: any) => `• ${r.issue}`).join('\n')}`
} : null,
criticalA11y.length > 0 ? {
type: 'mrkdwn',
text: `*♿ Critical Accessibility:*\n${criticalA11y.map((i: any) => `• ${i.description}`).join('\n')}`
} : null
].filter(Boolean)
};
}
private buildErrorBlock(errors: any): any {
if (!errors || errors.error) {
return {
type: 'section',
text: {
type: 'mrkdwn',
text: `*🐛 Error Scan:* ❌ Failed to scan`
}
};
}
const { summary, securityIssues } = errors;
const statusEmoji = summary.criticalErrors > 0 ? '🔴' :
summary.totalErrors > 0 ? '🟡' : '✅';
const highSecurityIssues = securityIssues.filter((i: any) => i.severity === 'high');
return {
type: 'section',
text: {
type: 'mrkdwn',
text: `*🐛 Error Scan:* ${statusEmoji} ${summary.totalErrors} errors found (${summary.criticalErrors} critical)`
},
fields: [
Object.keys(summary.byType).length > 0 ? {
type: 'mrkdwn',
text: `*Error Types:*\n${Object.entries(summary.byType).map(([type, count]) => `• ${type}: ${count}`).join('\n')}`
} : null,
highSecurityIssues.length > 0 ? {
type: 'mrkdwn',
text: `*🔒 Security Issues:*\n${highSecurityIssues.map((i: any) => `• ${i.description}`).join('\n')}`
} : null
].filter(Boolean)
};
}
private buildAttachments(results: any): any[] {
const attachments = [];
// Add screenshots if visual analysis included them
if (results.visual?.pages) {
const firstPage = results.visual.pages[0];
if (firstPage?.screenshot) {
attachments.push({
title: 'Homepage Screenshot',
image_url: `data:image/png;base64,${firstPage.screenshot}`,
color: this.getStatusColor(results.visual.overall.grade)
});
}
}
return attachments;
}
private calculateOverallStatus(results: any): any {
let severity = 'success';
let text = 'All checks passed';
let emoji = '✅';
// Critical failures
if (results.puppeteer?.criticalPaths?.some((p: any) => p.status === 'broken') ||
results.errors?.summary?.criticalErrors > 0 ||
results.errors?.securityIssues?.some((i: any) => i.severity === 'high')) {
severity = 'critical';
text = 'Critical issues detected';
emoji = '🚨';
}
// Major issues
else if (results.lighthouse?.summary?.performance < 50 ||
results.lighthouse?.summary?.accessibility < 70 ||
results.visual?.overall?.grade === 'F' ||
results.puppeteer?.failed > 0) {
severity = 'warning';
text = 'Performance or quality issues detected';
emoji = '⚠️';
}
// Minor issues
else if (results.lighthouse?.summary?.performance < 75 ||
results.visual?.overall?.grade === 'C' ||
results.errors?.summary?.totalErrors > 0) {
severity = 'info';
text = 'Minor issues to address';
emoji = '💡';
}
return { severity, text, emoji };
}
private generateScoreChart(scores: any): string {
// In a real implementation, this would generate a chart image
// For now, return a placeholder
return 'https://via.placeholder.com/200x100/ffffff/000000?text=Score+Chart';
}
private getStatusColor(grade: string): string {
const colors = {
'A': '#36a64f',
'B': '#3aa3e3',
'C': '#ff9900',
'D': '#ff6600',
'F': '#ff0000'
};
return colors[grade] || '#cccccc';
}
async reportError(deployment: any, error: Error): Promise<void> {
await this.client.chat.postMessage({
channel: this.config.channel,
text: `🚨 Post-deployment validation failed for ${deployment.environment}`,
blocks: [
{
type: 'header',
text: {
type: 'plain_text',
text: '🚨 Deployment Validation Failed'
}
},
{
type: 'section',
text: {
type: 'mrkdwn',
text: `Environment: *${deployment.environment}*\nError: ${error.message}`
}
},
{
type: 'section',
text: {
type: 'mrkdwn',
text: `${this.config.mentionOnFailure.map(u => `<@${u}>`).join(' ')} Please investigate immediately.`
}
}
]
});
}
}
```
### 7. Configuration File
```yaml
# post-deploy-config.yaml
pipeline:
enabled: true
environments:
- production
- staging
webhook_secret: ${POST_DEPLOY_WEBHOOK_SECRET}
lighthouse:
categories:
- performance
- accessibility
- best-practices
- seo
throttling: mobile
pages:
- path: /
name: Homepage
- path: /products
name: Products Page
- path: /checkout
name: Checkout Flow
waitForSelector: .checkout-form
- path: /account/dashboard
name: User Dashboard
waitForSelector: .dashboard-content
puppeteer:
viewport:
width: 1920
height: 1080
timeout: 30000
tests:
- name: Critical - User Login Flow
path: /login
actions:
- type: type
selector: '#email'
text: test@example.com
- type: type
selector: '#password'
text: testpassword123
- type: click
selector: '#login-button'
- type: wait
selector: '.dashboard-content'
assertions:
- type: url-matches
pattern: /dashboard
- type: element-exists
selector: .user-profile
- type: no-console-errors
screenshot: true
- name: Critical - Checkout Process
path: /products
actions:
- type: click
selector: .product-card:first-child .add-to-cart
- type: wait
selector: .cart-notification
- type: navigate
path: /checkout
- type: wait
selector: .checkout-form
assertions:
- type: element-exists
selector: .order-summary
- type: text-contains
selector: .cart-items
text: '1 item'
screenshot: true
- name: Search Functionality
path: /
actions:
- type: type
selector: '#search-input'
text: 'test product'
- type: click
selector: '#search-button'
- type: wait
selector: .search-results
assertions:
- type: element-exists
selector: .search-results
- type: url-matches
pattern: /search\?q=test
screenshot: false
- name: Mobile Menu Navigation
path: /
actions:
- type: click
selector: .mobile-menu-toggle
- type: wait
selector: .mobile-menu
assertions:
- type: element-exists
selector: .mobile-menu.open
screenshot: true
visual:
apiKey: ${OPENAI_API_KEY}
model: gpt-4-vision-preview
pages:
- path: /
name: Homepage
- path: /products
name: Product Listing
- path: /product/sample
name: Product Detail
- path: /checkout
name: Checkout Page
waitForSelector: .checkout-form
aspectsToAnalyze:
- layout-consistency
- color-contrast
- visual-hierarchy
- mobile-responsiveness
- loading-state
- accessibility-visual
errors:
pages:
- /
- /products
- /product/sample
- /checkout
- /account/dashboard
- /api/health
errorTypes:
- javascript-errors
- network-errors
- 404-errors
- mixed-content
- cors-errors
- csp-violations
timeout: 60000
slack:
token: ${SLACK_BOT_TOKEN}
channel: deployment-alerts
mentionOnFailure:
- U123456789 # Tech lead user ID
- U987654321 # DevOps lead user ID
webhookUrl: ${SLACK_WEBHOOK_URL}
database:
connectionString: ${DATABASE_URL}
retention: 30 # days
# Performance thresholds for alerts
thresholds:
lighthouse:
performance: 70
accessibility: 85
seo: 80
errors:
critical: 0
total: 5
visual:
minGrade: B
```
### 8. GitHub Actions Workflow
```ya