← back to Ga4 Fleet
inject-gtag.mjs: idempotent static-HTML gtag injector, flags JS-template heads (tested on wallpapersback)
a99fb18399c289f4e4c6c4ea6756ad2c67638bb1 · 2026-08-03 13:10:23 -0700 · Steve
Files touched
Diff
commit a99fb18399c289f4e4c6c4ea6756ad2c67638bb1
Author: Steve <steve@designerwallcoverings.com>
Date: Mon Aug 3 13:10:23 2026 -0700
inject-gtag.mjs: idempotent static-HTML gtag injector, flags JS-template heads (tested on wallpapersback)
---
inject-gtag.mjs | 108 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 108 insertions(+)
diff --git a/inject-gtag.mjs b/inject-gtag.mjs
new file mode 100644
index 0000000..a91f570
--- /dev/null
+++ b/inject-gtag.mjs
@@ -0,0 +1,108 @@
+#!/usr/bin/env node
+// gtag injector — inserts the GA4 gtag snippet into a site's <head>, idempotently.
+// Half two of the fleet pipeline: once provision.mjs fills measurement-ids.json with
+// domain -> G-id, this wires each site's code.
+//
+// SAFE BY DESIGN:
+// - Static-HTML sites (public/*.html): injects after the first <head> in each page,
+// skips any page that already carries the id. Pure string insert, reversible.
+// - JS-template/Express sites (a <head> living inside a `...` template literal): does
+// NOT auto-edit — auto-inserting into a template literal risks a syntax break (the
+// assetv-style 502). It FLAGS them for a targeted per-repo edit instead.
+//
+// USAGE:
+// node inject-gtag.mjs --repo <path> --domain <d> --id G-XXXX # DRY RUN
+// node inject-gtag.mjs --repo <path> --domain <d> --id G-XXXX --commit
+// node inject-gtag.mjs --all # iterate registry via repo-map.json
+
+import { readFileSync, writeFileSync, existsSync, readdirSync, statSync } from 'node:fs';
+import { join } from 'node:path';
+
+const DIR = '/Users/macstudio3/Projects/ga4-fleet';
+const args = process.argv.slice(2);
+const has = (f) => args.includes(f);
+const val = (f, d) => { const i = args.indexOf(f); return i >= 0 ? args[i + 1] : d; };
+const COMMIT = has('--commit');
+
+const snippet = (id) =>
+ `<!-- Google tag (gtag.js) — GA4 ${id} -->\n` +
+ `<script async src="https://www.googletagmanager.com/gtag/js?id=${id}"></script>\n` +
+ `<script>window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments);}gtag('js',new Date());gtag('config','${id}');</script>`;
+
+function walk(dir, out = [], depth = 0) {
+ if (depth > 4 || !existsSync(dir)) return out;
+ for (const f of readdirSync(dir)) {
+ if (['node_modules', '.git', 'dist', 'build', '.next', 'tmp'].includes(f)) continue;
+ const p = join(dir, f);
+ let s; try { s = statSync(p); } catch { continue; }
+ if (s.isDirectory()) walk(p, out, depth + 1);
+ else if (/\.html?$/.test(f)) out.push(p);
+ }
+ return out;
+}
+
+// Inject into every static HTML page's <head>. Returns {changed, skipped, files}.
+function injectStatic(repo, id) {
+ const pages = walk(repo).filter((p) => !/curator|admin|seam|debug|ghost|internal|\/tests?\//i.test(p));
+ const changed = [], skipped = [];
+ for (const p of pages) {
+ let html; try { html = readFileSync(p, 'utf8'); } catch { continue; }
+ if (!/<head[\s>]/i.test(html)) continue; // not a full page
+ if (html.includes(id)) { skipped.push(p); continue; } // already has THIS id
+ if (/gtag\/js\?id=G-/.test(html)) { skipped.push(p + ' (has a DIFFERENT gtag — left as-is)'); continue; }
+ const injected = html.replace(/(<head[^>]*>)/i, `$1\n${snippet(id)}`);
+ if (COMMIT) writeFileSync(p, injected);
+ changed.push(p);
+ }
+ return { changed, skipped };
+}
+
+// Detect a server-rendered <head> inside a JS template literal (Express layout). Flag, don't edit.
+function detectTemplate(repo) {
+ const hits = [];
+ for (const p of walk(repo.replace(/\/public$/, '')).length ? [] : []) {} // (html handled above)
+ const jsFiles = [];
+ (function jw(d, depth = 0) {
+ if (depth > 3 || !existsSync(d)) return;
+ for (const f of readdirSync(d)) {
+ if (['node_modules', '.git'].includes(f)) continue;
+ const pp = join(d, f); let s; try { s = statSync(pp); } catch { continue; }
+ if (s.isDirectory()) jw(pp, depth + 1);
+ else if (/\.(js|mjs|cjs)$/.test(f)) jsFiles.push(pp);
+ }
+ })(repo);
+ for (const p of jsFiles) {
+ let t; try { t = readFileSync(p, 'utf8'); } catch { continue; }
+ if (/`[^`]*<head[\s>]/i.test(t)) hits.push(p); // <head> inside a backtick template
+ }
+ return hits;
+}
+
+function runOne(repo, domain, id) {
+ if (!existsSync(repo)) return console.log(`MISS ${domain} — repo not found: ${repo}`);
+ const st = injectStatic(repo, id);
+ const tmpl = detectTemplate(repo);
+ console.log(`\n== ${domain} (${id}) — ${repo}`);
+ if (st.changed.length) console.log(` ${COMMIT ? 'INJECTED' : 'would inject'} into ${st.changed.length} page(s):\n ${st.changed.slice(0, 8).map((p) => p.replace(repo, '.')).join('\n ')}${st.changed.length > 8 ? '\n …' : ''}`);
+ if (st.skipped.length) console.log(` skipped ${st.skipped.length} (already tagged / different id)`);
+ if (tmpl.length) console.log(` ⚠ FLAG: ${tmpl.length} JS-template head(s) — needs targeted edit (not auto-injected):\n ${tmpl.slice(0, 5).map((p) => p.replace(repo, '.')).join('\n ')}`);
+ if (!st.changed.length && !st.skipped.length && !tmpl.length) console.log(' no <head> found (SPA shell or non-standard) — manual review');
+}
+
+// ---- main ----
+console.log(COMMIT ? '=== COMMIT ===' : '=== DRY RUN (add --commit to write) ===');
+if (has('--all')) {
+ const reg = existsSync(`${DIR}/measurement-ids.json`) ? JSON.parse(readFileSync(`${DIR}/measurement-ids.json`, 'utf8')) : {};
+ const map = existsSync(`${DIR}/repo-map.json`) ? JSON.parse(readFileSync(`${DIR}/repo-map.json`, 'utf8')) : {};
+ const domains = Object.keys(reg);
+ if (!domains.length) { console.log('measurement-ids.json is empty — run provision.mjs first.'); process.exit(0); }
+ for (const d of domains) {
+ const repo = map[d];
+ if (!repo) { console.log(`MAP? ${d} -> ${reg[d]} — no repo in repo-map.json (add it)`); continue; }
+ runOne(repo, d, reg[d]);
+ }
+} else {
+ const repo = val('--repo'), domain = val('--domain'), id = val('--id');
+ if (!repo || !domain || !id) { console.log('usage: --repo <path> --domain <d> --id G-XXXX [--commit] | --all'); process.exit(1); }
+ runOne(repo, domain, id);
+}
← 9c3e86b provision.mjs: explicit GA4_SA_KEY only — remove secret-scan
·
back to Ga4 Fleet
·
GA4 fleet: repo-map.json (104/402 domains -> local repos; fi a8c9727 →