Skip to content

Build tracking URLs from a shared prefix

Your tracking convention says every ad’s final URL ends in its own slug — utm_campaign=GH_Cookies_0708-rsa_a, …-rsa_b, …-sitelink_help — so no two ads share a URL, but everything before the slug is identical. Pasting the full URL into every ad is how campaigns end up shipping with a URL copied from last month’s campaign, quietly mis-attributing traffic.

Put the shared part in a local and build each URL from it with ${…} interpolation:

locals {
utm_base = "https://www.ghostery.com/ghostery-ad-blocker?utm_source=search&utm_campaign=GH_Cookies_0708"
}
resource "google_ads_ad_group_ad" "rsa_a" {
ad_group = google_ads_ad_group.popups.id
ad {
final_urls = ["${local.utm_base}-rsa_a"]
responsive_search_ad {
headlines = local.popup_headlines
descriptions = local.popup_descriptions
}
}
}
resource "google_ads_ad_group_ad" "rsa_b" {
ad_group = google_ads_ad_group.popups.id
ad {
final_urls = ["${local.utm_base}-rsa_b"]
responsive_search_ad {
headlines = local.consent_headlines
descriptions = local.consent_descriptions
}
}
}

Change the campaign tag or the landing page once, in the local, and every ad picks it up. bidsmith plan shows the fan-out as one in-place update per ad before anything changes live.

Where interpolation works

Anywhere a string is accepted — resource names, headlines, paths, URLs — including inside locals themselves, so you can build one local from another:

locals {
campaign_tag = "GH_Cookies_0708"
landing = "https://www.ghostery.com/ghostery-ad-blocker"
utm_base = "${local.landing}?utm_source=search&utm_campaign=${local.campaign_tag}"
}
resource "google_ads_campaign" "cookies" {
name = "Search — ${local.campaign_tag}"
advertising_channel_type = "SEARCH"
campaign_budget = google_ads_campaign_budget.cookies.id
}

Numbers and booleans interpolate too ("radius-${local.km}km"). Validation runs on the rendered string, so a headline that only exceeds the 30-character cap once the brand name is spliced in still gets flagged before it reaches Google Ads.

Let Google append the tracking instead

Interpolation keeps the source tidy, but every ad still ships a URL with the UTM query baked in. Google Ads has a field for exactly this job: final_url_suffix is a query string Google appends at click time, so the campaign can declare the convention once and each ad supply only what varies.

resource "google_ads_campaign" "cookies" {
name = "Search — GH_Cookies_0708"
advertising_channel_type = "SEARCH"
campaign_budget = google_ads_campaign_budget.cookies.id
final_url_suffix = "utm_source=search&utm_campaign=GH_Cookies_0708-{_slug}"
}
resource "google_ads_ad_group_ad" "cookies_rsa_a" {
ad_group = google_ads_ad_group.cookies.id
template = ad_template.cookies_rsa
ad {
final_urls = ["https://www.ghostery.com/ghostery-ad-blocker"]
custom_parameters = { slug = "rsa_a" }
}
}

{_slug} is a custom parameter: the custom_parameters map fills it in, and the leading underscore is Google’s, not a typo. Set the pair on a campaign, an ad group, or an ad — the most specific one wins, so a campaign-wide convention can be overridden for one ad group without touching the rest.

The payoff beyond brevity: a convention declared in one place is a convention you can review. A typo in utm_campaign pasted into 60 URLs breaks attribution silently and is invisible in a diff; the same typo in one final_url_suffix is one line in one pull request.

See also