Tracking Events with Google Analytics in Struts Actions

Google Analytics

Google Analytics is a great marketing tool that can be used in order to get detailed information about the users of your web sites and their navigation patterns. The standard approach is to track page visits (where statistics can be aggregated by visited URLs), but there are times where you would want a higher granularity of which actions your users do, where these can’t be tracked exclusively by URL.

Analytics provides a mechanism called event tracking for that purpose. Events can be tracked by calling a JavaScript function _trackEvent. A sample usage would be:

var gaJsHost = (("https:" == document.location.protocol) ? "https://ssl." : "http://www.");
document.write(unescape("%3Cscript src='" + gaJsHost + "google-analytics.com/ga.js' type='text/javascript'%3E%3C/script%3E"));

// get a tracker with your user account code
var pageTracker = _gat._getTracker("UA-XXXXXX-X"); 

// optionally track the page view:
pageTracker._trackPageview();

// track an event:
pageTracker._trackEvent(category, event, label);

The parameters to track event (category, event and label) can be used within Analytics to obtain three levels of statistical aggregation of data. E.g. in our case we may use categories for login, document, subscription… Within document we have events for upload, sign, reject, add comment… As a label to document operations we may assign a user id, a document type, or however we wish the document events to be labelled for later analysis.

Integrating event tracking with Struts actions

Event tracking is based on a Javascript API, which means that you need to generate specific Javascript code within your pages depending on the results of your actions. This code must be present in the pages rendered as a consequence of the action, which sometimes may result in a redirect or action forward (for example, to a document list). A specific action may generate more than one event (e.g. “document was signed”, “user entered comments”).

The approach we used in order to be able to track events from within Struts can be summarised in the following steps:

  • An object associated with the user session keeps a list of generated events
  • A custom tag present in all pages flushes all pending events in the page (generates the Javascript code and clears the event list)
  • We include this custom tag as part of the default site template, so that it appears in all pages

Design Details

Our design includes some classes, plus a custom tag library:

  • GoogleAnalyticsUsageTracker – track events via Google analytics.
  • TrackedEvent – defines an event to be tracked (with category, name and label)
  • BaseAnalyticsAction – includes functionality to be inherited by action classes in the application. It defines:
    • trackEvent – use in your derived action classes in order to track events, adds an event to the list (more than one event can be tracked in an action)
    • getUsageTracker – called from within our analytics tag, it will let it obtain the usage tracker object and flush the recorded events: this will generate the javascript code and clear the accumulated event list

The files for the tag library are:

  • GoogleAnalyticsCodeTag: has three parameters – account code at google, domain, and an HTML id.
  • analytics.tld – the tag library itself

Sample tag usage:

<analytics:code googleCode="UA-XXX..." domain="example.com" id="ga-code"/>

Additional support in Javascript code

In order to simplify and unify event tracking within Javascript, the analytics:code tag will also generate a function trackEvent with the same signature as the one in the base action. This function will have an empty body if tracking is disabled. This allows adding trackEvent calls anywhere within your Javascript without having to check each time whether tracking is enabled or not.

The implementation

We start by creating a class for tracked events. Each instance will represent a trackable event that has occured during action execution:

package es.isigma.analytics;

public class TrackedEvent {
    public static final String JS_VARIABLE = "tracker";
    private static final String JS_NOLABEL = 
        JS_VARIABLE + "._trackEvent('%1$s', '%2$s');\n";
    private static final String JS = 
        JS_VARIABLE + "._trackEvent('%1$s', '%2$s', '%3$s');\n";

    private String category;
    private String name;
    private String label;

    public TrackedEvent(String category, String name, String label) {
        this.category = category;
        this.name = name;
        this.label = label;
    }

    public String getCategory() { return category; }
    public String getEvent()    { return name; }
    public String getLabel()    { return label; }

    @Override
    public String toString() {
        String jsCode;
        if (label == null)
            jsCode = String.format(JS_NOLABEL, this.category, this.name);
        else
            jsCode = String.format(JS, this.category, this.name, this.label);

        return jsCode;
    }
}

Next, the tracker class that will track the events. It will keep a list of event instances, plus methods to add an event (with or without label) and to flush the event list:

package es.isigma.analytics;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;



public class GoogleAnalyticsUsageTracker implements UsageTracker, Serializable {

    List<TrackedEvent> trackedEvents;

    public GoogleAnalyticsUsageTracker() {
        this.trackedEvents = new ArrayList<TrackedEvent>();
    }

    public boolean hasEvents() {
        return (trackedEvents.size() > 0);
    }

    public List<TrackedEvent> getTrackedEvents() {
        return this.trackedEvents;
    }

    public void trackEvent(String category, String name) {
        trackEvent(category, name, null);
    }

    public void trackEvent(String category, String name, String label) {
        TrackedEvent event = new TrackedEvent(category, name, label);
        this.trackedEvents.add(event);
    }

    public String flushEvents() {
        StringBuffer out = new StringBuffer();
        for (TrackedEvent event : this.trackedEvents) {
            out.append(event.toString());
        }
        // in order not to re-generate events, clear after output
        clearEvents();
        return out.toString();
    }

    public void clearEvents() {
        this.trackedEvents.clear();
    }
}

The custom tag

Our implementation of a custom tag will require two files, the clas itself and the taglib (.tld) file. The class implements the UsageTracker interface, which I will not list here. It declares the public methods for this class. Using an interface has additional advantages for unit testing with mock objects, or for injecting alternative implementations (we use Spring which is great for that purpose).

package es.isigma.analytics.taglib;

import javax.servlet.jsp.JspException;
import javax.servlet.jsp.tagext.TagSupport;

import com.opensymphony.xwork2.ActionContext;
import com.opensymphony.xwork2.util.ValueStack;

import es.isigma.analytics.TrackedEvent;
import es.isigma.analytics.UsageTracker;


public class GoogleAnalyticsCodeTag extends TagSupport {
    private static final long serialVersionUID = -8152994269436524442L;


    private final String HTML_TEMPLATE =
        "<script type='text/javascript'>\n" +
        // Track page view:
        "var gaJsHost = (('https:' == document.location.protocol) ? 'https://ssl.' : 'http://www.');\n" +
        "var sc = unescape('%%3Cscript src=\"' + gaJsHost + 'google-analytics.com/ga.js\" type=\"text/javascript\"%%3E%%3C/script%%3E');\n" +
        "document.write(sc);\n" +
        "</script>\n"  +
        "<script type='text/javascript'>\n" +
        "try {\n" +
        "var " + TrackedEvent.JS_VARIABLE + " = _gat._getTracker('%1$s');\n" +  
        TrackedEvent.JS_VARIABLE + "._setDomainName('%2$s');\n" +  
        TrackedEvent.JS_VARIABLE + "._trackPageview();\n" +
        // Track events in actions (from GoogleAnlyticsUsageTracker):
        "%3$s" + 
        "} catch(err) {\n" +
        "}\n" +
        // Add function for tracking directly from Javascript:
        "function trackEvent(category, name, label){\n" +
        "try {\n" +
        TrackedEvent.JS_VARIABLE + "._trackEvent(category, name, label);\n" +
        "} catch(err) {\n" +
        "}\n" +
        "}\n" +
        "</script>";


    private final String HTML_TEMPLATE_NO_TRACK =
        "<script type='text/javascript'>\n" +
        // Add dummy tracking:
        "function trackEvent(category, name, label){\n" +
        "}\n" +
        "</script>";


    private String googleCode;
    private String domain;

    public String getGoogleCode() { return googleCode; }
    public void setGoogleCode(String code) { this.googleCode = code; }
    public String getDomain() { return domain; }
    public void setDomain(String domain) { this.domain = domain; }

    /**
     * Process this tag
     * @return int status
     */
    @Override
    public int doStartTag() throws JspException {
        String template;
        if(googleCode != null && !googleCode.equals("")) {
            template = HTML_TEMPLATE;
        } else {
            template = HTML_TEMPLATE_NO_TRACK;
        }
        try {
            String formattedTag = 
                String.format(template, googleCode, domain, getEventScript());
            pageContext.getOut().write(formattedTag);
        } catch (Exception e) {
            throw new JspException(e);
        }
        return super.doStartTag();
    }


    private String getEventScript() {
        String eventScript = "";
        ActionContext a = ActionContext.getContext();
        ValueStack valueStack = a.getValueStack();

        // call method getUsageTracker() on current action:
        // this method will be present if the action inherits from 
        // BaseAnalyticsAction
        Object value = valueStack.findValue("usageTracker");

        if(value instanceof UsageTracker) {
            UsageTracker usageTracker = (UsageTracker)value;
            eventScript = usageTracker.flushEvents();
        }
        return eventScript;
    }
}

We will have to declare this custom tag as part of a tag library. For that purpose we create the analytics.tld file with a tag named code:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE taglib PUBLIC "-//Sun Microsystems, Inc.//DTD JSP Tag Library 1.2//EN"
    "http://java.sun.com/dtd/web-jsptaglibrary_1_2.dtd">
<taglib>
    <tlib-version>1.0</tlib-version>
    <jsp-version>1.2</jsp-version>
    <short-name>ism-struts</short-name>
    <uri>http://www.isigma.es/tags/analytics</uri>
    <tag>
        <name>code</name>
        <tag-class>es.isigma.analytics.taglib.GoogleAnalyticsCodeTag</tag-class>
        <attribute>
            <name>id</name>
            <required>false</required>
            <rtexprvalue>true</rtexprvalue>
        </attribute>
        <attribute>
            <name>googleCode</name>
            <required>true</required>
            <rtexprvalue>true</rtexprvalue>
        </attribute>
        <attribute>
            <name>domain</name>
            <required>true</required>
            <rtexprvalue>true</rtexprvalue>
        </attribute>
    </tag>    
</taglib>

The new Base Action – BaseAnalyticsAction

Finally, the glue that will put everything together is the action class that we can use as a base class for all our actions we want to participate in usage tracking. If you already have a base action class that inherits from ActionSupport (as we did), you just need to make it inherit from this one instead, and all your actions will have the added bonus of event tracking.

package es.isigma.analytics;

import javax.servlet.http.HttpSession;
import org.apache.struts2.ServletActionContext;
import com.opensymphony.xwork2.ActionSupport;


public class BaseAnalyticsAction extends ActionSupport {
    /** The Usage tracker session attribute */
    public static final String USAGE_TRACKER = "es.isigma.analytics.usage-tracker";

    private UsageTracker usageTracker = new GoogleAnalyticsUsageTracker();

    private HttpSession getSession() {
        HttpSession session = ServletActionContext.getRequest().getSession();
        return session;
    }

    public UsageTracker getUsageTracker() {
        UsageTracker tracker =
            (UsageTracker)getSession().getAttribute(USAGE_TRACKER);
        if (tracker == null && usageTracker != null) {
            // if first access, save object in session
            getSession().setAttribute(USAGE_TRACKER, usageTracker);
        }
        return tracker;
    }

    protected void trackEvent(String category, String name, String label) {
        UsageTracker sessionAnalyser = this.getUsageTracker();
        if(sessionAnalyser == null)
            return;
        sessionAnalyser.trackEvent(category, name, label);
    }

}

Now we can use the trackEvent method within any of our action classes, as long as we derive them from BaseAnalyticsAction:

public class DocumentAction extends BaseAnalyticsAction {

    // [...]

    /**
     * Action to upload a file
     */
    public String add() throws Throwable {
        // do stuff...
        // [...]
        
        // track the event
        trackEvent("document", "uploaded", owner.getId().toString());

        // do more...
        // [...]
        return SUCCESS;
    }

}

Adding the new tag to our pages

The only step left is to include the newly created tag in our pages, and we’re done! If you want tracking in all pages, just add it to the base template for your site, substituting the attributes with your user account and domain:

<analytics:code googleCode="UA-XXX..." domain="example.com" id="ga-code"/>

That’s it, we can start collecting statistics in Analytics and better find out how our users are succeeding or failing in using our site. Our tracker class will track page views (always) and events (only if triggered in the actions).

Advertisements

One thought on “Tracking Events with Google Analytics in Struts Actions

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s