Skip to content

rush-cms/audits

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

100 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Rush CMS Audits Banner

Rush CMS Audits Microservice

Laravel 12 PHPStan Level 8 Tests Passing License MIT

rushcms.com/audits


A standalone, headless microservice dedicated to generating high-fidelity performance reports (Lighthouse/PageSpeed) in PDF format. Designed to be whitelabel, asynchronous, and webhook-oriented.

Ideally used with n8n or other automation tools to ingest PageSpeed API data and return professional client-ready PDFs.

Documentation: docs/

Features

  • Headless Architecture: API-first design with no frontend UI
  • Whitelabel Ready: Customize logos, brand names, CTA links, and more via config
  • Asynchronous Processing: Heavy PDF generation happens in the background via Queues
  • Webhook Callbacks: Receive a ping with the PDF URL as soon as it's ready
  • Webhook Signatures: HMAC-SHA256 signatures for webhook authenticity verification
  • Audit Persistence: All audits stored in database with status tracking (pending, processing, completed, failed)
  • State-Based Idempotency: Smart idempotency based on audit status, not time windows
  • Graceful Degradation: PDF generation continues even if screenshots fail (configurable)
  • Partial Data Persistence: PageSpeed data preserved even if pipeline fails midway
  • Dead Letter Queue: Failed jobs tracked with auto-cleanup after configurable retention period
  • Smart Pruning: Auto-cleanup of old PDF files to save storage
  • Token Authentication: Built-in console commands to manage API clients
  • Rate Limiting: Configurable per-token and global rate limits with Redis
  • SSRF Protection: Blocks private networks and localhost in production
  • Race Condition Handling: Optimistic locking with exponential backoff for concurrent requests
  • Multi-language Support: English, Portuguese (BR), and Spanish
  • Screenshot Capture: Automatic desktop & mobile screenshots with device mockup frames
  • SEO & Accessibility Audits: Optional sections with detailed issue reports
  • Core Web Vitals: LCP, FCP, CLS with contextual performance messages

Tech Stack

  • Core: Laravel 12 API (Slim setup)
  • PDF Engine: Spatie Browsershot (Puppeteer/Chromium)
  • Image Processing: Spatie Image (WebP conversion)
  • Queue: Redis (Recommended)
  • Storage: Local Public Disk / S3

Workflow

  1. Ingestion: Send a URL to POST /api/v1/scan
  2. State-Based Idempotency: Returns existing audit based on status (pending/processing = reuse, completed = new scan allowed, failed recent = retry window)
  3. Database Persistence: Creates audit record with status pending
  4. PageSpeed Fetch: Fetches Performance, SEO & Accessibility data, saves to pagespeed_data, marks audit as processing
  5. Screenshot Capture: Takes desktop (1920x1080) and mobile (375x812) screenshots, saves to screenshots_data
  6. Image Optimization: Converts screenshots to WebP (600px width)
  7. PDF Generation: Renders Blade views with Tailwind CSS, converts to PDF (continues even if screenshots fail)
  8. Status Update: Marks audit as completed with score, metrics, and PDF path
  9. Callback: Sends POST request to your webhook URL with the PDF link and metadata

Roadmap

Some features are not yet implemented, but are planned for the future:

  • S3/R2 storage support
  • CORS configuration
  • Factories for audit testing
  • Admin dashboard with template customization
  • Audit comparisions
  • Internal check-ups with advanced SEO metrics

Environment Configuration

cp .env.example .env

Edit the .env file to configure:

APP_URL=https://audits.rushcms.com
APP_TIMEZONE="America/Sao_Paulo"

# Service Logic
AUDITS_WEBHOOK_RETURN_URL="https://your-main-app.com/api/webhook/audit-ready"
AUDITS_CONCURRENCY=1
AUDITS_RETENTION_DAYS=7
AUDITS_IDEMPOTENCY_WINDOW=60
AUDITS_FAILED_RETRY_AFTER=300
AUDITS_REQUIRE_SCREENSHOTS=false
AUDITS_FAILED_JOBS_RETENTION_DAYS=30
AUDITS_JOB_MAX_ATTEMPTS=3
AUDITS_JOB_BACKOFF_BASE=30

# Branding (Whitelabel)
AUDITS_BRAND_NAME="Rush CMS"
AUDITS_LOGO_PATH="rush-cms-logo.png"

# Report Settings
AUDITS_DATE_FORMAT="d/m/Y H:i"
AUDITS_SHOW_SEO=false
AUDITS_SHOW_ACCESSIBILITY=false
AUDITS_CTA_URL="https://wa.me/5511999999999"

# PageSpeed Insights API (optional, increases rate limits)
PAGESPEED_API_KEY=

# Browsershot / Puppeteer (Critical for Linux/Docker)
BROWSERSHOT_NODE_BINARY="/usr/bin/node"
BROWSERSHOT_NPM_BINARY="/usr/bin/npm"
BROWSERSHOT_CHROME_PATH="/usr/bin/google-chrome"

Configuration Reference

Variable Default Description
APP_TIMEZONE America/Sao_Paulo Timezone for date display
AUDITS_BRAND_NAME Rush CMS Brand name in header/footer
AUDITS_LOGO_PATH null Path to logo in public folder
AUDITS_DATE_FORMAT d/m/Y H:i PHP date format for header
AUDITS_SHOW_SEO false Show SEO audit section
AUDITS_SHOW_ACCESSIBILITY false Show Accessibility section
AUDITS_CTA_URL WhatsApp link Call-to-action button URL
AUDITS_RETENTION_DAYS 7 Days to keep PDF files before pruning
AUDITS_IDEMPOTENCY_WINDOW 60 Minutes to prevent duplicate audits (legacy)
AUDITS_FAILED_RETRY_AFTER 300 Seconds before failed audit allows retry (5 min)
AUDITS_REQUIRE_SCREENSHOTS false Fail audit if screenshots fail (true) or continue (false)
AUDITS_FAILED_JOBS_RETENTION_DAYS 30 Days to keep failed jobs before cleanup
AUDITS_JOB_MAX_ATTEMPTS 3 Maximum retry attempts for failed jobs
AUDITS_JOB_BACKOFF_BASE 30 Base delay in seconds for job retry backoff
AUDITS_WEBHOOK_TIMEOUT 5 Webhook timeout in seconds
AUDITS_WEBHOOK_CONNECT_TIMEOUT 2 Webhook connection timeout in seconds
AUDITS_WEBHOOK_MAX_ATTEMPTS 5 Maximum webhook retry attempts
AUDITS_WEBHOOK_SECRET null Secret for webhook HMAC signatures
AUDITS_NOTIFY_ON_WEBHOOK_FAILURE true Send notifications on webhook failure
AUDITS_ADMIN_EMAIL null Admin email for failure notifications
AUDITS_SLACK_WEBHOOK_URL null Slack webhook URL for failure notifications
AUDITS_RATE_LIMIT_PER_MINUTE 60 Requests per minute per token
AUDITS_RATE_LIMIT_PER_HOUR 500 Requests per hour per token
AUDITS_RATE_LIMIT_PER_DAY 2000 Requests per day per token
AUDITS_RATE_LIMIT_GLOBAL_PER_MINUTE 200 Global requests per minute
AUDITS_DELETE_SCREENSHOTS_AFTER_PDF true Delete screenshots after PDF generation
AUDITS_ORPHANED_SCREENSHOTS_RETENTION_HOURS 24 Hours before orphaned screenshots are deleted
PAGESPEED_API_KEY null Google PageSpeed API key
PAGESPEED_RATE_LIMIT_PER_MINUTE 6 PageSpeed API calls per minute (free tier)
PAGESPEED_RATE_LIMIT_PER_DAY 25000 PageSpeed API calls per day (with API key)
QUEUE_PDF_CONCURRENCY 3 Max concurrent PDF generation jobs
QUEUE_SCREENSHOT_CONCURRENCY 5 Max concurrent screenshot capture jobs
QUEUE_MAX_DEPTH_ALERT 100 Queue depth threshold for alerts
API_MAX_REQUEST_SIZE 1048576 Max request payload size in bytes (1MB)
BROWSERSHOT_TIMEOUT 60 Browsershot process timeout in seconds
BROWSERSHOT_MEMORY_LIMIT 512 Browsershot max memory in MB per process
BROWSERSHOT_MAX_CONCURRENT_PDF 3 Max concurrent Browsershot PDF processes
BROWSERSHOT_MAX_CONCURRENT_SCREENSHOTS 5 Max concurrent Browsershot screenshot processes

API Reference

Authentication

All requests require a Bearer Token:

Authorization: Bearer <your-token>

Rate Limiting

API requests are rate-limited per token:

  • 60 requests/minute (default)
  • 500 requests/hour (default)
  • 2000 requests/day (default)
  • 200 requests/minute globally (all tokens combined)

Rate limit headers are included in all responses:

X-RateLimit-Limit: 60
X-RateLimit-Remaining: 45
X-RateLimit-Reset: 1704067200

When limit is exceeded, you'll receive a 429 Too Many Requests response:

{
  "message": "Too many requests. Please try again later.",
  "retry_after": 32
}

Webhook Signatures

If AUDITS_WEBHOOK_SECRET is configured, webhooks include HMAC-SHA256 signatures for verification:

Headers:

X-Webhook-Signature: sha256=abc123...
X-Webhook-Timestamp: 1704067200
X-Webhook-ID: 20250129120000-a1b2c3d4e5f6g7h8

Verification (PHP):

$secret = env('AUDITS_WEBHOOK_SECRET');
$timestamp = $_SERVER['HTTP_X_WEBHOOK_TIMESTAMP'];
$signature = str_replace('sha256=', '', $_SERVER['HTTP_X_WEBHOOK_SIGNATURE']);
$payload = file_get_contents('php://input');

$expectedSignature = hash_hmac('sha256', "{$timestamp}.{$payload}", $secret);

if (!hash_equals($expectedSignature, $signature)) {
    http_response_code(401);
    exit('Invalid signature');
}

if (abs(time() - $timestamp) > 300) {
    http_response_code(401);
    exit('Timestamp expired');
}

Verification (Node.js):

const crypto = require('crypto');

const secret = process.env.AUDITS_WEBHOOK_SECRET;
const timestamp = req.headers['x-webhook-timestamp'];
const signature = req.headers['x-webhook-signature'].replace('sha256=', '');
const payload = JSON.stringify(req.body);

const expectedSignature = crypto
  .createHmac('sha256', secret)
  .update(`${timestamp}.${payload}`)
  .digest('hex');

if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expectedSignature))) {
  return res.status(401).json({ error: 'Invalid signature' });
}

if (Math.abs(Date.now() / 1000 - timestamp) > 300) {
  return res.status(401).json({ error: 'Timestamp expired' });
}

Security & SSRF Protection

In production (APP_ENV=production), the service automatically blocks:

  • Private IP ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)
  • Localhost (127.0.0.1, ::1, localhost)
  • Link-local addresses (169.254.0.0/16 - AWS metadata, etc.)
  • Custom blocked domains (via config/blocked-domains.php)

Example blocked URLs in production:

http://localhost:8000/admin ❌ Blocked
http://192.168.1.1/ ❌ Blocked
http://169.254.169.254/latest/meta-data/ ❌ Blocked (AWS metadata)
https://example.com ✅ Allowed

In local/development (APP_ENV=local), SSRF protection is disabled to allow testing against local services.

Blocking Custom Domains:

Edit config/blocked-domains.php to add domains:

return [
    'internal.company.com',
    'staging.myapp.com',
    'localhost.example.com',
];

Subdomains are automatically matched (e.g., example.com blocks www.example.com).

Submit Scan

Endpoint: POST /api/v1/scan

Request:

{
  "url": "https://example.com",
  "lang": "pt_BR",
  "strategy": "mobile"
}
Field Type Default Description
url string required URL to analyze
lang string en Language: en, pt_BR, es
strategy string mobile Strategy: mobile or desktop

Response: 202 Accepted

{
  "message": "Audit queued",
  "audit_id": "550e8400-e29b-41d4-a716-446655440000",
  "url": "https://example.com",
  "lang": "pt_BR",
  "strategy": "mobile",
  "status": "pending"
}

Note: Idempotency is now state-based:

  • Pending/Processing audits: Returns existing audit_id
  • Completed audits: Creates new audit (allows immediate re-scan)
  • Failed audits: Returns existing if recent (< 5min), creates new if old

Get Audit Status

Endpoint: GET /api/v1/audits/{id}

Response: 200 OK

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "url": "https://example.com",
  "strategy": "mobile",
  "lang": "pt_BR",
  "status": "completed",
  "score": 95,
  "metrics": {
    "lcp": "1.2 s",
    "fcp": "0.8 s",
    "cls": "0.05"
  },
  "pdf_url": "https://audits.rushcms.com/storage/reports/550e8400.pdf",
  "error_message": null,
  "created_at": "2025-12-29T04:00:00Z",
  "completed_at": "2025-12-29T04:02:15Z"
}

Status values: pending, processing, completed, failed

Get Stats

Endpoint: GET /api/v1/stats

Response: 200 OK

{
  "minute": 3,
  "hour": 45,
  "day": 127,
  "month": 1542
}

Full API Documentation: docs/api.md

Console Commands

# Create API token
php artisan audit:create-token "Client Name"

# Test PDF generation (no API calls)
php artisan test:pdf --lang=pt_BR

# Prune old PDFs
php artisan audit:prune-pdfs

# Prune orphaned screenshots
php artisan audits:prune-orphaned-screenshots

# Cleanup old failed jobs
php artisan audits:cleanup-failed-jobs

# Retry failed webhook delivery
php artisan webhook:retry {audit_id}
php artisan webhook:retry-failed --limit=50

# Prune old webhook delivery records
php artisan webhook:prune-deliveries --days=30

# Analyze query performance
php artisan audits:explain-queries

# Check browser setup
php artisan audit:check-browser

Deployment

System Requirements

  • PHP 8.4+
  • Node.js 18+
  • Chromium/Chrome
  • Redis (for queues)

Queue Worker (Required)

php artisan queue:work --tries=2 --timeout=180

Scheduler

php artisan schedule:run

Report Components

The PDF report includes:

  1. Header: Brand logo + generation timestamp
  2. Performance Score: Circular gauge with pass/fail indicator
  3. Device Mockup: iPhone 16 + MacBook frames with live screenshots
  4. Core Web Vitals: LCP, FCP, CLS cards with progress bars
  5. Performance Messages: Contextual feedback based on metrics
  6. SEO Section: Score + failed audit list (optional)
  7. Accessibility Section: Score + failed audit list (optional)
  8. Closing CTA: Dynamic message based on score + WhatsApp button
  9. Footer: Brand name, Audit ID, data source

License

This project is open-sourced software licensed under the MIT License.

Releases

No releases published

Packages

No packages published

Contributors 2

  •  
  •  

Languages