Skip to main content
Full Circle Insights

Tracking UTM Parameters with Field Synchronization

Summary

A common dimension many marketers want to bring into their Campaign Attribution reporting are the UTM Parameters they are utilizing in their digital advertising Campaigns. For example, a marketer may want to understand whether ads from LinkedIn (UTM Source = LinkedIn) outperform ads from Twitter (UTM Source = Twitter). Following is an example of how to use Campaign Attribution’s Field Synchronization feature to accomplish this.

Another option to consider, that doesn't require the workaround detailed below, is our Digital Source Tracker feature, available with Full Circle's Response Management product. Digital Source Tracker provides UTM tracking of digital touches, as well as organic and other types of referral traffic that come to your site, even without UTM parameters. For more information, contact your Full Circle Customer Success Representative from our Support Center.

Campaign Structure

First, we need to think about our Campaign Structure in Salesforce. How do we set up Salesforce to successfully track the UTM Medium, UTM Source, UTM Campaign, UTM Term, and UTM Content of each conversion from a digital ad?

An example use case may be a single landing page with multiple digital ads and channels driving visitors to fill out a form:

landing page

In this case we have a “Schedule a Demo” campaign with a Google AdWords, Twitter, LinkedIn, and Content Syndication ads all pointing to this landing page in addition to our email and organic search traffic. How might a marketer structure their Campaigns in Salesforce to capture the UTM parameters used in all these ads?

Some marketers will create a Campaign for each channel/asset combination so they may have a “Google Adwords - Schedule a Demo” campaign and a “Twitter - Schedule a Demo” campaign. This can be cumbersome to maintain and may be difficult to manage depending on your Marketing Automation platform (e.g., it may require multiple landing pages, one for each channel). It also may not be possible to capture more granular parameters such as UTM Term and UTM Content.

A more scalable option would be to create a single Campaign for each asset and capture the UTM parameters in custom fields on the Campaign Member:

utm fields on campaign member

Field Synchronization

Once the UTM data exists on the Campaign Member, the Field Synchronization feature can be utilized to push this data from the Campaign Member object to the Campaign Attribution Detail object for use in Campaign Attribution reporting. More details on the Campaign Attribution data model here.

First, create corresponding UTM fields on the Campaign Attribution Detail object. The field type should match the field type of the corresponding Campaign Member field.

Second, navigate to the Field Synchronization section of the configuration by clicking the “Field Synchronization” on the main Full Circle Campaign Attribution page:

field synchronization button

Third, map the UTM fields on the Campaign Member to the UTM fields on the Campaign Attribution Detail object:

field synchronization field mapping

Once this is set up, the data from the Campaign Member will sync to the related Campaign Attribution Detail records during the next database rebuild. More details on the rebuild process here.

Finally, you can use the UTM data in your Campaign Attribution reporting!

campaign attribution by utm medium

Advanced Topic: Syncing Lead/Contact UTM Data to the Campaign Member

Many Marketing Automation platforms cannot write to custom fields on the Campaign Member object. In these cases, marketers are most often writing to fields directly on the Lead/Contact, overwriting the UTM data each time a Lead/Contact fills out another form.

In these cases, we need the UTM data on the Lead/Contact to sync down to the Campaign Member when it’s created so that each Campaign Member has the appropriate UTM data tied to it. Once synced to the Campaign Member, the Lead/Contact fields should be cleared so that the next time the Lead/Contact responds to a Campaign the process can be repeated. Clearing also ensures that if a Lead/Contact responds to a Campaign where UTM data might not be used (e.g., an offline event Campaign) that the previous Campaign’s parameters aren’t accidentally assigned to the new Campaign Member.

Code Example

NOTE: Response Management customers will not need to create custom code to implement the above approach as this can be configured natively in the Response Management "Sync Fields" section.

The following code implements the above approach using an Apex trigger on the CampaignMember object.  The trigger executes before an update or insert of a CampaignMember record, and performs the following logic:

  1. Determine which campaign member records will get processed.  Records that get processed have a Status value that matches one of the values in StatusesToProcess list.
  2. Retrieves the lead and contact records related to the campaign member records that will be processed.
  3. For each campaign member record to process, the UTM field values are set, using the values from the related lead or contact. 
  4. After the UTM field value is copied from the lead/contact to the campaign member, the UTM fields on the lead/contact are set to null.
  5. Updates the lead and contact records, which will persist the null values to the records.

Note: Because this trigger happens before insert/update, there is no need to make a DML call to update the campaign member records.  The trigger will pick up the new values that got populated by this code.

Trigger Code
trigger MyCampaignMemberTrigger on CampaignMember (before insert, before update)
{
    MyHelperClass.HandleUtmData(trigger.new);
}
Helper Class Code
public with sharing class MyHelperClass
{
    // list of cm status values. if a cm has one of these statuses the UTM data will get copied to cm and cleared on the related lead/contact
    private static List<String> StatusesToProcess = new List<String> { 'Responded' };
    
    // variables below get populated in the GetRecordsToProcess method
    private static List<CampaignMember> RecordsToProcess = new List<CampaignMember>();
    private static List<Id> ContactIds = new List<Id>();
    private static List<Id> LeadIds = new List<Id>();

    public static void HandleUtmData(List<CampaignMember> cms)
    {
        GetRecordsToProcess(cms);

        // if there are no records that meet criteria, then exit
        if(RecordsToProcess.size() == 0) return;

        // get the leads associated to the cms being processed
        Map<Id, Lead> leadMap = new Map<Id, Lead>([SELECT Id, UTM_Campaign__c, UTM_Content__c, UTM_Medium__c, UTM_Source__c, UTM_Term__c FROM Lead WHERE ID IN :LeadIds]);

        // get the contacts associated to the cms being processed
        Map<Id, Contact> contactMap = new Map<Id, Contact>([SELECT Id, UTM_Campaign__c, UTM_Content__c, UTM_Medium__c, UTM_Source__c, UTM_Term__c FROM Contact WHERE ID IN :ContactIds]);

        // update the values on the cms, leads, contacts
        for(CampaignMember cm : RecordsToProcess)
        {
            if(cm.ContactId != null)
            {
                if(!contactMap.containsKey(cm.ContactId)) continue;
                Contact contact = contactMap.get(cm.ContactId);

                // copy field values to the cm from the contact
                cm.UTM_Campaign__c = contact.UTM_Campaign__c;
                cm.UTM_Content__c = contact.UTM_Content__c;
                cm.UTM_Medium__c = contact.UTM_Medium__c;
                cm.UTM_Source__c = contact.UTM_Source__c;
                cm.UTM_Term__c = contact.UTM_Term__c;

                // clear the contact fields
                contact.UTM_Campaign__c = null;
                contact.UTM_Content__c = null;
                contact.UTM_Medium__c = null;
                contact.UTM_Source__c = null;
                contact.UTM_Term__c = null;
            }
            else // must be a lead
            {
                if(!leadMap.containsKey(cm.LeadId)) continue;
                Lead lead = leadMap.get(cm.LeadId);

                // copy field values to the cm from the lead
                cm.UTM_Campaign__c = lead.UTM_Campaign__c;
                cm.UTM_Content__c = lead.UTM_Content__c;
                cm.UTM_Medium__c = lead.UTM_Medium__c;
                cm.UTM_Source__c = lead.UTM_Source__c;
                cm.UTM_Term__c = lead.UTM_Term__c;

                // clear the lead fields
                lead.UTM_Campaign__c = null;
                lead.UTM_Content__c = null;
                lead.UTM_Medium__c = null;
                lead.UTM_Source__c = null;
                lead.UTM_Term__c = null;
            }
        }

        // save the updates for leads and contacts
        update contactMap.values();
        update leadMap.values();
    }

    private static void GetRecordsToProcess(List<CampaignMember> cms)
    {
        for(CampaignMember cm : cms)
        {
            for(String status : StatusesToProcess)
            {
                if(cm.Status != status) continue;

                RecordsToProcess.add(cm);
                
                if(cm.ContactId != null)
                {
                    ContactIds.add(cm.ContactId);
                }
                else
                {
                    LeadIds.add(cm.LeadId);
                }
                break;
            }
        }
    }
}
Test Code
@isTest
private class MyTest {
    
    @isTest static void Test1()
    {
        List<Lead> leads = initTestLeads('test', 100);
        insert leads;
        
        List<Id> leadIds = new List<Id>();
        for(Lead l : leads) leadIds.add(l.Id);

        List<Contact> contacts = initTestContacts('test', 100, true);   
        insert contacts;

        List<Id> contactIds = new List<Id>();
        for(Contact c : contacts) contactIds.add(c.Id);

        List<Campaign> camps = initTestCampaigns('test', 1);
        insert camps;

        List<CampaignMember> cms = new List<CampaignMember>();
        for(Integer i=0; i<leads.size();i++)
        {
            if(i < leads.size()/2)
            {
                cms.add(new CampaignMember(LeadId=leads[i].Id, CampaignId=camps[0].Id, Status='Sent'));
            }
            else
            {
                cms.add(new CampaignMember(LeadId=leads[i].Id, CampaignId=camps[0].Id, Status='Responded'));
            }
        }

        for(Integer i=0; i<contacts.size();i++)
        {
            if(i < contacts.size()/2)
            {
                cms.add(new CampaignMember(ContactId=contacts[i].Id, CampaignId=camps[0].Id, Status='Sent'));
            }
            else
            {
                cms.add(new CampaignMember(ContactId=contacts[i].Id, CampaignId=camps[0].Id, Status='Responded'));
            }
        }

        Test.startTest();
        insert cms;
        Test.stopTest();

        List<Id> cmsIds = new List<Id>();
        for(CampaignMember cm : cms) cmsIds.add(cm.Id);

        // leads that have cm with status of responded should all have a value in the UTM fields
        List<Lead> leadsResponded = [SELECT Id, UTM_Campaign__c, UTM_Content__c, UTM_Medium__c, UTM_Source__c, UTM_Term__c FROM Lead WHERE ID IN :leadIds AND (UTM_Campaign__c != null OR UTM_Content__c != null OR UTM_Medium__c != null OR UTM_Source__c != null OR UTM_Term__c != null)];

        System.assertEquals(leads.size()/2, leadsResponded.size());

        // leads that have cm with status of sent should all have null in the UTM fields
        List<Lead> leadsSent = [SELECT Id, UTM_Campaign__c, UTM_Content__c, UTM_Medium__c, UTM_Source__c, UTM_Term__c FROM Lead WHERE ID IN :leadIds AND (UTM_Campaign__c = null OR UTM_Content__c = null OR UTM_Medium__c = null OR UTM_Source__c = null OR UTM_Term__c = null)];

        System.assertEquals(leads.size()/2, leadsSent.size());

        // contacts that have cm with status of responded should all have a value in the UTM fields
        List<Contact> contactsResponded = [SELECT Id, UTM_Campaign__c, UTM_Content__c, UTM_Medium__c, UTM_Source__c, UTM_Term__c FROM Contact WHERE ID IN :contactIds AND (UTM_Campaign__c != null OR UTM_Content__c != null OR UTM_Medium__c != null OR UTM_Source__c != null OR UTM_Term__c != null)];
        
        System.assertEquals(contacts.size()/2, contactsResponded.size());

        // contacts that have cm with status of sent should all have null in the UTM fields
        List<Contact> contactsSent = [SELECT Id, UTM_Campaign__c, UTM_Content__c, UTM_Medium__c, UTM_Source__c, UTM_Term__c FROM Contact WHERE ID IN :contactIds AND (UTM_Campaign__c = null OR UTM_Content__c = null OR UTM_Medium__c = null OR UTM_Source__c = null OR UTM_Term__c = null)];
        
        System.assertEquals(contacts.size()/2, contactsSent.size());

        // campaign members should all have non-null value in UTM fields
        List<CampaignMember> newCms = [SELECT Id, UTM_Campaign__c, UTM_Content__c, UTM_Medium__c, UTM_Source__c, UTM_Term__c FROM CampaignMember WHERE ID IN :cmsIds AND (UTM_Campaign__c != null AND UTM_Content__c != null AND UTM_Medium__c != null AND UTM_Source__c != null AND UTM_Term__c != null)];

        System.assertEquals(contactsResponded.size() + leadsResponded.size(), newCms.size());
    }
    
    private static List<Lead> initTestLeads(String prefix, Integer count)  
     {    
        List<Lead>lds = new List<Lead>();    
        for(Integer x=1;x<count+1;x++)    
        {      
            lds.add(new Lead(
                Company= prefix + '_' + String.valueOf(x),
                LastName = prefix + '_' + String.valueOf(x),
                UTM_Campaign__c = 'campaign_' + String.valueOf(1),
                UTM_Content__c = 'content_' + String.valueOf(1),
                UTM_Medium__c = 'med_' + String.valueOf(1),
                UTM_Source__c = 'source_' + String.valueOf(1),
                UTM_Term__c = 'term_' + String.valueOf(1)
            ));    
        }
        return lds;  
    }

    private static List<Account> initTestAccounts(String prefix, Integer count)
    {
        List<Account> accounts = new List<Account>();
        for(Integer x=1; x<count + 1; x++)
        {
            accounts.add(new Account(Name= prefix + '_' + String.valueOf(x)));    
        }
        return accounts;
    }

    private static List<Contact> initTestContacts(String prefix, Integer count, Boolean withaccounts)  
    {
        List<Contact>cts = new List<Contact>();    
        for(Integer x=1;x<count+1;x++)    
        {   
            Contact ct = new Contact(
                LastName = prefix + '_' + String.valueOf(x),
                UTM_Campaign__c = 'campaign_' + String.valueOf(1),
                UTM_Content__c = 'content_' + String.valueOf(1),
                UTM_Medium__c = 'med_' + String.valueOf(1),
                UTM_Source__c = 'source_' + String.valueOf(1),
                UTM_Term__c = 'term_' + String.valueOf(1)
            );
            cts.add(ct);
        }
        if(withaccounts)
        {
            List<Account> accounts = null;
            accounts = initTestAccounts(prefix, count);
            insert accounts;
            for(Integer x=0;x<count;x++)    
            {
                cts[x].AccountID = accounts[x].id;
            }      
        }    
        return cts;  
    }

    private static List<Campaign> initTestCampaigns(String prefix, Integer count)
    {
        List<Campaign> camps = new List<Campaign>();
        for(Integer x=1; x<count+1; x++)
        {
            camps.add(new Campaign(Name = prefix+'_'+ String.ValueOf(x), IsActive = true));
        }
        return camps;
    }
}
  • Was this article helpful?