Tracking Facebook, Twitter and Pinterest interactions

Recently I was given a task by my current employer (an online retailer) to find a way to track social interactions of registered members on the company's website. The goal was to reward members with discounts and other types of bonuses for liking products, tweeting about them or pinning them to their pinboards on Pinterest.com.

I knew that tracking interactions with the first two services would not pose any problems as both, Facebook and Twitter, have callback functionality available for their buttons. Facebook has FB.Event.subscribe for their Like button and Twitter has a Web Intent called "tweet" notifying you about successful tweets.

However, things turned out to be more difficult with Pinterest as it doesn't have a similar functionality (at least at the time of writing this article, December 2012).

A quick investigation showed that no one else had managed to find a decent solution to this problem either. Everything I found was either a dirty hack or a very dirty hack. Some "solutions" even suggested recreating the Pin It button manually only to be able to define the custom logic, which in my view is plain crazy!

So I realized that I had to come up with my own solution if I was to track pins. And after a bit of thinking and investigating I did! Knowing that there are developers out there trying to achieve the same goal, I decided to share it here.

Database

As usual, we will need a place to store all the interactions and for this specific example, I'll be using a MySQL table.

Even though Facebook, Twitter and Pinterest are all different services, we will essentially be storing more or less the same information about interactions with all of them. In our case it will be id of the user that performed the action, the service involved and the target itself - the product they liked, tweeted about or pinned.

Having that in mind, it only makes sense to have one table storing all 3 types of interactions. Here is how I defined it:

  1. CREATE TABLE `social_tracking` (
  2.   `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  3.   `user_id` int(10) unsigned NOT NULL,
  4.   `source` enum('facebook','pinterest','twitter') NOT NULL,
  5.   `product_id` int(10) unsigned NOT NULL,
  6.   `added` datetime NOT NULL,
  7.   PRIMARY KEY (`id`),
  8.   UNIQUE KEY `usr_src_prod` (`user_id`,`source`,`product_id`)
  9. ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

Integrating the tracking

Facebook

As previously mentioned, Facebook integration is pretty straightforward if you use their Javascript SDK.

Knowing that we can subscribe to the edge.create event, we only needed to add a simple AJAX post for Facebook's callback like this (jQuery):

  1. FB.Event.subscribe('edge.create',
  2.     function(response) {
  3.         $.ajax({
  4.             url: '/tracking.php',
  5.             type: 'post',
  6.             data: {
  7.                 service: 'facebook',
  8.                 user_id: <?php print $userId; ?>,
  9.                 product_id: <?php print $productId; ?>
  10.             }
  11.         });
  12.     }
  13. );

Twitter

It isn't much more different with Twitter either, as, based on the web intents documentation, all we need to do to track a successful tweet is listen to the 'tweet' event:

  1. twttr.events.bind('tweet', function(event) {
  2.     $.ajax({
  3.         url: '/tracking.php',
  4.         type: 'post',
  5.         data: {
  6.             service: 'twitter',
  7.             user_id: <?php print $userId; ?>,
  8.             product_id: <?php print $productId; ?>
  9.         }
  10.     });
  11. });

Pinterest

If all is nice and simple with Facebook and Twitter, then it's definitely not the same story with Pinterest. Their library does not have any means of letting us know about successful pins, which leaves us with inventing a tricycle. Fear not, though! There is a solution to this issue but before we get to it, let's have a look at my discoveries after investigating how Pinterest actually works.

Let's assume there is a product page located on http://example.com/products/abc containing the product image that you want to pin - http://example.com/products/abc/picture.png.

If you were to generate code for the Pin It button on Pinterest's goodies page it would look like this (formatted for easier reading):

  1. <a href="http://pinterest.com/pin/create/button/
  2.    ?url=http%3A%2F%2Fexample.com%2Fproducts%2Fabc
  3.    &media=http%3A%2F%2Fexample.com%2Fproducts%2Fabc%2Fpicture.png
  4.    &description=My%20product"
  5.    class="pin-it-button"
  6.    count-layout="horizontal"
  7. >
  8.     <img border="0" src="//assets.pinterest.com/images/PinExt.png" title="Pin It" />
  9. </a>
  10. <script type="text/javascript" src="//assets.pinterest.com/js/pinit.js"></script>

Pinterest's explanation of parameters that I was interested in is as follows:

  • url: URL of the page to pin
  • media: URL of the image to pin
  • description: description of the page to pin

Note that value of the media parameter is location of the image that Pinterest is supposed to pin. Knowing that, I decided to do an experiment and replace URL of the image with the location of a custom PHP script that would send me contents of $_GET, $_POST and $_SERVER variables in an email and then redirect to the required image (in our case http://example.com/products/abc/picture.png).

Assuming that the script is located on http://example.com/track-pinterest.php, I updated my Pin It button code to this:

  1. <a href="http://pinterest.com/pin/create/button/
  2.    ?url=http%3A%2F%2Fexample.com%2Fproducts%2Fabc
  3.    &media=http%3A%2F%2Fexample.com%2Ftrack-pinterest.php%3Fi%3Dhttp%3A%2F%2Fexample.com%2Fproducts%2Fabc%2Fpicture.png
  4.    &description=My%20product"
  5.    class="pin-it-button"
  6.    count-layout="horizontal"
  7. >
  8.     <img border="0" src="//assets.pinterest.com/images/PinExt.png" title="Pin It" />
  9. </a>

Note that the updated media parameter now contained the following URL as its value:
http://example.com/track-pinterest.php?i=http://example.com/products/abc/picture.png.

That meant that instead of requesting the image, Pinterest would be requesting my PHP script, which will do whatever I specified. Contents of the track-pinterest.php script itself were rather simple:

  1. $to = "Your Name <youremail@yourdomain.com>";
  2. $subject = sprintf("Pinterest callback: %s", date("Y-m-d H:I:s"));
  3.  
  4. // extract variables
  5. $server = print_r($_SERVER, true);
  6. $get = print_r($_GET, true);
  7. $post = print_r($_POST, true);
  8.  
  9. // prepare and the message
  10. $message = sprintf("SERVER: %s\nGET: %s\nPOST: %s", $server, $get, $post);
  11. mail($to, $subject, $message);
  12.  
  13. // redirect
  14. $image = isset($_GET["i"]) ? $_GET["i"] : '/';
  15. header('Location: ' . $image);

After updating the button and attempting to pin a product I received an email right after I clicked on the Pin It button. It was to be expected as the popup displays the image preview but it was also disappointing at the same time as it meant that my script was getting triggered without me actually pinning anything.

Here is a trimmed down version of the email:

SERVER: Array
(
    [HTTP_REFERER] => http://pinterest.com/pin/create/button/?url=http%3A%2F%2Fexample.com%2Fproducts%2Fabc&media=http%3A%2F%2Fexample.com%2Ftrack-pinterest.php%3Fi%3Dhttp%3A%2F%2Fexample.com%2Fproducts%2Fabc%2Fpicture.png
    [HTTP_USER_AGENT] => Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_5) AppleWebKit/537.4 (KHTML, like Gecko) Chrome/22.0.1229.94 Safari/537.4
    [QUERY_STRING] => i=http://example.com/products/abc/picture.png
    [REDIRECT_STATUS] => 200
    [REMOTE_ADDR] => 62.7.89.140
    [REMOTE_PORT] => 35538
    [REQUEST_METHOD] => GET
    [REQUEST_URI] => /track-pinterest.php?i=http://example.com/products/abc/picture.png
    [REQUEST_TIME] => 1351005387
    [argv] => Array
        (
            [0] => i=http://example.com/products/abc/picture.png
        )
    [argc] => 1
)

GET: Array
(
    [i] => http://example.com/products/abc/picture.png
)

POST: Array
(
)

As you can see, nothing specific to pay attention to at this point, just the usual request data.

Next, after actually pinning the product I received two more emails with almost idential contents:

SERVER: Array
(
    [HTTP_USER_AGENT] => Pinterest/0.1 +http://pinterest.com/
    [QUERY_STRING] => i=http://example.com/products/abc/picture.png
    [REDIRECT_STATUS] => 200
    [REMOTE_ADDR] => 184.73.113.70
    [REMOTE_PORT] => 50441
    [REQUEST_METHOD] => GET
    [REQUEST_URI] => /track-pinterest.php?i=http://example.com/products/abc/picture.png
    [REQUEST_TIME] => 1351005407
    [argv] => Array
        (
            [0] => i=http://example.com/products/abc/picture.png
        )
    [argc] => 1
)

GET: Array
(
    [i] => http://example.com/products/abc/picture.png
)

POST: Array
(
)

The second email of the two had exactly the same contents as the first one with the exception of a different remote IP address.

As you can see, these subsequent emails are much more interesting to us than the first one as the value of HTTP_USER_AGENT is now set to "Pinterest/0.1 +http://pinterest.com/" (at least at the point of writing this article), which means that we can now distinguish between the initial image request for displaying in the popup and the actual pin!

So, to summarize it all before we move on, here is what happens when a user clicks on the Pin It button on:

  1. User is presented with a popup containing the image that was specified in the media parameter and the description from the description parameter.
    • At this point we receive the first request to our script.
  2. When the user clicks on the Pin It button, Pinterest's servers request the image (or rather contents of the URL) that was specified in the media parameter for the second time.
    • Here we receive the second and third request to our script.
  3. Once Pinterest is done fetching the contents, it stores the image on their own CDNs just to ensure that, even if you delete it from your servers, their users would still be able to see it on pinterest.com.
  4. Finally, after the pin is processed, user is presented with a confirmation and eventually the popup closes.

The key thing to note in this flow is the fact that Pinterest requests contents of the media URL 3 times - once when the user initiates the popup and twice when the user actually pins the item.

I have no definitive explanation on why Pinterest requests the URL twice but I suspect it could either have something to do with them having multiple data centers or more likely due to the fact that they actually generate two image sizes on their side - 192x139 and 412x300. Obviously, we can only guess not knowing their internal architecture but it's not too big of an issue for us, as our table has a multi key on the user_id + source + product_id fields.

Also, it's worth mentioning that once an image is pinned, Pinterest will not make any further requests related to it unless the same or any other user pins it, which can and will happen. If it does, you will get 3 more requests just like the first time.

The tracking script

As you saw earlier, I was passing location of the target image as a GET parameter to my tracking script. That, as you can imagine, means that you can pass any information you want as long as you specify it as GET parameters of the URL passed as the media entry.

For example, if you wanted to track which user pinned which product, you could pass two parameters like this:

  • track-pinterest.php?user=1&product=2

Depending on whether or not you can determine the target image by the product id, you might also need to pass the absolute or relative path to the image:

  • track-pinterest.php?user=1&product=2&image=/product/abc/picture.png

If we put this all together we end up with the following button code:

  1. <a href="http://pinterest.com/pin/create/button/
  2.    ?url=http%3A%2F%2Fexample.com%2Fproducts%2Fabc
  3.    &media=http%3A%2F%2Fexample.com%2Ftrack-pinterest.php%3Fuser%3D1%26product%3D2%26image%3D%2Fproduct%2Fabc%2Fpicture.png
  4.    &description=My%20product"
  5.    class="pin-it-button"
  6.    count-layout="horizontal"
  7. >
  8.     <img border="0" src="//assets.pinterest.com/images/PinExt.png" title="Pin It" />
  9. </a>
  10. <script type="text/javascript" src="//assets.pinterest.com/js/pinit.js"></script>

With all this in mind, contents of the track-pinterest.php script could look something like this:

  1.  
  2. // validate the user agent ("Pinterest/0.1 +http://pinterest.com/" as of 2012-12-20)
  3. $userAgent = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '';
  4.  
  5. // if user pinned the item
  6. if (preg_match('/^Pinterest/d+.d+.*$/', $userAgent)) {
  7.     // save activity
  8.     $tracker->registerPin($_GET['user'], $_GET['product']);
  9. }
  10.  
  11. // redirect to the target image even if it's not pin as we still need to display it in the popup
  12. $target = sprintf('http://%s%s', $_SERVER['HTTP_HOST'], $_GET['image']);
  13. header('Location: ' . $_GET['image'], true, 301);

Obviously, this is just a sample code to demonstrate that you can pass absolutely anything to the script and process it whichever way you want. It all depends on what you need to log or process.

If you were to use this sample code, I would advise you to add a few more validations just to make sure that all required parameters are set, etc. Alternatively you could encrypt the data that you are passing to the script so that users can't tamper with the parameters.

Consider a sample class to achieve that:

  1. class Encryptor
  2. {
  3.  
  4.     /**
  5.      * Get encryption key
  6.      *
  7.      * @return string
  8.      */
  9.     public function getKey()
  10.     {
  11.         return 'some-random-string';
  12.     }
  13.  
  14.     /**
  15.      * Encrypt data
  16.      *
  17.      * @param  mixed $data Data to encrypt
  18.      * @return string
  19.      */
  20.     public function encrypt(array $data)
  21.     {
  22.         $json = json_encode($data);
  23.         $encode = base64_encode(mcrypt_encrypt(MCRYPT_RIJNDAEL_256, $this->getKey(), $json, MCRYPT_MODE_CBC, md5($this->getKey())));
  24.         $encode = str_replace(array('+', '/'), array('@', '$'), $encode);
  25.         return $encode;
  26.     }
  27.  
  28.     /**
  29.      * Decrypt data
  30.      *
  31.      * @param  mixed $data Data to decrypt
  32.      * @return object
  33.      */
  34.     public function decrypt($data)
  35.     {
  36.         $data = str_replace(array('@', '$'), array('+', '/'), $data);
  37.         $json = rtrim(mcrypt_decrypt(MCRYPT_RIJNDAEL_256, $this->getKey(), base64_decode($data), MCRYPT_MODE_CBC, md5($this->getKey())), "\0");
  38.         return json_decode($json, true);
  39.     }
  40.  
  41. }

Then, instead of manually defining user and product in the list of tracking script parameters, you could encrypt them as follows:

  1. $data = array(
  2.     'user'    => 1,
  3.     'product' => 2,
  4. );
  5.  
  6. // generate hash from the data
  7. $encryptor = new Encryptor();
  8. $hash = $encryptor->encrypt($data);
  9. $media = 'http://example.com/track-pinterest.php?data=' . $hash;

Then use $media as value of the media parameter instead of composing the URL manually.

To decode the data passed to the script do the opposite:

  1. $encryptor = new Encryptor();
  2. $data = $encryptor->decrypt($_GET['data']);
  3. // ... data validations go here ...
  4. $tracker->registerPin($data['user'], $data['product']);

In any case, the final implementation depends on the developer, as every business will want to track different things.

As always, please leave your responses below and let me know if anything could be improved with the code presented above.

Comments
1
Thanks! This will help me out with my project!
Stuart, January 3rd 2013, 18:33
2
Thanks a lot. We are finding the solution to track the people pin our website. Your article help us a lot. 

By the way, do you think we can track the traffic of uploading image in the server log?
Kin, January 23rd 2013, 4:14
3
Hi Kin!

You should be able to, as the request to the image should show up in the access log (if you are using Apache, e.g.).

I haven't actually tested it as we needed to update the database with values whenever a user pinned an image, but if you don't need to do that, you could simply parse the access logs and extract all requests to your images from Pinterest's user agent.
Andris, January 24th 2013, 13:15
4
Hi, 

This is Kin again. Have you continued the project? Any recent update ? 

Kin
Kin, April 19th 2013, 9:41
5
Hi Kin,

Apologies for such a late reply. Hope you'll still read it!

Speaking about the code - I haven't done anything with it since. Not sure there is anything to be done with it to be honest.

Is there anything specific you need help with? Feel free to contact me via the contact page (Contact - codeaid.net) and I'll do my best to help out.
Andris, May 6th 2013, 19:09
6
This is such a genius method. Thanks for doing the detective work for us.
Aaron Francis, January 28th 2014, 17:37
Name
Email (required)
will not be published
Website
Recaptcha
you will only be required to fill it in once in this session

You can use [code][/code] tags in your comments