Digital Marketing Trends

Categories

Creating an exit survey using jQuery and Zend Framework

One of our clients was looking at his Google Analytics data and noticed that he was getting a high bounce rate on his site’s contact page. Naturally, he wanted to know why, so he asked us to create an exit survey that pops up when a user navigates away from the page. So, for example, when a user comes to the contact page, then clicks away to another page, they’re presented with a dialog box that looks like this:

Exit-Survey-Screenshot

Outline of Components

  1. Adding a unique id to the body tag of each page in the zend framework project
  2. A modal dialog using jQuery UI that displays the exit survey
  3. An AJAX call to our Zend Framework controller that logs the survey results
  4. A script that runs from the crontab to email the survey results on a daily basis

Directory Structure of Zend Framework Project

Here’s what the directory structure of my Zend Framework project looks like. Htdocs is the web root and all of the application files are in the FTP root, safely prevented from being accessed via the web in a folder called app_jsc.

Jsc-Directory-Tree

You’ll notice that the Zend Framework library is mysteriously missing. I keep this at the same directory level as app_jsc and htdocs because I might want to run multiple apps on this hosting account.

Add a Unique id to Each Page’s Body Tag

I like to add an id to the body tag of each of the pages in my Zend Framework projects. For example:

This comes in handy if you need to target css or javascript to certain pages only. In the app_jsc/views/layouts/main.phtml, put this code:

I have a controller plugin called ViewSetup that I use to execute certain code during the dispatch cycle. Setting up and registering controller plugins can vary depending on which version of Zend Framework you’re using, so I won’t cover how to specifically do that in this post, but you should be familiar with how this works. In the dispatchLoopStartup() of my plugin, I put this code that assigns a value to bodyId and passes it to the view:

public function dispatchLoopStartup(Zend_Controller_Request_Abstract $request)
{
  // Get the name of the controller and action of this request
  $controllerName = $request->getControllerName();
  $actionName = $request->getActionName();

  // Get static instance of ViewRenderer helper
  $viewRenderer = Zend_Controller_Action_HelperBroker::getStaticHelper('ViewRenderer');
  $viewRenderer->initView();
  $view = $viewRenderer->view;

  // Assign value to the bodyId and pass it to the view
  $view->bodyId = $controllerName . '-' . $actionName;
}

So, on the home page of the site, the HTML will look like:

jQuery UI Custom Build

First, we need to head over to jQuery UI and get the components we’ll need to construct the modal dialog. I want to download a custom version of the jQuery UI that contains only the components I really need for the exit survey. The UI demo pages are really good about specifying the dependencies needed for each widget to work. The jQuery Dialog widget needs UI Core, UI Draggable, and UI Resizeable. The last two are only necessary if I want the dialog to be resizeable or draggable (surprise). I might want this, so I’m going to include them in the custom build I specify on the download page. On this page, I specify that I want the Cupertino theme. I check the UI Core, Draggable and Resizeable from the Interactions, the Dialog widget, and all of the effects because I might want to apply one when the dialog appears. They aren’t necessary for this to work though.

When you build your own download package, jQuery is going to give you a zip file containing a css folder, a js folder, and another folder called development-bundle. I only need what’s in the css and js folders. The css folder will contain a folder with your theme name and, inside of that, the themed css file and an images folder. I’m just going to drop the cupertino folder into my public css folder (I call mine “c” for short) in my project. I’ll also drop the files from the js folder into my public js folder in my project. I need to place some code in app_jsc/views/layouts/main.phtml to make sure the css files and js files are included in my project.

Code to place in the head tag to include the css and javascript files:

<!-- First we need jquery -->
<script src="/js/jquery-1.3.2.min.js" type="text/javascript"><!--mce:0--></script>

<!– the custom jquery-ui javascript –>
<script src=”/js/jquery-ui-1.7.2.custom.min.js” type=”text/javascript”><!–mce:1–></script>

<!– the javascript where I put all of my DOM ready code –>
<script src=”/js/init.js” type=”text/javascript”><!–mce:2–></script>

<!– Main site stylesheet –>
<!– custom jquery-ui stylesheet –>

Now, you can get all fancy and use Zend Framework’s headScript, headLink, and headStyle helpers to append everything somewhere up the request dispatch cycle, but this is just a simple website that doesn’t use any logic to append certain styles or javascript. Every page gets everything, so let’s just keep it simple for now.

The HTML for the Modal Dialog

The HTML for the exit survey needs to go on the contact page (app_jsc/views/scripts/contact-us/index.phtml) and stay hidden until we call the dialog() method on it.

The Javascript

I like to put all of my DOM ready and onload code in a file called init.js and then include that on every page. I can hook page-specific javascript code on the unique body id that I’ve already set up. Here’s my init.js file:

$(document).ready(function(){

  // Check the radio button associated with the label
  $('span.reason').click(function(e){
    $(this).prev('input:first').attr('checked',true);
  });

  // Bind function to the click events of the navigation links
  $('#contact-us-index #topnav a, #contact-us-index #navigation a').click(function(e) {
    // Prevent the browser from loading the page the user tried to visit
    e.preventDefault();

    // Save the URL of where the user wanted to go
    destination = $(this).attr('href');

    // Show the exit survey
    showExitSurvey();
  });

});

function showExitSurvey()
{
  $('#survey').dialog({
    width: 500,
    bgiframe: true,
    modal: true,
    close: function() { $('#survey').dialog('destroy'); },  // Destroy the dialog.  Allows it to appear again if closed.
    buttons: {"Continue >>" : log_answer }                  // Add a continue button that logs the answer when clicked
  });
}

function log_answer() {
  $.ajax({
    type: "POST",
    url: "/contact-us/abandon/format/text",
    data: $("#survey-form").serialize(),
    dataType: "text",
    complete: function() { $(location).attr('href',destination); }
  });
  return;
}

The comments pretty well explain what is going on here, but lets discuss the basic flow. I like for the text associated with a radio button or check box on a form to select the radio button or checkbox when clicked, so that’s what my first bit of code does. When the text in the span tag is clicked, the first previous radio button sibling is checked.

Next, I’m going to assign a function to the click event of the page navigation links. Notice, I’m using my bodyId hook to only bind the click event to navigation links on the contact page. When the user clicks on a link, I want to stop the browser from taking them to the page they wanted, so I call preventDefault() on the click event. I’ll need to know where the user was going in order to redirect them after they fill out the exit survey, so I save the destination for later.

Finally, we call the showExitSurvey() function. This defines our jQuery modal dialog. The close option has a function associated with it that destroys the dialog box. If the user clicks on the X in the top right corner of the dialog to close it and then clicks on a link to leave the contact page again, this allows the dialog to reappear. I’m only defining one button on this dialog because I want to make it as simple as possible. I don’t want to force the user to enter an answer. They should just be able to hit the continue button and get on with what they were doing. The continue button has a callback associated with it, log_answer. Let’s take a look at that function.

The log_answer() function is where we build the AJAX call to our script that processes the survey. I’m simply sending the data as a POST request to our controller. I serialize the data from the form and tell jQuery to expect the AJAX response in plain text. When the AJAX request is complete, I forward the user on to where they wanted to go in the first place. I like to use the complete option instead of success. The complete event fires after the success and error events of the request. It’s pretty much the last event to be fired during the AJAX request cycle, so you can be sure that everything is done. In a more complex setup, you could have other things happen during the success and error events before redirecting, but here, I just want it to fail silently and let the user get on with their browsing.

A Word About When a User Closes the Browser Window

It would be nice to administer the survey when the user tries to close their browser window. Unfortunately, there is no good cross-browser way that I know of (commenters, got any ideas?) to implement this. Gmail users have seen something similar to this when they try to close the browser window before finishing a new email. It will say something like, “Are you sure you want to leave this page?” warning us that our changes will be lost. This topic (the onbeforeunload event) has been covered in great detail by many other bloggers.The bottom line is that, while it is possible to display a confirm message to prevent changes from being lost when we close a browser window, it is another matter entirely to collect data from a form and send the results to an asynchronous processing script.

The Controller

I need to tell my controller which actions can respond to AJAX requests. You do this by making use of the AjaxContext action helper. In this example, I’m using plain text as the AJAX response, so I’ll need to define this as a new context. I could easily have used xml or json as the response type. These are built in context types in Zend Framework, but this will give us an opportunity to see how to define a new context type for a controller action in Zend Framework. First, I need to get the AjaxContext helper, add my new context, then associate that context with the abandonAction() in my controller.

class ContactUsController extends Zend_Controller_Action
{

  public function init()
  {
    $ajaxContext = $this->_helper->getHelper('AjaxContext');
    $ajaxContext->addContext('text',
      array(
        'suffix' => 'text',
        'header' => array('
          Content-Type',
          'text/plain')
        )
      )
      ->addActionContext('abandon','text')
      ->initContext();
  }
}

What we’re doing here is using the addContext method to define our new ‘text’ context. After we define it, we can tell the controller that the abandon action can respond to XMLHttpRequests with this new context. The client request will be to /contact-us/abandon/format/text. You could just as easily say /contact-us/abandon?format=text.

View Scripts

First, let’s add the view scripts needed for the abandon action. Create two new files:

app_jsc/
  views/
    scripts/
      contact-us/
        abandon.phtml
        abondon.text.phtml

The first file, abandon.phtml, will be rendered if you type http://www.domain-name.com/contact-us/abandon/ into a browser. You don’t really need this file if you don’t care that an error is thrown should someone go to the URL. The second file, abandon.text.phtml, is the context-specific view script that will be rendered only if the controller detects an XMLHttpRequest (in other words, AJAX) to the abandon action. Note, that if you try to put http://www.domain-name.com/contact-us/abandon/format/text into the browser, the controller will still render the abandon.phtml view script because it’s not an AJAX request.

Here’s what’s in my abandon.text.phtml and my abandon.phtml file.

response ?>

The abandonAction()

class ContactUsController extends Zend_Controller_Action
{
  public function abandonAction()
  {
    // Bail if it wasn't a POST request
    if(!$this->_request->isPost())
    {
      $this->view->response = "false";
      return;
    }

    // Fetch the post variables into a local array
    $post = $this->_request->getPost();

    // If they selected nothing, reason set as no-reason, otherwise
    // it's set as the reason they selected
    $reason = empty($post['reasons'])  ? "No reason given" : $post['reasons'];

    // Set up a new logger that writes to a file
    // Log the reason
    $logger = new Zend_Log();
    $writer = new Zend_Log_Writer_Stream(APP_PATH . "/logs/contact-abandon-log.txt");
    $logger->addWriter($writer);
    $logger->log($reason,Zend_Log::INFO);

    // Tell the view that we're finished
    $this->view->response = "done";
  }
}

The first thing that happens here is the action just bails out if we don’t have a HTTP POST request. Then we’re going to use Zend_Log to create our log file. Passing a string to Zend_Log_Writer_Stream will always try to open a file in append mode or create the file if it doesn’t exist. So our log will just build and build each time this action is called.

Note that I did not call disableLayout() anywhere. That’s because the AjaxContext helper disables this for us. An AJAX request to this action will only render the code and text in the abandon.text.phtml view script.

So, now we’re logging people’s responses when they leave our fabulous page. Now to set up the script that will email the results daily.

The Emailer Script

I want a script that will check the contents of the log, email me the results, then clear out the log for the next day’s data.

app_jsc/
  cron_scripts/
    email-results-of-log.php
// Add zf library files to include path and require mailer
ini_set(
  'include_path',ini_get('include_path') .
  PATH_SEPARATOR . dirname(__FILE__) . "/the/relative/path/to/your/zf_library_files");
require_once('Zend/Mail.php');

// Get the contents of the log file
$filename = "/absolute/path/to/your/application/app_jsc/logs/contact-abandon-log.txt";
$contents = file_get_contents($filename);

// Bail out if there is nothing to report
if(empty($contents)) {exit(1);}

// Empty the log file
$handle = @fopen($filename,'w');
fclose($handle);

// Now send the mail
$body_html = nl2br($contents);
$mail = new Zend_Mail();
$mail->setFrom('info@thedomain.com',"Some Name");
$mail->addTo('person@test.com','Your Name');
$mail->setSubject('Reasons People Abandoned Contact Page - ' . date('Y-m-d'));
$mail->setBodyHtml($body_html);
$mail->send();

Pretty simple really. The only tricky part is making sure you add the Zend Framework library files to your include path. Then you can use Zend_Mail as a standalone component. That’s one of the things I like about Zend Framework. The ability to use parts of it as needed in your standalone scripts.

Schedule the Emailer to Run Automatically With Cron

I want my emailer script to send me the log file results every morning at 8:30am. That means executing this script from cron. Cron is just a task scheduler for unix/linux. If you’re not familiar with it, check it out, but chances are, if you’ve gotten this far in this article, you’re probably aware of it. You’ll need to work with your web host to find out how to edit your crontab. In this hosting environment, I have shell access, which allows me to use lots of powerful command line tools on the server. To edit my crontab, I type:

crontab -e

Now, in goes the following:

30 8 * * * php app_jsc/cron_scripts/email-results-of-abandon-log.php

Save the changes to your crontab. This will execute the command, “php app_jsc/cron_scripts/email-results-of-abandon-log.php,” in my user’s home directory every day at 8:30am. The email you receive every day should look something like this:

2009-11-30T15:30:43-05:00 INFO (6): No reason given
2009-11-30T15:43:04-05:00 INFO (6): Didn't want to leave email address

Obviously, if you’re site’s really busy, the log will grow quite large and maybe you’ll want to come up with a better way of examining the data.

Comments are closed.