From b5c87c5b32f60cfcf5fb718e58c7f0167443f4c3 Mon Sep 17 00:00:00 2001
From: nish3451 <257724087+nish3451@users.noreply.github.com>
Date: Sun, 9 Aug 2026 21:54:22 +0530
Subject: [PATCH] fix(worker): make the Google Ads conversion tag env-driven
instead of a dead placeholder
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The funnel's only Google Ads conversion measurement was dead by
construction: brief-requested.html hardcoded the gtag loader with a
placeholder conversion id, brief-requested.js fired the event to the same
placeholder, and the production CSP blocked googletagmanager.com entirely
— so even a real id pasted in would never load or record.
The tag is now generated by the worker at request time from
GOOGLE_ADS_CONVERSION_ID / GOOGLE_ADS_CONVERSION_LABEL, emitted only on
/brief-requested when both are configured and well-formed, with gtag's
CSP allowances scoped to that one noindex page's response. With either
value missing or malformed, the page ships with no tag at all. CI now
refuses any placeholder or hardcoded gtag in public/ or src/worker.js, so
the dead-by-construction shape cannot return; the tracking spec documents
the secret puts that activate the tag once the console-side conversion
action exists.
Re-applies PR #52's verified fix onto current main (favicon commits
touched the same files); supersedes the stale conflicting PR #52.
---
public/brief-requested.html | 13 +--
public/brief-requested.js | 15 ++--
scripts/check-site.mjs | 37 +++++++++
scripts/test-agent-worker.mjs | 74 +++++++++++++++++
.../tracking-setup.md | 15 +++-
src/worker.js | 80 ++++++++++++++++++-
6 files changed, 216 insertions(+), 18 deletions(-)
diff --git a/public/brief-requested.html b/public/brief-requested.html
index 75ab04f7..b90da93e 100644
--- a/public/brief-requested.html
+++ b/public/brief-requested.html
@@ -13,13 +13,14 @@
-
-
diff --git a/public/brief-requested.js b/public/brief-requested.js
index f0e1bc73..2bfc0d6a 100644
--- a/public/brief-requested.js
+++ b/public/brief-requested.js
@@ -1,7 +1,8 @@
-window.dataLayer = window.dataLayer || [];
- function gtag(){dataLayer.push(arguments);}
- gtag('js', new Date());
- gtag('config', 'AW-XXXXXXXXX');
- gtag('event', 'conversion', {
- 'send_to': 'AW-XXXXXXXXX/YYYYYYYYYYYYYYYYYYY'
- });
+// Google Ads conversion script.
+//
+// This file's real content is generated by the worker at request time from
+// the GOOGLE_ADS_CONVERSION_ID and GOOGLE_ADS_CONVERSION_LABEL env values —
+// see specs/003-wellness-clinic-launch/tracking-setup.md. A hardcoded tag
+// with a placeholder id is dead by construction (it can never record a
+// conversion), so without those env values this script stays a no-op and
+// nothing fires.
diff --git a/scripts/check-site.mjs b/scripts/check-site.mjs
index 494148f1..ebf535b9 100644
--- a/scripts/check-site.mjs
+++ b/scripts/check-site.mjs
@@ -622,6 +622,43 @@ if (renderBlockingScript) {
}
}
+// ---- Google Ads conversion tag (funnel measurement) ------------------------
+// The funnel's only conversion measurement was dead by construction: the
+// brief-requested page hardcoded a gtag loader with the AW-XXXXXXXXX
+// placeholder, and the production CSP blocked googletagmanager.com entirely,
+// so the event could never record. The tag is now generated by the worker at
+// request time from GOOGLE_ADS_CONVERSION_ID / GOOGLE_ADS_CONVERSION_LABEL
+// and only emitted on /brief-requested when both are configured (see
+// specs/003-wellness-clinic-launch/tracking-setup.md). These STATIC SOURCE
+// GUARDS make the dead-by-construction shape impossible again: no placeholder
+// may exist in public/ or src/worker.js, no public file may hardcode the gtag
+// loader, the static brief-requested.js may not fire anything, and the worker
+// must keep the env-driven injection wired.
+const adsHtml = read("public/brief-requested.html");
+const adsScript = read("public/brief-requested.js");
+for (const placeholder of ["AW-XXXXXXXXX", "YYYYYYYYYYYYYYYYYYY"]) {
+ for (const [label, content] of [
+ ["public/brief-requested.html", adsHtml],
+ ["public/brief-requested.js", adsScript],
+ ["src/worker.js", worker]
+ ]) {
+ if (content.includes(placeholder)) {
+ failures.push(`Google Ads placeholder must never ship (dead conversion): ${placeholder} in ${label}.`);
+ }
+ }
+}
+if (adsHtml.includes("googletagmanager.com")) {
+ failures.push("public/brief-requested.html must not hardcode the Google Ads tag; the worker injects it from env at request time.");
+}
+if (adsScript.includes("gtag(") || adsScript.includes("dataLayer")) {
+ failures.push("public/brief-requested.js must not fire a conversion statically; the worker generates it from env when configured.");
+}
+for (const needle of ["GOOGLE_ADS_CONVERSION_ID", "GOOGLE_ADS_CONVERSION_LABEL", "gtag/js", "/brief-requested.js"]) {
+ if (!worker.includes(needle)) {
+ failures.push(`Worker must keep the env-driven Google Ads conversion wiring (${needle}).`);
+ }
+}
+
if (existsSync(new URL("../public/pipeline-sprint/index.html", import.meta.url))) {
failures.push("Pipeline Sprint page should not remain as a separate stale public asset.");
}
diff --git a/scripts/test-agent-worker.mjs b/scripts/test-agent-worker.mjs
index d90fc891..845e010a 100644
--- a/scripts/test-agent-worker.mjs
+++ b/scripts/test-agent-worker.mjs
@@ -1065,3 +1065,77 @@ test("retired API host frames the current offer as The Website Appraisal, not th
assert.match(body.message, /free leak audit of high-ticket service homepages/, "retired API host message must state the current offer truth");
assert.doesNotMatch(body.message, /self-serve Agent Desk/, "retired API host message must not point at the retired Agent Desk as the current offer");
});
+
+// ---- Google Ads conversion tag (funnel measurement) ------------------------
+// The funnel's only conversion measurement was dead by construction: the
+// brief-requested page hardcoded the AW-XXXXXXXXX placeholder, and the
+// production CSP blocked googletagmanager.com, so the event could never
+// record. The tag must therefore be generated by the worker from env values
+// and only emitted on /brief-requested when both are configured; with either
+// missing or malformed, the page ships with no tag at all.
+const BRIEF_REQUESTED_HTML = '
briefok';
+
+function adsEnv(overrides = {}) {
+ return {
+ ASSETS: {
+ fetch: async () =>
+ new Response(BRIEF_REQUESTED_HTML, {
+ status: 200,
+ headers: { "Content-Type": "text/html; charset=utf-8" }
+ })
+ },
+ GOOGLE_ADS_CONVERSION_ID: "AW-1234567890",
+ GOOGLE_ADS_CONVERSION_LABEL: "AbCdEfGhIjKlMnOpQrSt",
+ ...overrides
+ };
+}
+
+test("brief-requested ships no Google Ads tag while the conversion env is not configured", async () => {
+ const env = adsEnv({ GOOGLE_ADS_CONVERSION_ID: "", GOOGLE_ADS_CONVERSION_LABEL: "" });
+ const res = await worker.fetch(new Request("https://tinystudio.io/brief-requested"), env);
+ assert.equal(res.status, 200);
+ const body = await res.text();
+ assert.ok(!body.includes("googletagmanager.com"), "no tag may ship while unconfigured");
+ const csp = res.headers.get("Content-Security-Policy") || "";
+ assert.ok(!csp.includes("googletagmanager.com"), "CSP must stay strict while unconfigured");
+ const js = await (await worker.fetch(new Request("https://tinystudio.io/brief-requested.js"), env)).text();
+ assert.ok(!js.includes("gtag("), "no conversion event may fire while unconfigured");
+});
+
+test("worker injects the configured Google Ads conversion tag on /brief-requested only", async () => {
+ const env = adsEnv();
+ const res = await worker.fetch(new Request("https://tinystudio.io/brief-requested"), env);
+ assert.equal(res.status, 200);
+ const body = await res.text();
+ assert.ok(
+ body.includes('src="https://www.googletagmanager.com/gtag/js?id=AW-1234567890"'),
+ "gtag loader must be injected with the real conversion id"
+ );
+ assert.ok(body.includes(""), "injected loader must sit inside the head");
+ const csp = res.headers.get("Content-Security-Policy") || "";
+ assert.ok(csp.includes("https://www.googletagmanager.com"), "brief-requested CSP must allow the gtag script origin");
+ assert.ok(csp.includes("googleads.g.doubleclick.net"), "brief-requested CSP must allow the conversion beacon");
+
+ const js = await (await worker.fetch(new Request("https://tinystudio.io/brief-requested.js"), env)).text();
+ assert.ok(js.includes("gtag('config', 'AW-1234567890')"), "generated script must configure the real conversion id");
+ assert.ok(js.includes("AW-1234567890/AbCdEfGhIjKlMnOpQrSt"), "generated script must send the real conversion label");
+
+ // Every other page keeps the strict CSP even when the tag is configured.
+ const other = await worker.fetch(new Request("https://tinystudio.io/pricing.html"), env);
+ const otherCsp = other.headers.get("Content-Security-Policy") || "";
+ assert.ok(!otherCsp.includes("googletagmanager.com"), "other pages must keep the strict CSP");
+});
+
+test("worker refuses to emit a Google Ads tag for a partial or malformed conversion config", async () => {
+ const malformed = [
+ adsEnv({ GOOGLE_ADS_CONVERSION_LABEL: "" }),
+ adsEnv({ GOOGLE_ADS_CONVERSION_ID: "AW-123" }),
+ adsEnv({ GOOGLE_ADS_CONVERSION_ID: "javascript:alert(1)" })
+ ];
+ for (const env of malformed) {
+ const res = await worker.fetch(new Request("https://tinystudio.io/brief-requested"), env);
+ const body = await res.text();
+ assert.ok(!body.includes("googletagmanager.com"), "no tag may emit for a partial or malformed config");
+ assert.equal(body, BRIEF_REQUESTED_HTML, "unconfigured page must pass through untouched");
+ }
+});
diff --git a/specs/003-wellness-clinic-launch/tracking-setup.md b/specs/003-wellness-clinic-launch/tracking-setup.md
index cee09ad5..45d0d28a 100644
--- a/specs/003-wellness-clinic-launch/tracking-setup.md
+++ b/specs/003-wellness-clinic-launch/tracking-setup.md
@@ -66,8 +66,17 @@ I cannot create this without account access. Exact steps:
- Click-through window: **30 days**
- Attribution: **Data-driven**, or last-click if data-driven is unavailable
4. Tag setup → copy the **conversion ID** (`AW-…`) and the **conversion label**
-5. Paste both into `brief-requested.html`, replacing `AW-XXXXXXXXX` and the
- `send_to` label. **Four placeholder occurrences — replace all of them.**
+5. Set both on the Worker so the tag is emitted at request time — no code
+ change, and no placeholder can ever ship as a dead conversion:
+ - `wrangler secret put GOOGLE_ADS_CONVERSION_ID` → the `AW-…` ID
+ - `wrangler secret put GOOGLE_ADS_CONVERSION_LABEL` → the conversion label
+ - (For `wrangler dev --remote`, put both in `.dev.vars` instead.)
+ The Worker validates both (`AW-` + digits; a 10+ character alphanumeric
+ label) and injects the gtag loader + conversion event into
+ `/brief-requested` **only** when both are set and well-formed. With either
+ missing or malformed, the page ships with no tag at all — a dead tag is
+ never served. The CSP allowances for gtag are scoped to that one noindex
+ page's response; every other page keeps the strict CSP.
## 4. GA4 + Ads link
@@ -94,7 +103,7 @@ works is a day of data you cannot use.
- [ ] `htmlRedirect()` in `src/worker.js` points at `/brief-requested`
- [ ] JS success path redirects to `/brief-requested`
- [ ] Conversion action created — Submit lead form, no value, count One, 30 days
-- [ ] `AW-…` ID and label pasted into `brief-requested.html` (all four spots)
+- [ ] `GOOGLE_ADS_CONVERSION_ID` and `GOOGLE_ADS_CONVERSION_LABEL` set on the Worker (`wrangler secret put`; `.dev.vars` for dev)
- [ ] GA4 linked to Ads
- [ ] Live test submission verified as a recorded conversion
- [ ] Negatives loaded, ads approved
diff --git a/src/worker.js b/src/worker.js
index 6986ecf1..73497d49 100644
--- a/src/worker.js
+++ b/src/worker.js
@@ -10,6 +10,17 @@ const SECURITY_HEADERS = {
"default-src 'self'; img-src 'self' data:; style-src 'self' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; script-src 'self' https://static.cloudflareinsights.com; connect-src 'self' https://cloudflareinsights.com; base-uri 'self'; frame-ancestors 'none'; form-action 'self'"
};
+// Page-scoped CSP for /brief-requested ONLY when the Google Ads conversion
+// tag is configured. gtag.js loads from googletagmanager.com and beacons to
+// Google's measurement endpoints; the global CSP above blocks both, which
+// made even a real conversion id dead on arrival. The allowances are scoped
+// to this one noindex page's response so every other page keeps the strict
+// CSP. Only reachable when GOOGLE_ADS_CONVERSION_ID / _LABEL are configured
+// (see googleAdsConversion below); when they are not, the page ships with
+// the strict CSP and no tag at all.
+const GOOGLE_ADS_CSP =
+ "default-src 'self'; img-src 'self' data: https://www.googleadservices.com; style-src 'self' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; script-src 'self' https://static.cloudflareinsights.com https://www.googletagmanager.com; connect-src 'self' https://cloudflareinsights.com https://www.googletagmanager.com https://googleads.g.doubleclick.net https://www.googleadservices.com https://www.google-analytics.com https://stats.g.doubleclick.net; base-uri 'self'; frame-ancestors 'none'; form-action 'self'";
+
const PUBLIC_ASSET_PATHS = new Set([
"/",
"/index.html",
@@ -94,10 +105,10 @@ const WEEKLY_METRIC_LABELS = [
"Cash collected"
];
-function withSecurityHeaders(response) {
+function withSecurityHeaders(response, contentSecurityPolicy) {
const headers = new Headers(response.headers);
for (const [key, value] of Object.entries(SECURITY_HEADERS)) {
- headers.set(key, value);
+ headers.set(key, key === "Content-Security-Policy" && contentSecurityPolicy ? contentSecurityPolicy : value);
}
return new Response(response.body, {
status: response.status,
@@ -1316,6 +1327,41 @@ function isHtmlNavigation(request) {
return (request.method === "GET" || request.method === "HEAD") && accept.includes("text/html");
}
+// ---- Google Ads conversion tag (funnel measurement) -----------------------
+// The funnel's only conversion measurement used to be dead by construction:
+// brief-requested.html shipped a hardcoded gtag loader with a placeholder
+// conversion id, and the production CSP blocked googletagmanager.com
+// entirely, so the event could never record. The tag is now generated at
+// request time from env values and only emitted on /brief-requested when
+// BOTH are configured and well-formed; a partial or malformed config emits
+// nothing rather than a dead tag. The strict patterns also mean the values
+// are safe to interpolate into the generated script.
+const GOOGLE_ADS_ID_PATTERN = /^AW-\d{6,15}$/;
+const GOOGLE_ADS_LABEL_PATTERN = /^[A-Za-z0-9_-]{10,50}$/;
+
+function googleAdsConversion(env) {
+ const id = String(env.GOOGLE_ADS_CONVERSION_ID || "").trim();
+ const label = String(env.GOOGLE_ADS_CONVERSION_LABEL || "").trim();
+ if (!GOOGLE_ADS_ID_PATTERN.test(id) || !GOOGLE_ADS_LABEL_PATTERN.test(label)) return null;
+ return { id, label };
+}
+
+function googleAdsLoader({ id }) {
+ return `
+`;
+}
+
+function googleAdsScript({ id, label }) {
+ return `window.dataLayer = window.dataLayer || [];
+function gtag(){dataLayer.push(arguments);}
+gtag('js', new Date());
+gtag('config', '${id}');
+gtag('event', 'conversion', {
+ 'send_to': '${id}/${label}'
+});
+`;
+}
+
export default {
async fetch(request, env) {
const url = new URL(request.url);
@@ -1342,6 +1388,36 @@ export default {
}
if (PUBLIC_ASSET_PATHS.has(url.pathname)) {
+ const ads = googleAdsConversion(env);
+ const isBriefRequestedPage =
+ url.pathname === "/brief-requested" || url.pathname === "/brief-requested.html";
+ const isBriefRequestedScript = url.pathname === "/brief-requested.js";
+
+ if (ads && request.method === "GET" && (isBriefRequestedPage || isBriefRequestedScript)) {
+ if (isBriefRequestedScript) {
+ return withSecurityHeaders(
+ new Response(googleAdsScript(ads), {
+ headers: { "Content-Type": "text/javascript;charset=UTF-8" }
+ })
+ );
+ }
+ const assetResponse = await env.ASSETS.fetch(request);
+ if (assetResponse.ok) {
+ const html = await assetResponse.text();
+ const rewritten = html.includes("")
+ ? html.replace("", `${googleAdsLoader(ads)}\n`)
+ : html;
+ return withSecurityHeaders(
+ new Response(rewritten, {
+ status: assetResponse.status,
+ statusText: assetResponse.statusText,
+ headers: { "Content-Type": "text/html; charset=utf-8" }
+ }),
+ GOOGLE_ADS_CSP
+ );
+ }
+ }
+
const assetResponse = await env.ASSETS.fetch(request);
return withSecurityHeaders(assetResponse);
}