Ad-hoc UTM concatenation produces utm_source=Email, utm_source=email, and utm_source=newsletter as three separate traffic sources inside GA4, Mixpanel, or Amplitude, which fragments every campaign comparison, corrupts channel-grouping rules, and prevents the conversion-linked-to-send join from matching rows. Inconsistent utm_campaign slugs also break the attribution model's joins against the sends table, so CAC and ROAS calculations understate email's contribution and misdirect budget toward channels with cleaner naming discipline.
High because inconsistent UTMs silently fragment attribution data and misroute channel spend decisions.
Centralize UTM construction in a single buildTrackedUrl(baseUrl, utmParams) helper that lowercases every value, slugifies the campaign name, and rejects free-form strings. Ban manual string concatenation in code review, and grep the repo for utm_source= to find stragglers. Place the helper at src/lib/tracking/utm.ts and import it everywhere.
const url = buildTrackedUrl('https://yourdomain.com/pricing', { source: 'email', medium: 'newsletter', campaign: 'q1-2026-product-update' })
ID: campaign-analytics-attribution.attribution-conversion.utm-parameters-standardized
Severity: high
What to look for: Examine how UTM parameters are appended to links in campaign emails. Check for a UTM builder utility or configuration that enforces consistent parameter naming and values across campaigns. Look for ad-hoc string concatenation of UTMs without validation (which leads to inconsistent naming like utm_source=Email vs utm_source=email vs utm_source=newsletter). Check whether UTM values are lowercase-enforced, whether utm_campaign follows a naming convention (e.g., slugified campaign name), and whether all five parameters are applied consistently.
Pass criteria: A UTM builder function or configuration enforces consistent naming for all five UTM parameters (utm_source, utm_medium, utm_campaign, utm_term, utm_content). Values are normalized (e.g., lowercased). A convention for utm_campaign naming is documented or enforced programmatically. Enumerate all locations where UTM parameters are appended to URLs — at least 90% must use the shared builder.
Fail criteria: UTM parameters are appended ad-hoc per campaign with no shared utility or validation. Inconsistent casing or naming causes the same traffic source to appear as multiple distinct entries in analytics (e.g., "Email" and "email" as separate sources).
Skip (N/A) when: The project does not send links in emails or does not use UTM tracking.
Cross-reference: The Campaign Analytics & Attribution Audit's conversion-linked-to-send check depends on consistent UTM naming for attribution joins.
Detail on fail: Example: "UTM parameters appended by string concatenation without a shared builder — utm_source varies between 'Email', 'email', and 'newsletter' across campaigns" or "No UTM normalization — analytics will show fragmented attribution data"
Remediation: Create a UTM builder that enforces conventions:
interface UtmParams {
source: string
medium: string
campaign: string
term?: string
content?: string
}
function buildTrackedUrl(baseUrl: string, utmParams: UtmParams): string {
const url = new URL(baseUrl)
const normalizedParams: Record<string, string> = {
utm_source: utmParams.source.toLowerCase().trim(),
utm_medium: utmParams.medium.toLowerCase().trim(),
utm_campaign: utmParams.campaign.toLowerCase().replace(/\s+/g, '-').trim()
}
if (utmParams.term) normalizedParams['utm_term'] = utmParams.term.toLowerCase().trim()
if (utmParams.content) normalizedParams['utm_content'] = utmParams.content.toLowerCase().trim()
Object.entries(normalizedParams).forEach(([key, value]) => {
url.searchParams.set(key, value)
})
return url.toString()
}
// Always use this builder, never concatenate UTMs manually
const link = buildTrackedUrl('https://yourdomain.com/pricing', {
source: 'email',
medium: 'newsletter',
campaign: 'q1-2026-product-update'
})