← back to Ga Allsites
gate2_inject.sh
369 lines
#!/usr/bin/env bash
# gate2_inject.sh — GATE-2 GA4 injection for the 34-site DW fleet microsite fleet
# Measurement ID: G-J2SL8P5KDL (property "DW Fleet Microsites" / 551081041)
#
# IDEMPOTENT: any file already containing G-J2SL8P5KDL is skipped unchanged.
# REVERSIBLE: timestamped .bak files written before every edit; gate2_rollback.sh restores them.
# RUNS ON KAMATERA: ship this entire heredoc over SSH.
#
# Deploy command (Steve pastes):
# ! ssh root@45.61.58.125 'bash -s' < ~/Projects/ga-allsites/gate2_inject.sh
#
# Rollback command:
# ! ssh root@45.61.58.125 'bash -s' < ~/Projects/ga-allsites/gate2_rollback.sh
# ---------------------------------------------------------------------------
set -euo pipefail
ID="G-J2SL8P5KDL"
ssh root@45.61.58.125 "ID=$ID" 'bash -s' <<'REMOTE'
set -uo pipefail
ID="${ID:-G-J2SL8P5KDL}"
TS=$(date +%Y%m%d-%H%M%S)
# ────────────────────────────────────────────────────────────────────────────
# Counters
N_INJECTED=0; N_SKIPPED=0; N_FAILED=0; N_MANUAL=0
declare -A RESULT # file -> status string
declare -A BAK_MAP # file -> .bak path (written to /tmp/gate2-bak-manifest.txt at end)
# ────────────────────────────────────────────────────────────────────────────
# STANDALONE TARGETS (file path | vendor slug | pm2 process name)
# Each gets: async gtag loader + gtag('js') + gtag('config', ID, {vendor:'<slug>'})
# inserted as the very first child of <head>.
STANDALONE_RAW=(
"/root/Projects/artmura-site/public/index.html|artmura|artmura-site"
"/root/Projects/carnegie-site/public/index.html|carnegie|carnegie-site"
"/root/Projects/thibaut-site/public/index.html|thibaut|thibaut-site"
"/root/Projects/elitis-site/public/index.html|elitis|elitis-site"
"/root/Projects/romo-site/public/index.html|romo|romo-site"
"/root/Projects/la-walls-site/public/index.html|la-walls|la-walls-site"
"/root/Projects/british-walls-site/public/index.html|british-walls|british-walls-site"
"/root/Projects/leed-walls-site/public/index.html|leed-walls|leed-walls-site"
"/root/Projects/surface-stick-site/public/index.html|surface-stick|surface-stick-site"
"/root/Projects/coleandson-site/public/index.html|cole-and-son|coleandson-site"
"/root/Projects/fallingstar-site/public/index.html|fallingstar|fallingstar-site"
"/root/Projects/philipjeffries-site/public/index.html|phillip-jeffries|philipjeffries-site"
"/root/Projects/primo-leathers-site/public/index.html|primo-leathers|primo-leathers-site"
"/root/Projects/quadrille-house-site/public/index.html|quadrille|quadrille-house-site"
"/var/www/fabricut.designerwallcoverings.com/public/index.html|fabricut|fabricut-landing"
"/root/Projects/fentucci-site/public/index.html|fentucci|fentucci-site"
)
# SHARED TEMPLATE (30-vendor wolfgordon binary)
# Gets: async loader + gtag('js') in <head>,
# gtag('config', ID, {vendor:cfg.vendor}) inserted INSIDE the async IIFE
# on the line immediately after applyConfig(cfg);
SHARED_FILE="/root/Projects/wolfgordon-site/public/index.html"
# All 30 pm2 processes that run wolfgordon-site/server.js — reload all of them.
WOLF_PM2_NAMES=(
anna-french-site brunschwig-fils-site coordonne-site designers-guild-site
designtex-site gp-j-baker-site groundworks-site harlequin-site knoll-site
koroseal-site kravet-site lee-jofa-site maharam-site maya-romanoff-site
mulberry-site nina-campbell-site nobilis-site osborne-little-site
phillip-jeffries-site pierre-frey-site ralph-lauren-site rebel-walls-site
sancar-site sandberg-site scalamandre-site threads-site william-morris-site
wolf-gordon-site wolfgordon-site zoffany-site
)
# FENTUCCI-WEBSITE (old binary with commented-out placeholder)
# Gets: commented placeholder uncommented + ID set to G-J2SL8P5KDL.
# The real loader + config are inserted the same way as standalones (head injection),
# but we ALSO remove the now-redundant commented placeholder if both coexist.
FENTUCCI_OLD_FILE="/root/Projects/Designer-Wallcoverings/DW-Websites/Fentucci/public/index.html"
FENTUCCI_OLD_PM2="fentucci-website"
# ────────────────────────────────────────────────────────────────────────────
# Helper: build the head snippet for a static vendor slug
head_snippet_static() {
local vendor="$1"
printf '<script async src="https://www.googletagmanager.com/gtag/js?id=%s"></script>\n<script>window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments);}gtag('\''js'\'',new Date());gtag('\''config'\'','\''%s'\'',{vendor:'\''%s'\''});</script>' \
"$ID" "$ID" "$vendor"
}
# Helper: build the head snippet for the shared template (no config here; fires in IIFE)
head_snippet_shared() {
printf '<script async src="https://www.googletagmanager.com/gtag/js?id=%s"></script>\n<script>window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments);}gtag('\''js'\'',new Date());</script>' \
"$ID"
}
# ────────────────────────────────────────────────────────────────────────────
# inject_standalone FILE VENDOR PM2_NAME
inject_standalone() {
local file="$1" vendor="$2" pm2_name="$3"
local label="$vendor"
if [ ! -f "$file" ]; then
RESULT["$label"]="MISSING — file not found: $file"
(( N_MANUAL++ )) || true; return
fi
if grep -qF "$ID" "$file"; then
RESULT["$label"]="SKIP — already contains $ID"
(( N_SKIPPED++ )) || true; return
fi
if ! grep -qiE '</head>' "$file"; then
RESULT["$label"]="NEEDS-MANUAL — no </head> anchor found: $file"
(( N_MANUAL++ )) || true; return
fi
local bak="${file}.bak-ga-${TS}"
cp -a "$file" "$bak"
BAK_MAP["$file"]="$bak"
local snippet
snippet="$(head_snippet_static "$vendor")"
# Insert the snippet immediately before </head>
perl -0777 -i -pe "s{(</head>)}{${snippet}\n\$1}i" "$file"
if grep -qF "$ID" "$file"; then
RESULT["$label"]="INJECTED — $file [pm2: $pm2_name]"
(( N_INJECTED++ )) || true
# Reload pm2 process
if pm2 reload "$pm2_name" >/dev/null 2>&1; then
RESULT["$label"]+=" | pm2 reload: OK"
else
RESULT["$label"]+=" | pm2 reload: WARN (try restart manually)"
fi
else
# Revert
cp -a "$bak" "$file"
RESULT["$label"]="FAIL — perl injection failed, reverted from bak: $file"
(( N_FAILED++ )) || true
fi
}
# ────────────────────────────────────────────────────────────────────────────
# inject_shared — wolfgordon-site/public/index.html
inject_shared() {
local file="$SHARED_FILE"
local label="wolfgordon-shared-template"
if [ ! -f "$file" ]; then
RESULT["$label"]="MISSING — file not found: $file"
(( N_MANUAL++ )) || true; return
fi
if grep -qF "$ID" "$file"; then
RESULT["$label"]="SKIP — already contains $ID"
(( N_SKIPPED++ )) || true; return
fi
# Both anchors must be present
if ! grep -qiE '</head>' "$file"; then
RESULT["$label"]="NEEDS-MANUAL — no </head> anchor: $file"
(( N_MANUAL++ )) || true; return
fi
if ! grep -qF 'applyConfig(cfg);' "$file"; then
RESULT["$label"]="NEEDS-MANUAL — no 'applyConfig(cfg);' anchor in IIFE: $file"
(( N_MANUAL++ )) || true; return
fi
local bak="${file}.bak-ga-${TS}"
cp -a "$file" "$bak"
BAK_MAP["$file"]="$bak"
local head_snip
head_snip="$(head_snippet_shared)"
# 1. Insert loader + gtag('js') before </head>
perl -0777 -i -pe "s{(</head>)}{${head_snip}\n\$1}i" "$file"
# 2. Insert gtag('config', ID, {vendor:cfg.vendor}) immediately after applyConfig(cfg);
# (single-line match; the line has 6 leading spaces in the current file)
perl -0777 -i -pe "s{(applyConfig\(cfg\);)(\s*\n)}{\$1\$2 gtag('config','${ID}',{vendor:cfg.vendor});\n}" "$file"
if grep -qF "$ID" "$file"; then
RESULT["$label"]="INJECTED — $file [30 pm2 processes]"
(( N_INJECTED++ )) || true
# Reload all 30 processes on this binary
local reload_ok=0 reload_fail=0
for pname in "${WOLF_PM2_NAMES[@]}"; do
if pm2 reload "$pname" >/dev/null 2>&1; then
(( reload_ok++ )) || true
else
(( reload_fail++ )) || true
fi
done
RESULT["$label"]+=" | pm2 reload: ${reload_ok}/${#WOLF_PM2_NAMES[@]} OK"
[ "$reload_fail" -gt 0 ] && RESULT["$label"]+=" (${reload_fail} failed — check pm2 log)"
else
cp -a "$bak" "$file"
RESULT["$label"]="FAIL — injection failed, reverted: $file"
(( N_FAILED++ )) || true
fi
}
# ────────────────────────────────────────────────────────────────────────────
# inject_fentucci_old — /root/Projects/Designer-Wallcoverings/DW-Websites/Fentucci/public/index.html
# Strategy: the file has a commented-out placeholder:
# <!-- <script async src="...G-XXXXXXXXXX"></script> -->
# Uncomment it and set the ID. Then also ensure the config call is present.
# If the file already has G-J2SL8P5KDL anywhere, skip.
inject_fentucci_old() {
local file="$FENTUCCI_OLD_FILE"
local label="fentucci-website-OLD-binary"
if [ ! -f "$file" ]; then
RESULT["$label"]="MISSING — file not found: $file"
(( N_MANUAL++ )) || true; return
fi
if grep -qF "$ID" "$file"; then
RESULT["$label"]="SKIP — already contains $ID"
(( N_SKIPPED++ )) || true; return
fi
if ! grep -qiE '</head>' "$file"; then
RESULT["$label"]="NEEDS-MANUAL — no </head> anchor: $file"
(( N_MANUAL++ )) || true; return
fi
local bak="${file}.bak-ga-${TS}"
cp -a "$file" "$bak"
BAK_MAP["$file"]="$bak"
# Try to uncomment the placeholder line and set ID
# Pattern: <!-- <script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"></script> -->
local placeholder_found=0
if grep -q 'GA4 placeholder\|gtag/js.*G-' "$file"; then
# Uncomment the script tag and set the correct ID
perl -i -pe 's{<!--\s*(<script async src="https://www\.googletagmanager\.com/gtag/js\?id=)[^"]*(">[^<]*</script>)\s*-->}{\1'"$ID"'\2}' "$file"
placeholder_found=1
fi
# Whether or not we uncommented a placeholder, we need the full bootstrap in <head>.
# Insert before </head> if the ID isn't present yet (the uncommented placeholder is only the loader tag).
if ! grep -qF "$ID" "$file"; then
# placeholder uncomment didn't land the ID — inject from scratch
local snippet
snippet="$(head_snippet_static "fentucci")"
perl -0777 -i -pe "s{(</head>)}{${snippet}\n\$1}i" "$file"
else
# We got the loader via placeholder uncomment but still need the config call + init script.
# Insert a config+init block right after the uncommented loader line.
perl -i -pe 's{(<script async src="https://www\.googletagmanager\.com/gtag/js\?id='"$ID"'"></script>)}{$1\n<script>window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments);}gtag('\''js'\'',new Date());gtag('\''config'\'','\'''"$ID"''\'',{vendor:'\''fentucci'\''});</script>}' "$file" || true
fi
if grep -qF "$ID" "$file"; then
RESULT["$label"]="INJECTED — $file [pm2: $FENTUCCI_OLD_PM2]"
(( N_INJECTED++ )) || true
if pm2 reload "$FENTUCCI_OLD_PM2" >/dev/null 2>&1; then
RESULT["$label"]+=" | pm2 reload: OK"
else
RESULT["$label"]+=" | pm2 reload: WARN"
fi
else
cp -a "$bak" "$file"
RESULT["$label"]="FAIL — injection failed, reverted: $file"
(( N_FAILED++ )) || true
fi
}
# ────────────────────────────────────────────────────────────────────────────
# EXECUTE
echo ""
echo "========================================================"
echo " GATE-2 GA4 INJECT | ID: $ID"
echo " Timestamp: $TS"
echo "========================================================"
echo ""
echo "[1/3] Shared wolfgordon template (30 vendors)..."
inject_shared
echo "[2/3] Standalone sites (16 files)..."
for entry in "${STANDALONE_RAW[@]}"; do
IFS='|' read -r fpath vendor pm2name <<< "$entry"
inject_standalone "$fpath" "$vendor" "$pm2name"
done
echo "[3/3] Fentucci-website old binary (placeholder uncomment)..."
inject_fentucci_old
# ────────────────────────────────────────────────────────────────────────────
# Write bak manifest so rollback can find all .bak files for this run
echo "" > /tmp/gate2-bak-manifest-${TS}.txt
for orig in "${!BAK_MAP[@]}"; do
echo "${orig}|${BAK_MAP[$orig]}" >> /tmp/gate2-bak-manifest-${TS}.txt
done
echo "(bak manifest: /tmp/gate2-bak-manifest-${TS}.txt)"
# ────────────────────────────────────────────────────────────────────────────
# VERIFY — curl each site, grep for the ID in served HTML
echo ""
echo "========================================================"
echo " VERIFY"
echo "========================================================"
# Map vendor slug -> live URL for curl verification
declare -A VERIFY_URL
VERIFY_URL["artmura"]="http://127.0.0.1:$(pm2 jlist 2>/dev/null | python3 -c "import json,sys;d=json.load(sys.stdin);[print(p['pm2_env']['env'].get('PORT',3000)) for p in d if p['name']=='artmura-site']" 2>/dev/null | head -1 || echo 3000)"
# Simpler: pull port from pm2 env for each site
get_port() {
local pm2name="$1"
pm2 jlist 2>/dev/null | python3 -c "
import json,sys
try:
d=json.load(sys.stdin)
for p in d:
if p.get('name')=='$pm2name':
env=p.get('pm2_env',{}).get('env',{})
port=env.get('PORT') or env.get('port') or 3000
print(port)
break
except:
print(3000)
" 2>/dev/null | head -1
}
# Build verify list: "label|url"
VERIFY_LIST=()
for entry in "${STANDALONE_RAW[@]}"; do
IFS='|' read -r fpath vendor pm2name <<< "$entry"
port=$(get_port "$pm2name")
VERIFY_LIST+=("$vendor|http://127.0.0.1:${port}/")
done
# wolfgordon shared: pick wolf-gordon-site process
wolf_port=$(get_port "wolf-gordon-site")
VERIFY_LIST+=("wolfgordon-shared|http://127.0.0.1:${wolf_port}/")
# fentucci-website old binary
fwo_port=$(get_port "$FENTUCCI_OLD_PM2")
VERIFY_LIST+=("fentucci-website-OLD|http://127.0.0.1:${fwo_port}/")
N_VERIFY_PASS=0; N_VERIFY_FAIL=0
for entry in "${VERIFY_LIST[@]}"; do
IFS='|' read -r label url <<< "$entry"
html=$(curl -sf --max-time 6 --retry 2 "$url" 2>/dev/null || true)
if echo "$html" | grep -qF "$ID"; then
printf " VERIFY PASS %-30s %s\n" "$label" "$url"
(( N_VERIFY_PASS++ )) || true
else
printf " VERIFY FAIL %-30s %s\n" "$label" "$url"
(( N_VERIFY_FAIL++ )) || true
fi
done
# ────────────────────────────────────────────────────────────────────────────
echo ""
echo "========================================================"
echo " SUMMARY"
echo "========================================================"
for label in $(echo "${!RESULT[@]}" | tr ' ' '\n' | sort); do
printf " %-40s %s\n" "$label" "${RESULT[$label]}"
done
echo ""
echo " Injected : $N_INJECTED"
echo " Skipped : $N_SKIPPED"
echo " Failed : $N_FAILED"
echo " Manual : $N_MANUAL"
echo " Verify : ${N_VERIFY_PASS} PASS / ${N_VERIFY_FAIL} FAIL"
echo ""
echo "Rollback: ssh root@45.61.58.125 'bash -s' < ~/Projects/ga-allsites/gate2_rollback.sh"
echo " (restores all .bak files matching *.bak-ga-${TS})"
echo "========================================================"
REMOTE