frequently misconfigured channels, the official conditions are:
- Email:
Source OR Medium matches email|e-mail|e_mail|e mail
- SMS:
Source OR Medium exactly matches sms
- Mobile Push:
Medium ends with push, OR Medium contains mobile or notification, OR Source equals firebase
Step 2: Build a Validation Pipeline
Instead of relying on manual checks, implement a validation layer that intercepts UTM generation and rejects non-compliant parameters before they reach production. This prevents schema drift across teams and tools.
type ChannelRule = {
name: string;
sourcePattern?: RegExp;
mediumPattern?: RegExp;
exactMatch?: 'source' | 'medium';
composite?: boolean;
};
const GA4_CHANNEL_RULES: Record<string, ChannelRule> = {
email: {
name: 'Email',
sourcePattern: /^email|e-mail|e_mail|e mail$/i,
mediumPattern: /^email|e-mail|e_mail|e mail$/i,
},
sms: {
name: 'SMS',
exactMatch: 'source',
sourcePattern: /^sms$/,
mediumPattern: /^sms$/,
},
mobile_push: {
name: 'Mobile Push',
composite: true,
mediumPattern: /push$|mobile|notification/i,
sourcePattern: /^firebase$/i,
},
};
interface UTMParams {
source: string;
medium: string;
campaign?: string;
content?: string;
term?: string;
}
class ChannelClassifier {
private rules: Record<string, ChannelRule>;
constructor(rules: Record<string, ChannelRule>) {
this.rules = rules;
}
classify(params: UTMParams): string {
for (const rule of Object.values(this.rules)) {
if (this.matchesRule(rule, params)) {
return rule.name;
}
}
return 'Unassigned';
}
private matchesRule(rule: ChannelRule, params: UTMParams): boolean {
if (rule.exactMatch === 'source') {
return rule.sourcePattern?.test(params.source) ?? false;
}
if (rule.exactMatch === 'medium') {
return rule.mediumPattern?.test(params.medium) ?? false;
}
if (rule.composite) {
const sourceMatch = rule.sourcePattern?.test(params.source) ?? false;
const mediumMatch = rule.mediumPattern?.test(params.medium) ?? false;
return sourceMatch || mediumMatch;
}
const sourceMatch = rule.sourcePattern?.test(params.source) ?? false;
const mediumMatch = rule.mediumPattern?.test(params.medium) ?? false;
return sourceMatch || mediumMatch;
}
}
export { ChannelClassifier, GA4_CHANNEL_RULES, UTMParams };
Step 3: Enforce Generation Standards
Replace free-form UTM builders with a factory that enforces the validated schema. This guarantees that every generated link satisfies at least one channel rule.
class UTMFactory {
private classifier: ChannelClassifier;
constructor() {
this.classifier = new ChannelClassifier(GA4_CHANNEL_RULES);
}
createEmailLink(source: string, campaign: string, content?: string): UTMParams {
const params: UTMParams = {
source,
medium: 'email',
campaign,
content,
};
this.validateOrThrow(params);
return params;
}
createSMSLink(source: string, campaign: string): UTMParams {
const params: UTMParams = {
source,
medium: 'sms',
campaign,
};
this.validateOrThrow(params);
return params;
}
createPushLink(source: string, medium: string, campaign: string): UTMParams {
const params: UTMParams = {
source,
medium,
campaign,
};
this.validateOrThrow(params);
return params;
}
private validateOrThrow(params: UTMParams): void {
const result = this.classifier.classify(params);
if (result === 'Unassigned') {
throw new Error(
`UTM parameters do not match any GA4 channel rule. Source: ${params.source}, Medium: ${params.medium}`
);
}
}
}
export { UTMFactory };
Architecture Decisions and Rationale
- Why separate validation from generation? Decoupling allows the classifier to be reused across CI/CD link audits, marketing platform integrations, and runtime validation. Generation factories remain lightweight and focused on business logic.
- Why strict regex over fuzzy matching? GA4's engine is deterministic. Fuzzy matching introduces false positives that corrupt benchmarking. Strict patterns guarantee parity with Google's specification.
- Why throw on
Unassigned? Failing fast during development prevents silent attribution leaks in production. Marketing teams receive immediate feedback instead of discovering missing data 30 days later.
- Why cache rules? The classification specification is static. Caching eliminates repeated regex compilation and improves throughput in high-volume link generation pipelines.
Pitfall Guide
1. Assuming Case-Insensitivity for SMS
Explanation: The SMS channel requires an exact match on sms. GA4 evaluates parameters case-sensitively. SMS, Sms, or sMs will fail the condition and route to Unassigned.
Fix: Enforce lowercase sms in all generation templates. Add a normalization step that lowercases utm_medium before validation.
2. Using Descriptive Mediums Like newsletter or blast
Explanation: Human-readable labels feel intuitive but violate the regex pattern email|e-mail|e_mail|e mail. The engine does not infer semantic meaning.
Fix: Reserve utm_medium for classification only. Use utm_content or utm_campaign for descriptive metadata like newsletter, promo, or welcome_flow.
3. Leaving utm_medium Blank
Explanation: When utm_medium is omitted, GA4 cannot evaluate the medium-side condition. If utm_source also lacks the required substring, the session drops to Referral or Unassigned.
Fix: Make utm_medium a required field in all link generation forms. Implement schema validation that rejects payloads missing the medium parameter.
4. Treating Messaging Apps as SMS
Explanation: LINE, WhatsApp, Telegram, and Messenger operate over internet protocols, not cellular SMS gateways. GA4's SMS rule only triggers on the literal string sms. Messaging apps route to Referral or Organic Social based on the Source Categories table.
Fix: Track messaging apps separately using utm_source=whatsapp or utm_source=line. If cross-channel messaging aggregation is required, build a Custom Channel Group rather than forcing them into SMS.
5. Overcomplicating utm_source with Campaign IDs
Explanation: Embedding campaign identifiers in utm_source (e.g., utm_source=email_q2_launch) breaks the email substring match. The regex expects clean source identifiers.
Fix: Keep utm_source reserved for the delivery platform or vendor (mailchimp, sendgrid, twilio). Use utm_campaign for temporal or thematic identifiers.
6. Ignoring the firebase Push Exception
Explanation: Mobile Push has three independent OR conditions. Teams often focus only on utm_medium patterns and forget that utm_source=firebase alone qualifies the channel, regardless of the medium value.
Fix: Document all three push triggers in team runbooks. When using Firebase Cloud Messaging, ensure utm_source is explicitly set to firebase to guarantee classification.
7. Relying on GA4 Auto-Tagging for Non-Google Sources
Explanation: Auto-tagging only applies to Google Ads. Email, SMS, and push campaigns require manual UTM attachment. Assuming GA4 will auto-classify these channels leads to consistent Unassigned traffic.
Fix: Implement mandatory UTM injection at the ESP, SMS gateway, or push provider level. Use webhooks or API integrations to append parameters automatically before link distribution.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| High-volume email campaigns | utm_medium=email with ESP-specific utm_source | Guarantees Email channel classification while preserving vendor segmentation | Low (schema update only) |
| Transactional SMS alerts | utm_medium=sms with gateway utm_source | Exact match required; case sensitivity prevents silent drops | Low (normalization step) |
| Cross-platform messaging (WhatsApp/LINE) | Custom Channel Group or Referral tracking | Default SMS rule excludes internet-based messaging apps | Medium (custom report setup) |
| Firebase push notifications | utm_source=firebase or utm_medium ending in push | Leverages composite OR conditions for reliable classification | Low (parameter injection) |
| Legacy campaign links with broken UTMs | Redirect mapping + UTM correction at edge | Prevents traffic loss while maintaining historical data integrity | High (CDN/edge config) |
Configuration Template
// ga4-channel-schema.ts
export const GA4_CHANNEL_SCHEMA = {
email: {
required_medium: 'email',
acceptable_mediums: ['email', 'e-mail', 'e_mail', 'e mail'],
source_restriction: 'none',
notes: 'Source can be any ESP identifier. Medium must match one of the four variants.',
},
sms: {
required_medium: 'sms',
acceptable_mediums: ['sms'],
source_restriction: 'none',
notes: 'Exact case-sensitive match. Uppercase SMS will fail.',
},
mobile_push: {
required_medium: 'none',
acceptable_mediums: ['*push', '*mobile*', '*notification*'],
source_restriction: 'firebase',
notes: 'Three OR conditions: medium ends with push, contains mobile/notification, or source equals firebase.',
},
};
export type ChannelName = keyof typeof GA4_CHANNEL_SCHEMA;
export function validateUTMForChannel(
channel: ChannelName,
source: string,
medium: string
): boolean {
const schema = GA4_CHANNEL_SCHEMA[channel];
if (channel === 'sms') {
return medium === 'sms';
}
if (channel === 'mobile_push') {
const sourceMatch = source.toLowerCase() === 'firebase';
const mediumEndsPush = medium.toLowerCase().endsWith('push');
const mediumContainsMobile = medium.toLowerCase().includes('mobile');
const mediumContainsNotification = medium.toLowerCase().includes('notification');
return sourceMatch || mediumEndsPush || mediumContainsMobile || mediumContainsNotification;
}
if (channel === 'email') {
const validMediums = schema.acceptable_mediums.map(m => m.toLowerCase());
return validMediums.includes(medium.toLowerCase());
}
return false;
}
Quick Start Guide
- Install the classifier: Copy the
ChannelClassifier and UTMFactory modules into your shared utilities package. Export them for use across marketing and engineering codebases.
- Replace free-form builders: Locate all legacy UTM generation functions. Swap them with
UTMFactory.createEmailLink(), createSMSLink(), or createPushLink() to enforce schema compliance at runtime.
- Add validation to CI/CD: Create a GitHub Action or GitLab CI step that scans markdown files, email templates, and configuration JSON for UTM parameters. Run them through
validateUTMForChannel() and fail the pipeline on mismatches.
- Verify in GA4: After deployment, navigate to Acquisition > Traffic acquisition. Apply a 30-day date range and add Session source/medium as a secondary dimension. Confirm that new campaigns route to the expected channels and
Unassigned volume declines.
- Document team standards: Publish a UTM naming convention wiki page referencing the three channel rules. Require all campaign owners to validate links against the factory before distribution.