Push UTM Tracking on iOS for GA4
Emin Deniz · August 5, 2024 · 9 min read
Introduction
In today’s data-driven world, understanding the source of your app traffic is crucial for making informed marketing decisions. This is where UTM (Urchin Tracking Module) parameters come into play. UTM parameters are simple snippets of text added to URLs that help track the effectiveness of online marketing campaigns. By using UTM parameters, you can gain insights into which campaigns are driving traffic and conversions. Google Analytics 4 (GA4), the latest version of Google Analytics, offers enhanced tracking and analysis capabilities. This capability usually works out of the box for deep links if you do the right configuration in GA4. However, tracking campaign data from push notifications in iOS requires a bit of manual setup. This article will guide you through the process of sending UTM parameters to GA4 after a push notification is received in an iOS app.
What are UTM Parameters?
UTM parameters are tags you can add to the URLs that you use in your marketing campaigns. They consist of five variants of URL parameters you can track: source, medium, campaign, term, and content. Here’s a brief overview of each:
- utm_source: Identifies the source of your traffic, such as a search engine (e.g., Google) or a newsletter.
- utm_medium: Indicates the marketing medium, such as email or CPC (cost per click).
- utm_campaign: Defines the campaign name to track different marketing efforts.
- utm_term (optional): Used for paid search to identify the keywords for your ad.
- utm_content (optional): Differentiates similar content or links within the same ad, helping to track specific elements of a campaign.
These parameters help you dissect your traffic data in Google Analytics, giving you granular insights into your marketing efforts.
Sending Manual Campaign Details Event to GA4
To manually send campaign details to GA4, you can use the logEvent
method provided by the Firebase Analytics SDK. Here’s a general approach:
- Initialize Firebase in Your App: Ensure Firebase is properly set up in your iOS app.
- Retrieve UTM Parameters: Extract UTM parameters from the push notification data.
- Log an Event to GA4: Use the
logEvent
method to send the campaign details to GA4.
Although the steps I explained above seem quite straightforward, both Firebase and GA4 docs are not super clear about what should be the format of the event payload.
In this article, I will explain what is missing in the documents and create together a solution that can achieve this purpose.
Let’s first create a function in our iOS app to send this event per GA4 and Firebase docs.
func sendCampaignDetailsEvent(){
var campaignParams: [String:Any] = [
"campaign": "FancySummerCampaign",
"source": "SourceFromGoogle",
"medium": "email",
"term": "summer+travel",
"content": "logolink"
]
Analytics.logEvent("campaign_details", parameters:campaignParams)
}
What is not mentioned in the Firebase and GA4 documents is this event needs to be a Key Event to be visible on the GA4 Traffics.
I can hear your confusion because I also felt it!
According to Firebase documentation, a Key Event (formerly known as Conversation Event) is an event that measures an action that’s particularly important to the success of your business. This means if an event causes your app to open the first time, making some purchases or subscriptions, your business success increases. Here are the 5 predefined Key Events:
- first_open
- in_app_purchase
- app_store_subscription_convert
- app_store_subscription_renew
- purchase
But the event we are trying to send is campaign_details which is not in the list above. So we have 2 options (My suggestion is the second one).
The first one is to create a custom Key Event, let’s call it campaign_details_custom
. Then we can use the function above to log the event like Analytics.logEvent("campaign_details_custom", parameters:campaignParameters)
. You may ask “How can I create a custom Key Event?”. Here is an amazing article to explain how you can create a Key Event. In the article, it is called Custom Conversation because Google just wants to change its name to confuse us even more 😪.
The second approach, which seems to be a more suitable approach, is to use campaign_details
and provide 2 additional information:
- session_id: The id that identifies the session in Firebase.
- user_id: An id to identify the user. This value must be provided by the application and can be any number. But it must be provided as a string.
Let’s update the function above with those parameters.
func sendCampaignDetailsEvent(sessionID: String, userID: String){
var campaignParams: [String:Any] = [
"campaign": "FancySummerCampaign",
"source": "SourceFromGoogle",
"medium": "email",
"term": "summer+travel",
"content": "logolink",
"session_id": sessionID,
"user_id": userID
]
Analytics.logEvent("campaign_details", parameters:campaignParams)
}
Ok, we have a working function to report the campaign_detail event now.
Providing the Session ID
When you search Google for “Firebase Session ID” you probably saw the Firebase Authentication ID. But that is not what we are looking for, we only care about Analytics in this article’s scope. Here is how you can obtain the session ID.
let sessionID = try await Analytics.sessionID()
You may ask “Why did you create a section just for one line of code?”. This API may not always return the sessionID, especially if the app has just started. Because push notifications may start the app and we aim to send the UTM via push this API is not always reliable.
Even though this is unfortunate, we can workaround it by a retry logic. It is not an ideal solution but at least we will have a trustable solution. Let’s create another function with retry.
func sendCampaignDetailsWithRetry(maxAttempts: Int, attempt: Int = 0, userID: String) {
guard attempt < maxAttempts else {
// We have tried but no luck 🫤
return
}
Task {
if let sessionID = try? await Analytics.sessionID() {
// Found the sessionId 🙂
sendCampaignDetailsEvent(sessionID: sessionID, userID: userID)
}else {
// Let's retry in 0.5 secs later
try? await Task.sleep(nanoseconds: 500_000_000)
self.sendCampaignWithRetry(maxAttempts: maxAttempts, attempt: attempt + 1)
}
}
}
Cool, we have a fully working solution to send campaign_details
now.
Fully Working Solution UTM Sending After Push
I assume you already have push notification support in your app and you have already integrated with Firebase. If not, there are thousands of good article explains how you can achieve both on the internet. For the sake of simplicity let’s do all the work in AppDelegate, which I don’t suggest for production quality code. Let’s merge what we talked above with the push notification setup you already have.
import Firebase
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
FirebaseApp.configure()
return true
}
func application(_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable: Any],
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
if let aps = userInfo["aps"] as? [String: AnyObject] {
handleNotificationData(userInfo)
}
completionHandler(.newData)
}
func handleNotificationData(_ data: [AnyHashable: Any]) {
// Parse push payload to extract UTM parameters
if let source = data["utm_source"] as? String,
let medium = data["utm_medium"] as? String,
let campaign = data["utm_campaign"] as? String {
// Those are the mandatory UTM parameters, but don't forget to parse the optional ones as well.
let campaignParameters: [String:String] = [
"source" : source,
"medium" : medium,
"campaign" : campaign,
]
// In this sample I am using UUID but you can use any ID you prefer for user id.
sendCampaignDetailsWithRetry(userID: UUID().uuidString,
campaignParameters: campaignParameters)
}
}
func sendCampaignDetailsWithRetry(maxAttempts: Int = 5,
attempt: Int = 0,
userID: String,
campaignParameters: [String:String]) {
guard attempt < maxAttempts else {
// We have tried but no luck 🫤
return
}
Task {
if let sessionID = try? await Analytics.sessionID() {
// Found the sessionId 🙂
sendcampaignEvent(sessionID: sessionID, userID: userID, campaignParams: campaignParameters)
}else {
// Let's retry in 0.5 secs later
try? await Task.sleep(nanoseconds: 500_000_000)
self.sendCampaignDetailsWithRetry(maxAttempts: maxAttempts,
attempt: attempt + 1,
userID: userID,
campaignParameters: campaignParameters)
}
}
}
func sendcampaignEvent(sessionID: Int64, userID: String, campaignParams: [String:String]){
var parameters = campaignParams
parameters["session_id"] = String(sessionID)
Analytics.logEvent("campaign_details", parameters:parameters)
}
}
The code above assumes you have a simple push payload such as this:
{
"aps": {
"alert": {
"title": "Sample push message",
"subtitle": "Subtilte in here",
"body": "Here is my body",
"utm_source": "test_source",
"utm_medium": "test_medium",
"utm_campaign": "test_campaign"
}
}
}
We have a fully working solution now. 🎉
Testing Your Implementation
I have bad news for you. If you think the implementation of this capability is the hard part, unfortunately, you are wrong. In the GA4 dashboard, you can check the traffic acquisition by selecting “Reports”, then “Traffic acquisition” and filtering the “Session source/medium”. Please see the screenshot below to see the steps.
The problem is it takes 24–48 hours for a traffic event to become visible here. On top of that, you can’t use the Debug View provided by Firebase for the campaign_details
. Somehow it is not shown in the Debug View, I tried lots of times but had no luck. There is some kind of filtering for this event in the Debug View.
So, you have to wait for 24–48 hours to see if you did everything correctly or not!
But don’t worry if you follow this guide you will see your events. Eventually!
Conclusion
It was quite a time-consuming journey for me to discover the steps I explained in this article. GA4 and Firebase documentation are usually excellent sources for discovering analytic capabilities. However, for this specific case, I can honestly say that both of them are not clear. Additionally, the testing and verification process takes almost 24–48 hours. So, I had to wait for more than 24 hours to verify if I had done everything correctly. I had to send almost 20 different variations of the analytics event for campaign_details
to find the right payload. After 2 weeks of a blindfolded dive, I managed to find the right formula. We have been using it in the AutoScout24 iOS app for a while, and the product teams have been able to see the UTM traffic for more than 2 months. By following the steps outlined above, you can effectively track UTM parameters from push notifications in your iOS app and send them to Google Analytics 4. This allows you to gain deeper insights into the effectiveness of your push notification campaigns, helping you make data-driven decisions to optimize your marketing efforts.