Learn how to effectively use UTM parameters in mobile apps for campaign tracking. Includes best practices, implementation examples in Flutter and React Native, and how to preserve UTMs through install.
UTM parameters (Urchin Tracking Module) are URL parameters that you add to links to track the source, medium, and campaign that drove traffic to your app. They're a standard way to measure marketing effectiveness.
utm_source
The referrer: google, facebook, twitter, newsletter, etc.
Example: utm_source=google
utm_medium
The marketing medium: cpc, social, email, organic, etc.
Example: utm_medium=cpc
utm_campaign
The campaign name: summer_sale, q1_promotion, etc.
Example: utm_campaign=summer_sale
utm_content
Optional: differentiates similar content. Useful for A/B testing.
Example: utm_content=banner_v2
utm_term
Optional: keyword for paid search campaigns.
Example: utm_term=mobile+app
https://redirectly.app/product/123?utm_source=facebook&utm_medium=social&utm_campaign=summer_sale&utm_content=carousel_ad
This URL tells you: Traffic came from Facebook (source), via social media (medium), for the summer sale campaign (campaign), using a carousel ad (content).
Mobile deep links can include UTM parameters. The format depends on your deep linking scheme:
// Custom scheme myapp://product/123?utm_source=google&utm_medium=cpc&utm_campaign=summer // Universal/App Links https://myapp.com/product/123?utm_source=google&utm_medium=cpc&utm_campaign=summer // Deep link service (Redirectly) https://redirectly.app/link/abc123?utm_source=google&utm_medium=cpc&utm_campaign=summer
When a user clicks a deep link and installs your app for the first time, the URL parameters are lost during the install process. This is called "deferred linking" and requires special handling to preserve UTM data.
Deferred deep linking services capture UTM data before app install and deliver it to your app after installation:
User clicks deep link with UTMs
https://redirectly.app/product/123?utm_source=facebook
Service captures UTM data and redirects to app store
UTMs stored on Redirectly servers
User installs app
App opens for first time
App retrieves stored UTM data
UTMs delivered to app and route user accordingly
import 'package:flutter/material.dart';
import 'package:uni_links/uni_links.dart';
import 'dart:async';
class UTMTracker {
static Future<Map<String, String>> extractUTMParameters(Uri uri) async {
final params = <String, String>{};
// Extract UTM parameters from query string
final source = uri.queryParameters['utm_source'] ?? '';
final medium = uri.queryParameters['utm_medium'] ?? '';
final campaign = uri.queryParameters['utm_campaign'] ?? '';
final content = uri.queryParameters['utm_content'] ?? '';
final term = uri.queryParameters['utm_term'] ?? '';
if (source.isNotEmpty) params['utm_source'] = source;
if (medium.isNotEmpty) params['utm_medium'] = medium;
if (campaign.isNotEmpty) params['utm_campaign'] = campaign;
if (content.isNotEmpty) params['utm_content'] = content;
if (term.isNotEmpty) params['utm_term'] = term;
return params;
}
static Future<void> trackUTMs(Uri uri) async {
final utms = await extractUTMParameters(uri);
if (utms.isNotEmpty) {
print('UTM Parameters: $utms');
// Send to analytics
_sendToAnalytics(utms);
}
}
static void _sendToAnalytics(Map<String, String> utms) {
// Example: Send to Firebase Analytics or your analytics service
// analytics.logEvent(
// name: 'app_opened_via_utm',
// parameters: utms,
// );
}
}
class DeepLinkHandler {
static Future<void> initialize() async {
// Handle initial deep link (cold start)
final String? initialLink = await getInitialLink();
if (initialLink != null) {
final uri = Uri.parse(initialLink);
await UTMTracker.trackUTMs(uri);
}
// Listen for deep links when app is running
linkStream.listen(
(String link) async {
final uri = Uri.parse(link);
await UTMTracker.trackUTMs(uri);
},
onError: (err) {
debugPrint('Deep link error: $err');
},
);
}
}Redirectly automatically preserves UTM parameters through install:
import 'package:redirectly_flutter/redirectly_flutter.dart';
class MyApp extends StatefulWidget {
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@override
void initState() {
super.initState();
_initializeRedirectly();
}
Future<void> _initializeRedirectly() async {
// Initialize Redirectly
await Redirectly.init(apiKey: 'your-api-key');
// Listen for deep links with UTM parameters
Redirectly.onDeepLink((deepLink) {
final path = deepLink.path;
final campaign = deepLink.campaign;
// deepLink.data contains all UTM parameters
final utmSource = deepLink.data['utm_source'];
final utmMedium = deepLink.data['utm_medium'];
final utmCampaign = deepLink.data['utm_campaign'];
print('Path: $path');
print('Campaign: $campaign');
print('UTM Source: $utmSource');
print('UTM Medium: $utmMedium');
print('UTM Campaign: $utmCampaign');
// Route user and track analytics
_handleDeepLink(deepLink);
});
}
void _handleDeepLink(RedirectlyDeepLink deepLink) {
// Route to correct screen based on path
// Log attribution data for analytics
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'My App',
home: const HomeScreen(),
);
}
}import { useEffect } from 'react';
import { useNavigation } from '@react-navigation/native';
import { Redirectly } from '@redirectly/react-native';
interface UTMData {
utm_source?: string;
utm_medium?: string;
utm_campaign?: string;
utm_content?: string;
utm_term?: string;
}
export const useUTMTracking = () => {
const navigation = useNavigation();
useEffect(() => {
// Handle initial deep link on app launch
Redirectly.getInitialLink().then((deepLink) => {
if (deepLink) {
handleDeepLinkWithUTM(deepLink);
}
});
// Listen for deep links when app is running
const unsubscribe = Redirectly.onDeepLink((deepLink) => {
handleDeepLinkWithUTM(deepLink);
});
return unsubscribe;
}, []);
const handleDeepLinkWithUTM = (deepLink: DeepLinkData) => {
const { path, campaign, data } = deepLink;
// Extract UTM parameters
const utm: UTMData = {
utm_source: data?.utm_source,
utm_medium: data?.utm_medium,
utm_campaign: data?.utm_campaign,
utm_content: data?.utm_content,
utm_term: data?.utm_term,
};
// Log UTM data for analytics
console.log('Deep Link UTM Data:', utm);
// Send to analytics service
logToAnalytics(utm);
// Route to correct screen
if (path.startsWith('/product/')) {
const productId = path.split('/')[2];
navigation.navigate('ProductScreen', {
productId,
utm
});
} else if (path.startsWith('/promo/')) {
navigation.navigate('PromoScreen', { utm });
} else {
navigation.navigate('Home', { utm });
}
};
return null;
};
const logToAnalytics = (utm: UTMData) => {
// Send to your analytics service
// Example: Firebase Analytics, Amplitude, Mixpanel
if (utm.utm_source || utm.utm_campaign) {
console.log('Logging UTM attribution:', utm);
// analytics.logEvent('deep_link_opened', utm);
}
};Create a company-wide naming convention for UTM values. Use consistent source and medium names across all campaigns.
Use Google Analytics URL Builder or similar tools to generate UTM links. This reduces typos and ensures consistency.
Send UTM data to your analytics service on app open. This creates a complete view of user acquisition and engagement.
Implement deferred deep linking to preserve UTM parameters through app installs. This ensures attribution works even for new users.
Regularly review your UTM data for completeness and accuracy. Check for missing or inconsistent values that might indicate problems.
Redirectly makes it easy to preserve and track UTM parameters through app install with deferred deep linking.