This article showcases how to track AMP email opens in Salesforce Marketing Cloud using server-side JavaScript.
Yes, but why?
Because at this moment in time, Marketing Cloud doesn’t allow us to make a difference in email opens between AMP emails and regular HTML emails.
Fortunately for us, there is a relatively easy way to create a custom opens tracking solution for AMP emails.
Before we start
The solution requires 3 assets to be created:
- Data Extension to hold the tracking data.
- Cloud page to process the data coming from the AMP email.
- AMP email.
Data Extension
If you have some experience with retrieving and analyzing the email tracking information, the following fields will seem familiar:
Name | Type | Length | Nullable |
---|---|---|---|
Subscriberkey | Text | 50 | Yes |
EmailAddress | EmailAddress | 254 | Yes |
EmailId | Number | Yes | |
JobId | Number | Yes | |
ListId | Number | Yes | |
BatchId | Number | Yes | |
EventDate | Date | Yes |
Cloud page
Please create a new empty Cloud page with the Content Builder option enabled. Then view its properties and take a note of the PageId.
AMP email
In order for the solution to work, you need to have created a valid AMP email. Please feel free to have a look at my previous article about AMP if you don’t know where to start.
How it works
This custom solution is quite simple: when a customer opens an AMP email, it sends a request to Marketing Cloud to create a new record in a Data Extension with all the tracking information it can gather.
AMP email
In order for this to work, we need to add the following code to the AMP email. It uses the amp-list component that sends an AJAX request on every email refresh (so basically every time the email is opened).
<amp-list
src="%%=CloudPagesURL(1234)=%%"
binding="refresh"
width="1"
height="1"
layout="fixed"
>
</amp-list>
In this context, 1234 is the PageId of the Cloud page that will process the data from the email.
Cloud page
Now, let’s write some code for the Cloud page.
It has to use same security-related bits from my previous article in order to comply with the CORS policy of AMP for Email.
And we also need to account for Firefox’s and IE11’s shortcomings when it comes to retrieving the request origin header, as Firefox can only retrieve the Referer and IE retrieves the full URL instead of the base URL.
But apart from that, it’s pretty straightforward: retrieve the data from the request and create a record in the Data Extension using the WSProxy method.
Note that in the end the page returns an empty list object, as a response for the amp-list component, in order to avoid any JavaScript errors in the AMP email.
<script runat='server'>
Platform.Load('core', '1');
var api = new Script.Util.WSProxy();
// Please add your allowed origins here
var allowedOrigin = [
"https://playground.amp.dev",
"https://amp.gmail.dev",
"https://mail.google.com"
];
var sender = Platform.Request.GetRequestHeader("AMP-Email-Sender");
var origin = Platform.Request.GetRequestHeader("Origin");
var referer = Platform.Request.GetRequestHeader("Referer");
var source = Platform.Request.GetQueryStringParameter("__amp_source_origin");
if(origin == null || origin.length === 0) origin = referer;
// Origin doesn't work in FF, in IE11 Origin is the full URL (not only domain)
var regex = /^(?:\/\/|[^\/]+)*/g;
var match = origin.match(regex);
var origin = (match.length > 0) ? match[0] : null;
try {
if(!!inArray(allowedOrigin, origin)) {
HTTPHeader.SetValue("Access-Control-Allow-Methods", "POST");
HTTPHeader.SetValue("Access-Control-Allow-Origin", origin);
HTTPHeader.SetValue("Content-Type", "application/json");
if(source != null) {
HTTPHeader.SetValue("Access-Control-Expose-Headers", "AMP-Redirect-To, AMP-Access-Control-Allow-Source-Origin");
HTTPHeader.SetValue("AMP-Access-Control-Allow-Source-Origin", source);
}
HTTPHeader.SetValue("Access-Control-Allow-Credentials", "true");
} else {
throw "Origin not allowed";
}
Write('{"items":[]}');
var config = {
de: 'AMP Email Opens',
attrs: {
Subscriberkey: Attribute.GetValue('_subscriberkey'),
EmailAddress: Attribute.GetValue('emailaddr'),
EmailId: Attribute.GetValue('_emailid'),
JobId: Attribute.GetValue('jobid'),
ListId: Attribute.GetValue('listid'),
BatchId: Attribute.GetValue('_JobSubscriberBatchID'),
EventDate: Platform.Function.SystemDateToLocalDate(Platform.Function.Now())
}
}
var result = api.createItem('DataExtensionObject', {
Name: config.de,
Properties: wsPack(config.attrs)
});
} catch(err) {
Write(Stringify(err));
}
function wsPack(o) {
var arr = [];
for (var k in o) {
arr.push({
Name: k,
Value: o[k]
});
}
return arr;
}
function inArray(arr, k) {
var b = false;
for (var i in arr) {
if (arr[i] == k) b = true;
}
return b;
}
</script>
Conclusion
That’s it folks! This is not an elegant solution but it works!
Hopefully, Salesforce will come up with something out-of-the-box in the near future, but for now, this is our best solution.
Credits
This article could not have been possible without Eliot Harper’s MC Chat videos. Please “smash that like button” for the great work he’s doing.
Have I missed anything?
Please poke me with a sharp comment below or use the contact form.