Building a Referral Tracking add-on for concrete5 5.7

5.7+
Building a Referral Tracking add-on for concrete5 5.7

Track visitors to your website who arrive from affiliate websites, and their interactions and transactions


Article by Ollie / / Comments / Difficulty 
Building a Referral Tracking add-on for concrete5 5.7

I recently needed to write a referral tracking add-on for a third party website I've been helping build. It's the usual idea - the website wants to set up an affiliate programme so needs a way to track visitors to their website who arrive from affiliates' websites, and more importantly track visitors who go on to interact and perhaps even transact with the website, so that proper attribution can be given by way of a referral fee.

Since the affiliate piece was something of an afterthought, the challenge was to build this without having to rewrite all the contact and quote forms already in place on the website. It's a great basis for a tutorial, since there are only two PHP files in the entire package and there's less than 150 lines of code, but there is some reasonably advanced stuff going on including event listeners, cookie setting and checking and POST request interception and modification.

I considered adding this to the concrete5 marketplace, but its a surprising simple add-on with very little code, and it just meets my use case. So I'll add it to Bitbucket and I've written up the approach below.

Required

  • Identify visitors referred from affiliate partner websites
  • Ensure that interactions, such as enquiry form submissions can be attributed to any affiliate
  • Ensure that affiliate gets the credit for referral for a period of time, even if the visitor leaves the site and comes back at later date.
  • No, or minimal modification of existing contact and quote form code.

First, the package controller. This extends the Package class. Besides the standard stuff of providing name, description, version information for the package there's only an on_start method included.

The on_start method is used to set up two event listeners as shown below.

/controller.php

public function on_start() {
// Extend Events
Events::addListener(
    'on_before_render', function() {
      referralTracking::getReferrer();
    }
  );

  Events::addListener(
    'on_block_load', function() {
      referralTracking::checkReferrer();
    }
  );
}

Our instructions are simple. Before every render of a page we want to call the getReferrer() method of the referralTracking class (which in 5.7 are generally controllers and that's how I'll refer to this class from now on), and before every block load we want to call the checkReferrer() method from the same controller.

I want to backtrack a little and just highlight the namespace and use directives that we need at the top of the controller.php file.

namespace Concrete\Package\ReferralTracking;
use Package;
use Events;
use Concrete\Package\ReferralTracking\Controller\referralTracking;

The namespace is our package namespace. The following two use directives call namespace aliases to ensure our controller has access to the Package controller which we're inheriting from and the Events controller which we need to setup our listeners. The last use directive, provides the correct namespace for our referralTracking controller which we're going to look at next.

Lets cover the namespacing and aliasing of the referralTracking controller first.

We set a namespace for the our referralTracking controller and three use directives which refer to aliased namespaces. You should be able to see how the namespace plus the controller name, makes up the use directive Concrete\Package\ReferralTracking\Controller\referralTracking which we specified in the package controller.

/controllers/referralTracking.php

namespace Concrete\Package\ReferralTracking\Controller;
use Controller;
use Core;
use Cookie;

class referralTracking extends Controller {
  public static function getReferrer() {
    $referral = array();
    if(isset($_SERVER["HTTP_REFERER"])) {
    // This is a link in to the site, lets check it the querystring for a "partner_id"
      if(strpos(strtolower($_SERVER['QUERY_STRING']), "rid") != -1) {
      // Referral we need to track this
      // Is there already a referral cookie?
        if(!cookie::has('BIREFERRAL')) {
          parse_str($_SERVER['QUERY_STRING']); // gives us $partner_id variable
          $referral['id'] = $rid;
          $referral['uri'] = $_SERVER["HTTP_REFERER"];
          $referral['user-agent'] = $_SERVER["HTTP_USER_AGENT"];
          $referral['landed'] = $_SERVER["PHP_SELF"];
          $referral['timestamp'] = $_SERVER["REQUEST_TIME"];

          // Basic encryption, intended to obfuscate really
         $encrypt = self::encryptDecrypt("encrypt", serialize($referral));

         // Set it
        $cookieExpire = 30;
        setcookie("BIREFERRAL", $encrypt, time() + $cookieExpire * 86400, "/");
      }
    }
  }
}

Next our getReferrer() method, called before render of every page of our site. It checks for the existence of HTTP_REFERER (which is the site which the visitor linked from), looks to see if the querystring contains the "rid" parameter which affiliates add to the url they use to link to our website. If that exists it checks to see if a BIREFERRAL cookie has been set, and, if it has not, it sets it. The cookie contains information which identifies the referrer. This is encrypted using an encryptDecrypt routine to prevent tampering. An expiry of 30 days is set on the cookie - the visitor can leave our site, and come back up to 30 days later and our affiliate partner will be credited with any business done.

This is the encryptDecrypt method mentioned. It's a private method, only this controller should be able to use it.

private static function encryptDecrypt($action, $string) {
  $output = false;
  $encrypt_method = "AES-256-CBC";
  $secret_key = 'Does not really matter just want to obfuscate';
  $secret_iv = 'Does not really matter just want to obfuscate';
  $key = hash('sha256', $secret_key);
  $iv = substr(hash('sha256', $secret_iv), 0, 16);
  if ( $action == 'encrypt' ) {
    $output = openssl_encrypt($string, $encrypt_method, $key, 0, $iv);
    $output = base64_encode($output);
  } else if( $action == 'decrypt' ) {
    $output = openssl_decrypt(base64_decode($string), $encrypt_method, $key, 0, $iv);
  }
  return $output;
}

Next up, checkReferrer(). This is called on every block load. Its purpose is simple. If it detects that this is a POST request, suggesting that a form submission is in progress, it looks for our BIREFERRAL cookie. If it finds one, it decrypts the information it contains and adds each piece to the POST request in progress. $_POST is the variable name for this array in PHP.

public static function checkReferrer() {
  if($_POST) {
    if(cookie::has('BIREFERRAL')) {
      $referral = unserialize(self::encryptDecrypt("decrypt", cookie::get('BIREFERRAL')));
      foreach ($referral as $key => $val) {
        $_POST[$key] = $val;
      }
    }
  }
}

Which brings us to the last piece of the package functionality. An addReferralData() method.

public static function addReferralData() {
  $html = "";
  if($_POST['id']) {
    $html.= "\n\nThis form has been tracked as a referral.\n";
    $html.= "Referral data\n";
    $html.= "=============\n";
    $html.= "Referral ID: " . $_POST['id'] ."\n";
    $html.= "Referring URL: ". $_POST['uri']."\n";
    $html.= "Landed on page: ". $_POST['landed']."\n";
    $html.= "Timestamp: ". $_POST['timestamp'];
  } else {
    $html.= "\n\nNo referral data\n";
  }
  return $html;
}

It should be very obvious what is going on here. To enable all our forms to collect referrer information on submission we need to make a small modification to them. Assuming we are submitting via email our form will construct a message body which contains information included in the submitted form.

You'd want to sanitize the included data first, but below is an example of a very simple message body that we construct to send name and telephone number information from a form. This code would probably appear in a controller method called by the forms element's action attribute.

$msg = 'Name: ' . $_POST['name'] . "\n";
$msg.= 'Telephone: ' . $_POST['tel'] . "\n";

To append any available referrer information we use the addReferralData() method. First we need the controller namespace adding to the top of the file, it's the same line we used in the package controller.

use Concrete\Package\ReferralTracking\Controller\referralTracking;

Then we add this single method call to the routine that is building the message body.

$msg = 'Name: ' . $_POST['name'] . "\n";
$msg.= 'Telephone: ' . $_POST['tel'] . "\n";
$msg.= referralTracking:addReferralData();

And that's it! We have an affiliate programme. We can give out unique ids to our partners, who will link to our site using the format http://oursite.com/?rid=12345, and we'll be able to track referrals for up to 30 days all the way through to the contact forms they submit. If those contacts result in business we can ensure that partner gets whatever consideration has been agreed. I should caveat this, since it won't work where an affiliate partners website uses HTTPS if our site is HTTP since HTTP_REFERER will not be available, so this is something to be aware of.

One final thing. The sharp eyed will notice I used a generic PHP function to set the cookie, not the built in concrete5, or rather Symfony2 component cookie library. The reason is that I couldn't get the helper to set an expiry on the cookie, which may be a bug. The generic PHP function works just fine though.

You could go much further with this. A report that logs visits and contacts. A user profile page that gives out your affiliate programme ids so you don't have to do it manually. Why not have a go?

Join the conversation

comments powered by Disqus