Skip to content

Instantly share code, notes, and snippets.

@leggetter
Created April 20, 2012 01:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save leggetter/c5343258c6bc4a293d9e to your computer and use it in GitHub Desktop.
Save leggetter/c5343258c6bc4a293d9e to your computer and use it in GitHub Desktop.

Location aware Realtime Visitor Tracker using Pusher

The purpose of this tutorial is to show how to create a realtime location aware visitor tracker for a web site or application. When a visitor navigates to the page on your site, or accesses your mobile web app, the visitor tracker will display the new visitor hit in realtime, displaying the page they've landed on and where each visitor is on a Google map (lat/long and locality). For the purposes of the tutorial we'll write our server code in PHP, but you could use any server side language you like.

For the realtime functionality we'll use Pusher, who I work for. Pusher is a hosted service for quickly and easily adding realtime features into web and mobile applications. The service is used for all sorts of features such as notifications, game movements, chat, live data feeds and much more, so it fits the bill nicely here too. We'll be using the Pusher JavaScript API and the Pusher PHP Library.

Here's the order in which we'll cover things:

  1. Get the basics out of the way
  2. Connect to Pusher
  3. Subscribe to new visitor page hits notifications
  4. Get the visitor's location using the HTML5 Geolocation API
  5. Broadcast the visitor location and page to all application visitors using Pusher
  6. Display the visitor location and page information on a Google map

Getting Started

Before we start building the realtime tracker let's get a few basics set up.

Web Application Files

You can download a template containing all the files you'll need here. Here are the details about each file:

  • index.php - an example page
  • config.example.php - example application configuration including our Pusher app configuration
  • css
    • styles.css - CSS rules
  • js
    • tracker.js - the main body of JavaScript for the tracker
    • StyledMarker.js - used with Google maps later in the tutorial
  • pusher
    • new_hit.php - called whenever a new visitor is tracked
    • Pusher.php - the PHP Pusher library

Pusher Configuration

In order to use Pusher you need to set up a Pusher application. To do this you'll need to signup for a free Pusher sandbox account. Once you've signed up you'll be taken to a page which provides you with your API access credentials. Create a file called config.php, by renaming the existing config.example.php file, and add the credentials to it as follows:

<?php
define('APP_ID', 'YOUR_APP_ID');
define('APP_KEY', 'YOUR_APP_KEY');
define('APP_SECRET', 'YOUR_APP_SECRET');
?>

This file is included within the index.php and pusher/new_hit.php files e.g.

<?php
  require_once('config.php')
?>

We're nearly getting to the good stuff. The final things we want to do is expose the APP_KEY that we have set in the config.php file to JavaScript, include the Pusher JavaScript library and include a JavaScript file to contain our tracker logic. So, within index.php should now contain the following:

<script>
  var APP_KEY = '<?php echo(APP_KEY); ?>';
</script>
<script src="http://js.pusher.com/1.12/pusher.min.js"></script>
<script src="js/tracker.js"></script>

Debugging your app

It's essential to be able to keep track of what's going on within your Pusher application. So, we've created a debugging resource detailing ways to help you track down and troubleshoot problems.

Now that we have the application structure in place, and you know how to troubleshoot problems, we're in a good position where we can make a start on the really interesting code which gets us connected to Pusher.

Connecting to Pusher

Pusher uses WebSockets for realtime bi-directional communication between a user's web browser and Pusher. First we need to establish that WebSocket connection. We mentioned earlier that we would be using the Pusher JavaScript library. By using this library we can provide additional features that WebSockets don't natively offer but are essential when building realtime web applications; like auto reconnection.

Let's get connected by adding some code to js/tracker.js:

var pusher = new Pusher(APP_KEY);

It's as easy as that! But, let's also add some code to detect when we've connected. The Pusher JavaScript library uses events to expose new information or changes in state (we'll see more of this later). In the case of connecting we need to bind to a state_change event on a pusher.connection object. We do this using a bind function:

var pusher = new Pusher(APP_KEY);
pusher.connection.bind('state_change', function(state) {
  if( state.current === 'connected' ) {
    alert("Yipee! We've connected!");
  }
});

We won't use this functionality within the tracker so feel free to remove the pusher.connection.bind call.

Now that we need a way of registering interest in realtime visitor event data.

Subscribing to Visitor Hits

You access data using Pusher by subscribing to channels. Channels offer a great way of filtering data. In our case the channel is for visitor hits so we'll call it visitor-hits.

Now we have the channel name to subscribe to we simply need to call the subscribe function on our Pusher object:

var channel = pusher.subscribe('visitor-hits');

Once you are subscribed to the channel you can bind to an event on the channel, in the same way we bound to the state_change event on the pusher.connection object earlier. At this point we're not 100% subscribed to the channel, Pusher still needs to acknowledge the subscription. So, we can bind to a pusher:subscription_succeeded event so that we know when we are.

var channel = pusher.subscribe('visitor-hits');
channel('pusher:subscription_succeeded', function() {
  alert('subscribed!');
});

We also want to bind to a data event so we are notified whenever a new_hit occurs:

var channel = pusher.subscribe('visitor-hits');
channel.bind('pusher:subscription_succeeded', function() {
    // we'll add some code here later
});
channel.bind('new_hit', newHitReceived);

function newHitReceived() {
  alert('new hit');
}

Now we've got the code in place it would be great if we could test the Pusher data integration. We can do this using the Event Creator. Simply enter the channel name, event name and, for now, some example channel data and click the Send event button.

Pusher Event Creator

You'll then see the alert in the newHitReceived function trigger in the web app.

Now we've got the basics of the Pusher basics in place, let's take a look at accessing the visitor's location. Once we've got the location data into the event we'll look at hooking into displaying the data on a Google map.

Using the HTML5 Geolocation API to get the visitor location

The HTML5 Geolocation API provides a way for us to get the location from a visitor's web browser. To use this functionality the visitor will need to be using a modern web browser. Although there are polyfill solutions available for older browsers, we won't use them here.

The API is available on the navigator.geolocation object and It provides three functions:

  • getCurrentPosition(successCallback[, errorCallback, options])
  • var watchId = watchPosition(successCallback[, errorCallback, options])
  • clearWatch(watchId)

When you call either getCurrentPosition or watchPosition the browser will prompt the visitor, asking them if they wish to share their location with the web page or application.

HTML5 Geolocation Browser Prompt

If the visitor clicks "Allow" the successCallback will be called. Otherwise the errorCallback will be called. When the successCallback is called, an object is passed to that callback. The object looks as follows:

Tip: When developing a location aware application you may need to fake your geolocation. The [Manual Geolocation extension for Chrome](https://chrome.google.com/webstore/detail/mfodligkojepnddfhkbkodbamcagfhlo) and [Geolocator add-on for Firefox](https://addons.mozilla.org/en-us/firefox/addon/geolocater/) can come in very handy - I wasn't really in the local pub ;).

Get the visitor's location

Let's update the code in js/tracker.js to use the getCurrentPosition HTML5 Geolocation function once we get subscription confirmation to the visitor-hits channel:

var pusher = new Pusher(APP_KEY);
var channel = pusher.subscribe('visitor-hits');
pusher.bind('pusher:subscription_succeeded', function() {
  navigator.geolocation.getCurrentPosition(positionSuccess, positionError);
});

var visitorPosition = null;
function positionSuccess(position) {
  visitorPosition = position; // store for future reference
  var lat = visitorPosition.coordinates.latitude;
  var lng = visitorPosition.coordinates.longitude;
  // we'll use these variables later
}

function positionError(error) {
  alert('position error');
}

If the visitor declines to share their location the positionError function will be called. In this situation we could display a message to the visitor, but we'll concentrate on the success scenario.

Supplementing the data using the Google Maps API

As you can see from we can get the latitude and longitude from the web browser. But those values aren't all that readable. It would be great to have a friendly name to put to the lat/long values. Luckily the Google Maps JavaScript API comes with a Geocoder service that lets you fetch additional information about a set of lat/long coordinates.

To use the Google Maps API you first need to include the v3 JavaScript library:

<script>
  var APP_KEY = '<?php echo(APP_KEY); ?>';
</script>
<script src="http://js.pusher.com/1.12/pusher.min.js"></script>
<script src="http://maps.googleapis.com/maps/api/js?sensor=true"></script>
<script src="js/tracker.js"></script>

In js/tracker.js we can then use the Geocoder service to find the locality of the visitor. We won't dig into the following code too much. If you are interested in how it works have a look at the Geocoder service docs. The key point is that we either get a friendly locality name for the geolocation or a value of null:

var visitorPosition = null;
function positionSuccess(position) {
  visitorPosition = position; // store for future reference
  var lat = visitorPosition.coordinates.latitude;
  var lng = visitorPosition.coordinates.longitude;
  
  var location = new google.maps.LatLng(lat, lng);
  var geocoder = new google.maps.Geocoder();
  geocoder.geocode({location: location}, geocodeResponse);
}

In the response handler we use the findLocality helper function to find a nicely formatted locality to use:

function geocodeResponse(response, status) {
  var locality = null;
  if(status == google.maps.GeocoderStatus.OK) {
    locality = findLocality(response);
  }
  
  if(locality != null) {
    alert(locality.formatted_address);
  }
}

function findLocality(addresses) {
  if(addresses.length == 0) {
    return null;
  }
  
  for(var addrIndex = 0, addrCount = addresses.length; addrIndex < addrCount; ++addrIndex) {
    var address = addresses[addrIndex];
    for(var typeIndex = 0, typeCount = address.types.length; typeIndex < typeCount; ++typeIndex) {
      if(address.types[typeIndex] == "locality") {
        return address;
      }
    }
  }
  
  return addresses[0];
}

Now that we have the latitude and longitude, along with a friendly locality name, we are in the position to broadcast this information to any other connected visitors to ultimately be displayed in the tracker.

Broadcasting the visitor's location

In order to broadcast the data we've gathered about the visitor we're going to make an AJAX request to the server and use the Pusher PHP library to trigger an event. We could do this in the web browser using client events, but by doing this via the server we also benefit from:

  • Added security - we could verify and sanitise the data. Ultimately you can't trust the client.
  • Persistence - we could store the data for future reference.

Sending location info to the server

To keep things simple and avoid handling cross browser XMLHttpRequest inconsistencies in older browsers I'm going to include jQuery in index.php to use the jQuery.ajax function.

<script src="http://code.jquery.com/jquery-1.7.2.min.js"></script>

Now we can make the AJAX call and send the lat, long and locality to the server:

function geocodeResponse(response, status) {
  var locality = null;
  if(status == google.maps.GeocoderStatus.OK) {
    locality = findLocality(response);
  }
  
  if(locality != null) {
    $.ajax({
      url: 'pusher/new_hit.php',
      type: 'post',
      data: {
        locality: locality.formatted_address,
        lat: visitorPosition.coordinates.latitude,
        lng: visitorPosition.coordinates.longitude
      },
      success: function(data, textStatus, jqXHR) {
        alert('ok');
      },
      error: function(jqXHR, textStatus, errorThrown) {
        alert('error');
      }
    });
  }
}

Now that the JavaScript POSTs the values to the server as part of the AJAX call we can access them in our pusher/new_hit.php file. We can also get access to the page the visitor is on using $_SERVER['HTTP_REFERER']:

<?php
  require_once('../config.php');
  
  $data = array(
    'lat' => $_POST['lat'],
    'lng' => $_POST['lng'],
    'locality' => $_POST['locality'],
    'page' => $_SERVER['HTTP_REFERER']
  );
  
  header('Content-type: application/json');
  echo( json_encode($data) );
?>

In this file we extract them from the $_POST and put them in an associative array called $data. So that we can ensure things are working we'll also echo the data back to the client, encoded as JSON (we won't use this here, but it can be handy for debugging).

Triggering a Pusher event

You can trigger an event in just a few lines of code; require_once the Pusher PHP library, create an instance of a Pusher object and call trigger on it, passing the $data we've already created:

<?php
  require_once('../config.php');
  require_once('Pusher.php');
  
  $data = array(
    'lat' => $_POST['lat'],
    'lng' => $_POST['lng'],
    'locality' => $_POST['locality'],
    'page' => $_SERVER['HTTP_REFERER']
  );
  
  $pusher = new Pusher(APP_KEY, APP_SECRET, APP_ID);
  $pusher->trigger('visitor-hits', 'new_hit', $data);
  
  header('Content-type: application/json');
  echo( json_encode($data) );
?>

I'm sure you'll agree, that couldn't be easier. Of course, you should verify and sanitise your data if you want to do something similar in production.

When you load the web app now you'll get the new_hit alert we added earlier. It's now time to create a UI and display a Google Map. You'll probably want to remove that alert now too.

Displaying realtime visitors on a Google Map

The final steps involved creating the Google Map and then taking the new_hit event data and using it to display visitors on the map.

Creating the Google Map

We've already included the Google Maps JavaScript library so now we need to first define some basic HTML and CSS for the page and then create the Google Map object. The HTML couldn't be any simpler, just add a <div> with and id attribute with a value of map to the <body>:

<div id="map"></div>

We'll add CSS to make the map the full page size. Update css/styles.css to contain the following:

html, body {
  margin: 0;
  padding: 0;
  width: 100%;
  height: 100%;
}
#map {
  width: 100%;
  height: 100%;
}

The next thing to do is create the Google Map. To do this we need to create a new google.maps.Map object, passing in a reference to where we want the map to appear (in our <div id="map"></div>" element) and some options, on of which is where the map should centre. For this we'll use the visitor's location we've already got referenced through visitorPosition. We'll store a reference to the new Map object via a map variable as we'll use that later:

var map = null;
function createMap() {
  var visitorLocation =
    new google.maps.LatLng(
      visitorPosition.coords.latitude,
      visitorPosition.coords.longitude);
      
  var mapOptions = {
    zoom: 2,
    center: visitorLocation,
    mapTypeId: google.maps.MapTypeId.ROADMAP
  };
  
  map = new google.maps.Map(document.getElementById("map"), mapOptions);
}

We also need to call this new createMap function once the visitorPostion is set. Let's add a call in positionSuccess:

var visitorPosition = null;
function positionSuccess(position) {
  visitorPosition = position;
  var lat = visitorPosition.coords.latitude;
  var lng = visitorPosition.coords.longitude;
  
  var location = new google.maps.LatLng(lat, lng);
  var geocoder = new google.maps.Geocoder();
  geocoder.geocode({location: location}, function(response, status) {
    geocodeResponse.call(this, response, status, location)
  });
  
  createMap();
}

We've all seen a Google Map before, but for completeness here's what you will see:

Google Map

Using Pusher Event Data

We now want to use the data that is being passed to our newHitReceived received function and add a marker to the map to show where the visitor is. Surprisingly enough the default Marker doesn't come with the ability to easily display text so we'll use a StyledMarker library instead. To use this we just need to include our local version in index.php:

<script src="js/StyledMarker.js"></script>

We can then use it in our function which displays the visitor location on the map.

Before we add the marker it's worth covering the format of the data that is passed to the newHitReceived function. If you remember back to the PHP code we passed an array object to the trigger function:

$data = array(
  'lat' => $_POST['lat'],
  'lng' => $_POST['lng'],
  'locality' => $_POST['locality'],
  'page' => $_SERVER['HTTP_REFERER']
);

The Pusher PHP library converts this to a JSON representation so when we receive the data in our JavaScript it looks as demonstrated in the output from the JavaScript console below:

Event data stringified

This means we can easily access the lat, long and locality of the visitor and add a marker to the map:

function newHitReceived(data) {
  var latLng = new google.maps.LatLng(data.lat, data.lng);
  
  var styleMaker = new StyledMarker({
    styleIcon: new StyledIcon(
      StyledIconTypes.BUBBLE,
      {
        color:"fff",
        text:data.locality
      }),
    position: latLng,
    map: map
  });
}

If you try the tracker out now you will see visitor locations drop down, in realtime, as they arrive in the app. We'll add one final touch in the form of a popup InfoWindow which will display the visitors locality, geolocation and the page (using data.page) that they are on when it is clicked. The full source for the function becomes:

function newHitReceived(data) {
  var latLng = new google.maps.LatLng(data.lat, data.lng);
  
  var styleMaker = new StyledMarker({
    styleIcon: new StyledIcon(
      StyledIconTypes.BUBBLE,
      {
        color:"fff",
        text:data.locality
      }),
    position: latLng,
    map: map,
    animation: google.maps.Animation.DROP
  });
  
  var contentString = '' +
  '<div class="visitor-info">' +
    '<strong>Locality:</strong> ' + data.locality + '<br />' +
    '<strong>Lat/Lng:</strong> ' + data.lat + ', ' + data.lng + '<br />' +
    '<strong>Page:</strong> <a href="' + data.page + '">' + data.page.replace(/https?:\/\/[^/]*/, '') + '</a><br />' +
  '</div>';

  var infowindow = new google.maps.InfoWindow({content: contentString});
  google.maps.event.addListener(styleMaker, 'click', function() {
    infowindow.open(map, styleMaker);
  });
}

And that's it! We now have a fully functional location-aware realtime visitor tracking system, displaying visitor information on a Google Map.

Realtime Vistor Tracker using Pusher

Conclusion

We've covered quite a few things in reasonable detail in this tutorial:

  • Basic Pusher principles including connecting, subscribing to data, publishing data, binding to an event and using event data.
  • The HTML5 Geolocation API
  • Google Map basics

The tutorial has demonstrated how mashing these technologies together is not only fun, but also very powerful. As you get people visiting the application the realtime nature can really make it come alive, and give it a 'stickiness' that realtime technologies now make possible. Hopefully it's also inspired you add increased interaction and engagement to your existing applications, or to start a new project that focuses on Pusher powered realtime activity and user interaction.

Demo & Code

About the author:

Phil Leggetter is a Developer Evangelist at Pusher. He's been involved in developing and using realtime web technologies for over 10 years. His focus is to help people use these technologies to build the next generation of interactive and engaging realtime web applications.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment