Skip to content

fix(worker): make the Google Ads conversion tag env-driven instead of a dead placeholder - #136

Closed
nish3451 wants to merge 8 commits into
mainfrom
lane1/ads-tag-env-driven
Closed

fix(worker): make the Google Ads conversion tag env-driven instead of a dead placeholder#136
nish3451 wants to merge 8 commits into
mainfrom
lane1/ads-tag-env-driven

Conversation

@nish3451

@nish3451 nish3451 commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Why

The funnel's only Google Ads conversion measurement is dead by construction: brief-requested.html hardcodes a gtag loader with the AW-XXXXXXXXX placeholder ID, brief-requested.js fires the event to the same placeholder, and the production CSP blocks googletagmanager.com entirely — so even a real ID pasted in would never load or record.

What

The tag is generated by the worker at request time from GOOGLE_ADS_CONVERSION_ID / GOOGLE_ADS_CONVERSION_LABEL env values:

  • Emitted only on /brief-requested (and its generated brief-requested.js) when both values are configured and well-formed (AW- + digits; 10+ alphanumeric label).
  • No tag at all when either value is missing or malformed — a dead tag can never ship.
  • CSP allowances for gtag are scoped to that one noindex page's response; every other page keeps the strict CSP. The global CSP previously blocked gtag entirely, which is the second half of "dead by construction".
  • CI guards (check-site.mjs) now refuse any placeholder or hardcoded gtag in public/ or src/worker.js, so the dead shape cannot return.
  • tracking-setup.md documents the activation step: create the Brief requested conversion action in Google Ads, then wrangler secret put the ID/label — no code change needed to go live.

Verification

  • npm test — full suite green (95 tests, 0 failures), including 3 new worker tests: no tag while unconfigured; tag + scoped CSP on /brief-requested only; partial/malformed config emits nothing.
  • npm run check:render-blocking — all six pages PASS.
  • npm run deploy:dry-run — bundle OK.

Supersedes PR #52 (same verified fix, re-applied onto current main after the favicon commits conflicted with that branch).

Summary by CodeRabbit

  • Enhancements
    • Google Ads conversion tracking now activates on the thank-you page only when valid tracking settings are configured.
    • Tracking is limited to the intended page and does not affect other site pages.
    • When settings are missing, incomplete, or invalid, no conversion event is sent.
  • Security
    • Content security settings are applied narrowly to support tracking without broadly relaxing site protections.
  • Documentation
    • Updated setup guidance to reflect the required tracking configuration.

… a dead placeholder

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.

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nish3451 has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 08985f93-207d-49c8-b15c-475fb7d8604b

📥 Commits

Reviewing files that changed from the base of the PR and between b5c87c5 and e46a81e.

📒 Files selected for processing (1)
  • scripts/check-site.mjs
🚧 Files skipped from review as they are similar to previous changes (1)
  • scripts/check-site.mjs

📝 Walkthrough

Walkthrough

The worker reads validated Google Ads conversion values from environment secrets and injects tracking only into /brief-requested responses. Public assets no longer contain hardcoded tags. Static checks and worker tests cover configured and disabled tracking paths.

Changes

Google Ads tracking

Layer / File(s) Summary
Worker configuration and security headers
src/worker.js
The worker validates conversion ID and label values, generates the loader and conversion event, and applies a page-specific CSP.
Thank-you page request handling
src/worker.js, public/brief-requested.html, public/brief-requested.js, specs/003-wellness-clinic-launch/tracking-setup.md
/brief-requested receives runtime-generated tracking when configuration is valid. Public assets and setup instructions no longer use embedded placeholders.
Static and runtime validation
scripts/check-site.mjs, scripts/test-agent-worker.mjs
Static checks reject hardcoded tracking. Tests cover valid, missing, malformed, route-specific, and fallback behavior.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Browser
  participant Worker
  participant WorkerSecrets
  participant GoogleAds
  Browser->>Worker: Request /brief-requested
  Worker->>WorkerSecrets: Read conversion ID and label
  WorkerSecrets-->>Worker: Valid or unavailable configuration
  Worker-->>Browser: HTML or generated JavaScript with page-scoped CSP
  Browser->>GoogleAds: Load tag and send conversion event when configured
Loading

Possibly related PRs

  • nish3451/TinyStudio.io#8: Introduced the hardcoded /brief-requested Google Ads tracking implementation that this PR replaces.
  • nish3451/TinyStudio.io#52: Covers related environment-driven Google Ads tracking changes in the same worker and public assets.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: making the Google Ads conversion tag environment-driven in the worker.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch lane1/ads-tag-env-driven

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/check-site.mjs`:
- Around line 637-655: Update the tracking validation around the
adsHtml/adsScript checks to enumerate every file under public/ rather than only
the two named assets. Scan each asset for the existing Google Ads placeholders,
Google Tag Manager references, and static tracking code using
whitespace-tolerant gtag call detection and dataLayer references, while
retaining the worker checks and failure reporting with the affected asset name.

In `@scripts/test-agent-worker.mjs`:
- Around line 1129-1140: Extend the malformed fixtures in the worker test to
include a non-empty invalid GOOGLE_ADS_CONVERSION_LABEL, such as a too-short or
quote-containing value. For that fixture, assert both /brief-requested and
/brief-requested.js responses contain no Google Ads tracking code, while
preserving the existing untouched HTML assertion for /brief-requested.

In `@specs/003-wellness-clinic-launch/tracking-setup.md`:
- Around line 74-75: Update the label validation description in the tracking
setup documentation to match GOOGLE_ADS_LABEL_PATTERN: document that labels are
10–50 characters and may contain letters, digits, underscores, and hyphens. Keep
the existing AW- plus digits rule unchanged.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: cb1414ac-0855-4e69-9204-776d0e50586e

📥 Commits

Reviewing files that changed from the base of the PR and between 18128e8 and b5c87c5.

📒 Files selected for processing (6)
  • public/brief-requested.html
  • public/brief-requested.js
  • scripts/check-site.mjs
  • scripts/test-agent-worker.mjs
  • specs/003-wellness-clinic-launch/tracking-setup.md
  • src/worker.js

Comment thread scripts/check-site.mjs
Comment on lines +637 to +655
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.");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Scan all public assets for static tracking code.

Lines 637-643 inspect only two files under public/. A hardcoded tag in another public asset will pass CI. The exact gtag( check also misses whitespace variants such as gtag (.

Enumerate all files under public/. Use syntax-tolerant patterns for gtag calls and dataLayer references.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/check-site.mjs` around lines 637 - 655, Update the tracking
validation around the adsHtml/adsScript checks to enumerate every file under
public/ rather than only the two named assets. Scan each asset for the existing
Google Ads placeholders, Google Tag Manager references, and static tracking code
using whitespace-tolerant gtag call detection and dataLayer references, while
retaining the worker checks and failure reporting with the affected asset name.

Comment on lines +1129 to +1140
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");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Add a malformed conversion-label fixture.

The fixtures test a missing label, but none tests a non-empty invalid GOOGLE_ADS_CONVERSION_LABEL. Add a malformed label such as a too-short value or a value containing a quote. Verify that both /brief-requested and /brief-requested.js emit no tracking code.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/test-agent-worker.mjs` around lines 1129 - 1140, Extend the malformed
fixtures in the worker test to include a non-empty invalid
GOOGLE_ADS_CONVERSION_LABEL, such as a too-short or quote-containing value. For
that fixture, assert both /brief-requested and /brief-requested.js responses
contain no Google Ads tracking code, while preserving the existing untouched
HTML assertion for /brief-requested.

Comment on lines +74 to +75
The Worker validates both (`AW-` + digits; a 10+ character alphanumeric
label) and injects the gtag loader + conversion event into

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Match the documented label rule to GOOGLE_ADS_LABEL_PATTERN.

The text says the label is “10+ character alphanumeric.” src/worker.js accepts 10-50 letters, digits, _, and -. Document the implemented range so operators have an accurate configuration rule.

Proposed fix
-   The Worker validates both (`AW-` + digits; a 10+ character alphanumeric
-   label) and injects the gtag loader + conversion event into
+   The Worker validates both (`AW-` + 6-15 digits; a 10-50 character label
+   containing letters, digits, `_`, or `-`) and injects the gtag loader + conversion event into
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
The Worker validates both (`AW-` + digits; a 10+ character alphanumeric
label) and injects the gtag loader + conversion event into
The Worker validates both (`AW-` + 6-15 digits; a 10-50 character label
containing letters, digits, `_`, or `-`) and injects the gtag loader + conversion event into
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@specs/003-wellness-clinic-launch/tracking-setup.md` around lines 74 - 75,
Update the label validation description in the tracking setup documentation to
match GOOGLE_ADS_LABEL_PATTERN: document that labels are 10–50 characters and
may contain letters, digits, underscores, and hyphens. Keep the existing AW-
plus digits rule unchanged.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b5c87c5b32

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/worker.js
// (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'";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Permit Ads image beacons in img-src

When gtag sends the conversion or view-through pixel as an image request to googleads.g.doubleclick.net, this policy silently blocks it: that host appears only in connect-src, while img-src allows only www.googleadservices.com. CSP permissions do not carry across directives, so the loader can run while some conversion requests are still discarded; add the Google Ads pixel hosts to img-src and assert their placement rather than merely checking that the hostname occurs somewhere in the policy.

Useful? React with 👍 / 👎.

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nish3451 has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nish3451 has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nish3451 has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 60379b7d26

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/worker.js
);
}
const assetResponse = await env.ASSETS.fetch(request);
if (assetResponse.ok) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Rewrite conditional thank-you responses before returning 304

When tracking is enabled after an unconfigured visit, the browser can revalidate its cached static /brief-requested HTML with If-None-Match; the Assets binding then returns 304, for which assetResponse.ok is false. This branch consequently skips loader injection and returns another 304, so the browser reuses the cached loader-free HTML while the generated brief-requested.js merely queues calls to a gtag library that was never loaded, losing that conversion. Fetch the source without the conditional headers or otherwise ensure the configured response is rewritten even during revalidation.

Useful? React with 👍 / 👎.

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nish3451 has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nish3451 has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nish3451 has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your trial has ended. Reactivate Greptile to resume code reviews.

nish3451 added a commit that referenced this pull request Aug 13, 2026
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
nish3451 added a commit that referenced this pull request Aug 13, 2026
… a dead placeholder (#172)

* fix(worker): make the Google Ads conversion tag env-driven instead of a dead placeholder

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.

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>

* docs(evidence): record lane report for the env-driven Google Ads conversion tag fix

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>

* docs(evidence): note superseded stale PR #136 in the lane report

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>

---------

Co-authored-by: nish3451 <nish3451@users.noreply.github.com>
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
@nish3451

Copy link
Copy Markdown
Collaborator Author

Closing: superseded by #172 (env-driven ads landed on main).

@nish3451 nish3451 closed this Aug 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant