Bert Johnson » Blog »

Embedding Tracx Data Using JavaScript

I recently launched a new intranet for a Fortune 500 client focused on internal communications and marketing awareness.

They use Tracx to mine and aggregate insights from social networks like Facebook and Twitter. Tracx enables multiple views for slicing and dicing tweets and analyzing sentiments of social posts. Imagine being able to query across all of your brands, segmented by country, age group, or gender.

Our goal was pretty simple in this case. We just wanted to display the ten most recent social posts (Facebook or Twitter) attached to a portfolio of nearly 100 brands.

Tracx provides a REST API, but no SDKs or guidance for integrating client-side. It seems like their API is targeted more towards .NET, Java, and open-source back-ends.

Here’s how I accomplished Tracx integration via JavaScript.

Getting Started: Your Key, Secret, and Query

In order to access Tracx’s OData endpoints, you’ll need an application key and secret. Those will be 32-digit alphanumeric keys.

You can log into the Tracx API portal at https://api.tra.cx/. From there, find the appropriate query to run. That took me a bit of trial and error before I found the following parameters to pass to the “/activity/posts” endpoint:

 map["filters"] = '{"campaignID":XXXX, "topicIDs":[YYYYY], "dataType": 2, "spotlightsMode":[3], "startDate":"' + prevDate + '", "endDate":"' + nextDate + '"}';
 map["limit"] = 10;
 map["offset"] = 0;
 map["orderBy"] = 0;
 map["key"] = ZZZZZZ;

Calculating HMACs and OAuth

The Tracx API using hash-based message authentication codes (HMAC) for security. To calculate HMAC, SHA1, and help with OAuth requests, we can rely on the following two scripts (created by Netflix and hosted on Google Code):

http://oauth.googlecode.com/svn/code/javascript/sha1.js http://oauth.googlecode.com/svn/code/javascript/oauth.js

When we’re assembling our parameters to pass in, we have to order and sign/hash them in a precise way.

Supporting IE8/9 and Providing a Shared Cache

In our case, we decided that making a web service call directly to Tracx was undesirable for two reasons:

  1. Tracx requires requests to be submitted using the “POST” verb, but older browsers including IE8 and IE9 only reliable support “GET” requests via XmlHttpRequest.
  2. Tracx service calls have a cost, for both performance and licensing. Instead of having many users make dynamic calls simultaneously, we’d be better off having service calls cached for everybody for a period of time.

In order to accomplish both, we implemented my .NET Proxy open source solution, hosted in Azure. .NET Proxy supports shared caching, configurable via the web.config. It also supports verb transformation; by passing in a “httpmethod” parameter on the “GET” query string with the value “POST”, it proxies its outbound calls as “POST”. That allows “GET”-only browsers to simulate “POST”s.

The source code below assumes .NET Proxy is being used as an intermediary.

Tracx in JavaScript: Source Code

Here’s the complete source code for embedding Tracx tweets and social posts on your website.

Make sure to include the sha1.js and oauth.js dependencies referenced above.

The “LoadSocial” function loads the data asynchronously. Once loaded, the second method outputs the posts to a DIV with ID “TracxContent”.

function LoadSocial(){
 if ($('#TracxContent').length > 0) {
 var tracxApiKey = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
 var tracxApiSecret = "YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY";

 var baseUrl = "https://api.tra.cx/1/activity/posts";

 var lastWeek = new Date();
 lastWeek = new Date(lastWeek.setDate(lastWeek.getDate() - 7));
 var year = lastWeek.getYear();
 if (year < 1000)
 year += 1900;
 var prevDate = year + '-' + Pad(lastWeek.getMonth() + 1, 2) + '-' + Pad(lastWeek.getDate(), 2)
 var tomorrow = new Date();
 tomorrow = new Date(tomorrow.setDate(tomorrow.getDate() + 1));
 var year = tomorrow.getYear();
 if (year < 1000)
 year += 1900;
 var nextDate = year + '-' + Pad(tomorrow.getMonth() + 1, 2) + '-' + Pad(tomorrow.getDate(), 2)

 var map = {};
 map["filters"] = '{"campaignID":XXXX, "topicIDs":[YYYY], "dataType": 2, "spotlightsMode":[3], "startDate":"' + prevDate + '", "endDate":"' + nextDate + '"}';
 map["limit"] = 10;
 map["offset"] = 0;
 map["orderBy"] = 0;
 map["key"] = tracxApiKey;

 var message = {};
 message["method"] = "POST";
 message["action"] = baseUrl;
 message["parameters"] = map;

 var baseURL = OAuth.SignatureMethod.getBaseString(message);
 var parameters = "filters=" + encodeURIComponent(map["filters"]) + "&key=" + map["key"] + "&limit=" + map["limit"] + "&offset=" + map["offset"] + "&orderBy=" + map["orderBy"];
 var proxyUrl = "https://netproxy.mydomain.com/netproxy.aspx?" + encodeURIComponent(baseUrl);
 var signature = b64_hmac_sha1(tracxApiSecret + "&", baseURL);

 // Try up to 3 times.
 var attempts = 0;
 // Send a GET, but tell the proxy to forward as a POST.
 while (attempts < 3) {
 attempts++;
 $.ajax({
 url: proxyUrl + "&httpmethod=POST&httppostdata=" + encodeURIComponent(parameters + "&signature=" + encodeURIComponent(signature) + "="),
 success: function (e) {
 if (WriteTracx(e))
 attempts = 3;
 else {
 if (attempts == 3)
 $('#TracxContent').html(Terms["unable to load social data"]);
 }
 },
 error: function (e) {
 if (attempts == 3)
 $('#TracxContent').html(Terms["unable to load social data"]);
 },
 timeout: 60000
 });
 }
 }
}

function WriteTracx(res) {
 if (res.indexOf("while(1);") > -1) {
 var result = jQuery.parseJSON(res.replace("while(1);", "")).data;

 if (result.length > 0) {
 var socialFeed = "<ol>";

 var dynamHtml = '<ol>';

 var monthNames = new Array("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec");

 for (var i = 0; i < result.length; i++) {
 var author = result[i].author.name;
 var authorID = result[i].author.id;
 var title = result[i].title;
 var postDate = result[i].publishedDate.replace(' |', '');
 var postDateParts = postDate.split(' ');
 var postDateParts2 = postDateParts[0].split('-');
 var postDate = postDateParts2[1] + '-' + postDateParts2[0] + '-' + postDateParts2[2] + ' ' + postDateParts[1] + ' ' + postDateParts[2];
 var url = result[i].externalUrl;
 var externalID = result[i].externalID;

 var testDate = new Date(Date.parse(postDate));
 var postDateObject;
 try {
 postDateObject = parseDate(testDate, "mm-dd-yyyy hh:MM TT");
 }
 catch (e) {
 postDateObject = 'NaN';
 }

 if (postDateObject == 'NaN') {
 try {
 postDateObject = parseDate(postDate, "mm-dd-yyyy hh:MM TT");
 } catch (e) {
 postDateObject = 'NaN';
 }
 }

 var curDate = postDateObject.getDate();
 var curMonth = postDateObject.getMonth();
 var curYear = postDateObject.getFullYear();
 var postDateFormatted = monthNames[curMonth] + " " + curDate;

 var network = result[i].network.toLowerCase();
 switch (network) {
 case "twitter":
 var titleParts = title.split(' ');
 for (var j = 0; j < titleParts.length; j++) {
 if (titleParts[j].toLowerCase().substr(0, 4) == "http")
 titleParts[j] = "<a href=\"" + titleParts[j] + "\" target='_blank'>" + titleParts[j] + "</a>";
 else if (titleParts[j].toLowerCase().substr(0, 1) == "#")
 titleParts[j] = "<a href=\"https://twitter.com/hashtag/" + titleParts[j].substr(1) + "\" target='_blank'>" + titleParts[j] + "</a>"
 }
 title = titleParts.join(' ');

 var socialPost = "\t\t\t<li class='h-entry tweet with-expansion customisable-border' data-tweet-id='" + externalID + "'>\r\n" +
 "\t\t\t\t<div class='header'>\r\n" +
 "\t\t\t\t\t\t<div class='h-card p-author'>\r\n" +
 "\t\t\t\t\t\t\t<a class='u-url profile' href='https://twitter.com/" + author + "' target='_blank'>\r\n" +
 "\t\t\t\t\t\t\t\t<img class='twitter-pic' src='/images/" + network + "-" + author + ".png'>\r\n" +
 "\t\t\t\t\t\t\t\t<span class='full-name'>\r\n" +
 "\t\t\t\t\t\t\t\t\t<span class='p-name customisable-highlight'>" + author + "</span>\r\n" +
 "\t\t\t\t\t\t\t\t</span>\r\n" +
 "\t\t\t\t\t\t\t</a>\r\n" +
 "\t\t\t\t\t\t\t<span class='p-nickname' dir='ltr'>@" + author + "</span>\r\n" +
 "\t\t\t\t\t\t\t<span class='u-floatLeft'>&nbsp;·&nbsp;</span>\r\n" +
 "\t\t\t\t\t\t\t<a class='twitter-timestamp' href='" + url + "' target='_blank'><time pubdate='" + postDate + "' class='dt-updated' datetime='" + postDateFormatted + "' title='Time posted: " + postDateFormatted + "'>" + postDateFormatted + "</time></a>\r\n" +
 "\t\t\t\t\t\t</div>\r\n" +
 "\t\t\t\t\t</div>\r\n" +
 "\t\t\t\t\t<div class='twitter-content'>\r\n" +
 "\t\t\t\t\t\t<p class='twitter-c'>" + title + "</p>\r\n" +
 "\t\t\t\t\t\t<div class='detail-expander'></div>\r\n" +
 "\t\t\t\t\t</div>\r\n" +
 "\t\t\t\t\t<div class='footer customisable-border'>\r\n" +
 "\t\t\t\t\t<img src='/images/twitter.png'>\r\n" +
 "\t\t\t\t\t<ul class='tweet-actions' role='menu' aria-label='Tweet actions'>\r\n" +
 "\t\t\t\t\t\t<li><a href='#' onclick=\"TwitterReply('" + externalID + "','" + authorID + "')\" class='reply-action web-intent' title='Reply'><i class='ic-reply ic-mask'></i><b>" + Terms['Reply'] + "</b></a></li>\r\n" +
 "\t\t\t\t\t\t<li><a href='#' onclick=\"TwitterRetweet('" + externalID + "')\" class='retweet-action web-intent' title='Retweet'><i class='ic-retweet ic-mask'></i><b>" + Terms['Retweet'] + "</b></a></li>\r\n" +
 "\t\t\t\t\t\t<li><a href='#' onclick=\"TwitterFavorite('" + externalID + "')\" class='favorite-action web-intent' title='Favorite'><i class='ic-fav ic-mask'></i><b>" + Terms['Favorite'] + "</b></a></li>\r\n" +
 "\t\t\t\t\t</ul>\r\n" +
 "\t\t\t\t</div>\r\n" +
 "\t\t\t</li>";

 socialFeed += socialPost;
 break;
 case "facebookpage":
 var titleParts = title.split(' ');
 for (var j = 0; j < titleParts.length; j++) {
 if (titleParts[j].toLowerCase().substr(0, 4) == "http")
 titleParts[j] = "<a href=\"" + titleParts[j] + "\" target='_blank'>" + titleParts[j] + "</a>";
 else if (titleParts[j].toLowerCase().substr(0, 1) == "#")
 titleParts[j] = "<a href=\"https://twitter.com/hashtag/" + titleParts[j].substr(1) + "\" target='_blank'>" + titleParts[j] + "</a>"
 }
 title = titleParts.join(' ');

 var socialPost = "\t\t\t<li class='h-entry tweet with-expansion customisable-border' data-tweet-id='" + externalID + "'>\r\n" +
 "\t\t\t\t<div class='header'>\r\n" +
 "\t\t\t\t\t\t<div class='h-card p-author'>\r\n" +
 "\t\t\t\t\t\t\t<a class='u-url profile' href='https://twitter.com/" + author + "' target='_blank'>\r\n" +
 "\t\t\t\t\t\t\t\t<img class='twitter-pic' src='/images/facebook-" + author + ".png'>\r\n" +
 "\t\t\t\t\t\t\t\t<span class='full-name'>\r\n" +
 "\t\t\t\t\t\t\t\t\t<span class='p-name customisable-highlight'>" + author + "</span>\r\n" +
 "\t\t\t\t\t\t\t\t</span>\r\n" +
 "\t\t\t\t\t\t\t</a>\r\n" +
 "\t\t\t\t\t\t\t<span class='p-nickname' dir='ltr'>@" + author + "</span>\r\n" +
 "\t\t\t\t\t\t\t<span class='u-floatLeft'>&nbsp;·&nbsp;</span>\r\n" +
 "\t\t\t\t\t\t\t<a class='twitter-timestamp' href='" + url + "' target='_blank'><time pubdate='" + postDate + "' class='dt-updated' datetime='" + postDateFormatted + "' title='Time posted: " + postDateFormatted + "'>" + postDateFormatted + "</time></a>\r\n" +
 "\t\t\t\t\t\t</div>\r\n" +
 "\t\t\t\t\t</div>\r\n" +
 "\t\t\t\t\t<div class='twitter-content'>\r\n" +
 "\t\t\t\t\t\t<p class='twitter-c'>" + title + "</p>\r\n" +
 "\t\t\t\t\t\t<div class='detail-expander'></div>\r\n" +
 "\t\t\t\t\t</div>\r\n" +
 "\t\t\t\t\t<div class='footer customisable-border'>\r\n" +
 "\t\t\t\t\t<img src='/images/facebook.png'>\r\n" +
 "\t\t\t\t\t<ul class='tweet-actions' role='menu' aria-label='Tweet actions'>\r\n" +
 "\t\t\t\t\t\t<li><a href='#' onclick=\"FacebookShare('" + rfc3986EncodeURIComponent(url) + "', '', '" + rfc3986EncodeURIComponent(url) + "', '" + rfc3986EncodeURIComponent(title) + "')\" class='reply-action web-intent' title='Reply'><i class='ic-reply ic-mask'></i><b>" + Terms["Share"] + "</b></a></li>\r\n" +
 "\t\t\t\t\t</ul>\r\n" +
 "\t\t\t\t</div>\r\n" +
 "\t\t\t</li>";

 socialFeed += socialPost;
 break;
 }
 }
 }
 else
 return false;

 $('#TracxContent').html(socialFeed);
 }
 else {
 return false;
 }

 return true;
}}