#!/usr/bin/env bash ######################################################################################### #### lighthouse-audit.sh — Automated Lighthouse audits with Prometheus integration #### #### Runs headless Chrome via Lighthouse CLI, extracts scores with jq, pushes #### #### metrics to Pushgateway — saves HTML reports and color-codes output #### #### #### #### Author: Phil Connor #### #### Contact: contact@mylinux.work #### #### License: MIT #### #### Version 1.00 #### #### #### #### Usage: #### #### ./lighthouse-audit.sh https://mylinux.work #### #### ./lighthouse-audit.sh --file urls.txt --pushgateway http://localhost:9091 #### #### #### #### See --help for all options. #### ######################################################################################### set -euo pipefail # ── Defaults ────────────────────────────────────────────────────────── URLS_FILE="" OUTPUT_DIR="${OUTPUT_DIR:-/var/lib/lighthouse/reports}" PUSHGATEWAY_URL="${PUSHGATEWAY_URL:-}" CHROME_FLAGS="--headless --no-sandbox --disable-dev-shm-usage --disable-gpu" JOB_NAME="${JOB_NAME:-lighthouse}" LIGHTHOUSE_TIMEOUT="${LIGHTHOUSE_TIMEOUT:-60}" RUNS="${RUNS:-1}" VERBOSE="${VERBOSE:-false}" COLOR="${COLOR:-auto}" # ── State ───────────────────────────────────────────────────────────── SCRIPT_NAME="$(basename "$0")" readonly SCRIPT_NAME TEMP_DIR="" # ── Colors ──────────────────────────────────────────────────────────── setup_colors() { if [[ "$COLOR" == "never" ]]; then RED="" GREEN="" YELLOW="" CYAN="" BOLD="" DIM="" RESET="" return fi if [[ "$COLOR" == "always" ]] || [[ -t 1 ]]; then RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[0;33m' CYAN='\033[0;36m' BOLD='\033[1m' DIM='\033[2m' RESET='\033[0m' else RED="" GREEN="" YELLOW="" CYAN="" BOLD="" DIM="" RESET="" fi } # ── Logging ─────────────────────────────────────────────────────────── log() { echo -e "${CYAN}[INFO]${RESET} $*"; } warn() { echo -e "${YELLOW}[WARN]${RESET} $*" >&2; } err() { echo -e "${RED}[ERROR]${RESET} $*" >&2; } verbose() { if [[ "$VERBOSE" == "true" ]]; then echo -e "${DIM}[DEBUG]${RESET} $*"; fi; } # ── Cleanup ─────────────────────────────────────────────────────────── cleanup() { if [[ -n "$TEMP_DIR" && -d "$TEMP_DIR" ]]; then rm -rf "$TEMP_DIR" fi } trap cleanup EXIT # ── Color-coded score output ───────────────────────────────────────── color_score() { local score=$1 if (( score >= 90 )); then echo -e "${GREEN}${score}${RESET}" elif (( score >= 50 )); then echo -e "${YELLOW}${score}${RESET}" else echo -e "${RED}${score}${RESET}" fi } # ══════════════════════════════════════════════════════════════════════ # AUDIT # ══════════════════════════════════════════════════════════════════════ run_audit() { local url="$1" local date_stamp date_stamp=$(date +%Y-%m-%d_%H%M%S) local slug slug=$(echo "$url" | sed 's|https\?://||;s|/|_|g;s|[^a-zA-Z0-9_.-]||g') log "Auditing: ${url}" local perf_total=0 a11y_total=0 bp_total=0 seo_total=0 local lcp_total=0 cls_total=0 tbt_total=0 local best_json="" local successful_runs=0 for ((run=1; run<=RUNS; run++)); do [[ "$RUNS" -gt 1 ]] && log " Run ${run}/${RUNS}" local json_file="${TEMP_DIR}/${slug}_run${run}.json" lighthouse "$url" \ --output json \ --output-path "$json_file" \ --chrome-flags="$CHROME_FLAGS" \ --max-wait-for-load "$((LIGHTHOUSE_TIMEOUT * 1000))" \ --quiet 2>/dev/null || { warn "Lighthouse failed for ${url} (run ${run})" continue } local perf a11y bp seo lcp cls tbt perf=$(jq -r '.categories.performance.score * 100 | floor' "$json_file") a11y=$(jq -r '.categories.accessibility.score * 100 | floor' "$json_file") bp=$(jq -r '.categories["best-practices"].score * 100 | floor' "$json_file") seo=$(jq -r '.categories.seo.score * 100 | floor' "$json_file") lcp=$(jq -r '.audits["largest-contentful-paint"].numericValue / 1000' "$json_file") cls=$(jq -r '.audits["cumulative-layout-shift"].numericValue' "$json_file") tbt=$(jq -r '.audits["total-blocking-time"].numericValue' "$json_file") perf_total=$(echo "$perf_total + $perf" | bc) a11y_total=$(echo "$a11y_total + $a11y" | bc) bp_total=$(echo "$bp_total + $bp" | bc) seo_total=$(echo "$seo_total + $seo" | bc) lcp_total=$(echo "$lcp_total + $lcp" | bc) cls_total=$(echo "$cls_total + $cls" | bc) tbt_total=$(echo "$tbt_total + $tbt" | bc) successful_runs=$((successful_runs + 1)) best_json="$json_file" done if [[ -z "$best_json" ]]; then warn "All runs failed for ${url}" return 1 fi # Average scores local perf_avg a11y_avg bp_avg seo_avg lcp_avg cls_avg tbt_avg perf_avg=$(echo "$perf_total / $successful_runs" | bc) a11y_avg=$(echo "$a11y_total / $successful_runs" | bc) bp_avg=$(echo "$bp_total / $successful_runs" | bc) seo_avg=$(echo "$seo_total / $successful_runs" | bc) lcp_avg=$(echo "scale=2; $lcp_total / $successful_runs" | bc) cls_avg=$(echo "scale=3; $cls_total / $successful_runs" | bc) tbt_avg=$(echo "scale=0; $tbt_total / $successful_runs" | bc) # Generate HTML report from last run's JSON local report_file="${OUTPUT_DIR}/${slug}_${date_stamp}.html" lighthouse "$url" \ --output html \ --output-path "$report_file" \ --chrome-flags="$CHROME_FLAGS" \ --max-wait-for-load "$((LIGHTHOUSE_TIMEOUT * 1000))" \ --quiet 2>/dev/null || true # Print results log "Results for ${url}:" printf " ${BOLD}Performance:${RESET} %s\n" "$(color_score "$perf_avg")" printf " ${BOLD}Accessibility:${RESET} %s\n" "$(color_score "$a11y_avg")" printf " ${BOLD}Best Practices:${RESET} %s\n" "$(color_score "$bp_avg")" printf " ${BOLD}SEO:${RESET} %s\n" "$(color_score "$seo_avg")" printf " LCP: %ss CLS: %s TBT: %sms\n" "$lcp_avg" "$cls_avg" "$tbt_avg" [[ -f "$report_file" ]] && log "Report: ${report_file}" # Push to Prometheus Pushgateway if [[ -n "$PUSHGATEWAY_URL" ]]; then local encoded_url encoded_url=$(echo "$url" | sed 's|/|%2F|g;s|:|%3A|g') cat <&2 exit 1 ;; *) POSITIONAL_URLS+=("$1"); shift ;; esac done } # ══════════════════════════════════════════════════════════════════════ # MAIN # ══════════════════════════════════════════════════════════════════════ main() { parse_args "$@" setup_colors # Collect URLs URLS=() for u in "${POSITIONAL_URLS[@]+"${POSITIONAL_URLS[@]}"}"; do URLS+=("$u") done if [[ -n "$URLS_FILE" ]]; then if [[ ! -f "$URLS_FILE" ]]; then err "URLs file not found: ${URLS_FILE}" exit 1 fi while IFS= read -r line; do line=$(echo "$line" | xargs) [[ -z "$line" || "$line" == \#* ]] && continue URLS+=("$line") done < "$URLS_FILE" fi if [[ ! -t 0 ]]; then while IFS= read -r line; do line=$(echo "$line" | xargs) [[ -z "$line" || "$line" == \#* ]] && continue URLS+=("$line") done fi if [[ ${#URLS[@]} -eq 0 ]]; then err "No URLs provided (see --help)" exit 1 fi # Validate dependencies command -v lighthouse >/dev/null 2>&1 || { err "lighthouse not found — npm install -g lighthouse"; exit 1; } command -v jq >/dev/null 2>&1 || { err "jq not found — apt install jq"; exit 1; } mkdir -p "$OUTPUT_DIR" TEMP_DIR=$(mktemp -d) log "Lighthouse Audit — $(date -u +%Y-%m-%dT%H:%M:%SZ)" log "URLs: ${#URLS[@]} | Runs: ${RUNS} | Timeout: ${LIGHTHOUSE_TIMEOUT}s" for url in "${URLS[@]}"; do run_audit "$url" || true [[ ${#URLS[@]} -gt 1 ]] && sleep 5 done log "Done." } main "$@"