For the launch of Wooki, we wanted to track our visitors to have some feedback about their use of the website.
To achieve that, we implemented a transparent service that automatically adds Google Analytics scripts to the end of pages. This can be easily done using the MarkupWriter and manipulating the generated Document.
Declaring a new MarkupRendererFilter
To indirectly manipulate the generated Document, we chose to create a new MarkupRendererFilter named “GAnalyticsScriptInjector”. Theses filter are dedicated to code execution before and/or after the main rendering process.
In our case, we wanted to add the script only when the application was in “production mode” and after the rendering process.
com.spreadthesource.tapestry.ganalytics.services.GAnalyticsModule
public void contributeMarkupRenderer(OrderedConfiguration<MarkupRendererFilter> configuration,
@Symbol(SymbolConstants.PRODUCTION_MODE) final boolean productionMode) {
if (productionMode) {
configuration.addInstance("GAnalyticsScript", GAnalyticsScriptsInjector.class, "after:RenderSupport");
}
}
Script injector filter
The filter just needed to add a script in the page. We added a Symbol to make this filter more configurable: the Google Analytics key can be provided by your AppModule, in a web.xml, in a properties file, or as a JVM argument.
com.spreadthesource.tapestry.ganalytics.services.GAnalyticsScriptsInjector
public class GAnalyticsScriptsInjector implements MarkupRendererFilter {
private final static Messages SCRIPTS = MessagesImpl.forClass(GAnalyticsScriptsMessages.class);
private final String key;
public GAnalyticsScriptsInjector(@Inject @Symbol(GAnalyticsConstants.GANALYTICS_KEY) String key) {
this.key = key;
}
private void addScript(Document document) {
if (key != null && !key.trim().equals("")) {
Element root = document.getRootElement();
if (root == null)
return;
Element body = root.find("body");
if (body == null) {
body = root.element("body");
}
Element e = body.element("script", "type", "text/javascript");
e.raw(SCRIPTS.get("scriptOne"));
e = body.element("script", "type", "text/javascript");
e.raw(SCRIPTS.format("scriptTwo", key));
}
}
public void renderMarkup(MarkupWriter writer, MarkupRenderer renderer) {
renderer.renderMarkup(writer);
this.addScript(writer.getDocument());
}
}
Finalisation
We said that we made possible to configure the Google Analytics key, so we created an interface to store the constant valued to the key symbol.
com.spreadthesource.tapestry.ganalytics.GAnalyticsConstants
public interface GAnalyticsConstants {
public static final String GANALYTICS_KEY = "ganalytics.key";
}
To get the Google Analytics scripts, we used the facility provided by “Messages.forClass()”: you just have to create a class which names ends by “Messages” and Tapestry will automatically load the catalog messages corresponding to yourClassNameStrings.
com.spreadthesource.tapestry.ganalytics.GAnalyticsScriptsMessages
public class GAnalyticsScriptsMessages {
}
com.spreadthesource.tapestry.ganalytics.GAnalyticsScriptsStrings.properties
scriptOne=var gaJsHost = (("https:" == document.location.protocol) ? "https://ssl." : "http://www.");\n \
document.write(unescape("%3Cscript src='" + gaJsHost + "google-analytics.com/ga.js' type='text/javascript'%3E%3C/script%3E"));
scriptTwo=try { \n\
var pageTracker = _gat._getTracker("%s");\n\
pageTracker._trackPageview();\n\
} catch(err) {}
We also contributed to the “factory defaults” in order to declare the Symbol:
com.spreadthesource.tapestry.ganalytics.services.GAnalyticsModule
public static void contributeFactoryDefaults(MappedConfiguration<String, String> configuration) {
configuration.add(GAnalyticsConstants.GANALYTICS_KEY, "");
}
Check out the source code on Github!
This feature has been extracted from Wooki and is available as a standalone project. You just have to include it as a dependency into your own project. Download it now from :
http://github.com/robink/tap-ganalytics/
Let the magic happen
We use Jetty webserver to host Wooki in production. We edited the webdefault.xml file and added this:
<context-param> <param-name>ganalytics.key<param-name> <param-value>UA-dummyGA-foo</param-value> </context-param>
So now, every time we will add a project that include our library as a dependency, Google Analytics code will be added. No more configuration, no declaration in our project, just a dependency. And that last thing is really awesome. :)


Another useful article here … you guys are digging nice & deep.
I think you should identify that GAnalyticsScriptsStrings is a .properties file, explicitly.
In addition, I’m finding the code to inject the final JavaScript a little awkward, because it duplicates a lot of code present in the internal DocumentLinkerImpl class; is it not possible to inject the JavaScript using the normal RenderSupport environmental? That is, can the Google JS execute from inside a Prototype “document:loaded” event handler function?
I know the Google people use document.write() to add the link to load their JS, but is that actually necessary? I think they do that so that you can cut-n-paste a single blob of JS into your page (or template), but it seems to me you could just add an explicit link to the page (i.e., what scriptOne does). The remaining code in ScriptTwo is so simple, it could just about as easily be inline Java code using String.format().
Keep up the good work!
Also, I think I would have merged the filter and the service into one thing; you can still inject into objects, as long as you autobuild them (via the @Autobuild annotation on a parameter, or via the configuration’s addInstance() method).
Social comments and analytics for this post…
This post was mentioned on Twitter by spreadthesource: blog post: Track your visitors with Tapestry 5 Google Analytics Plugin : http://bit.ly/8BKmkt...
Thanx for the feedback, I’ve made the merged like you said for the filter and the service.
I also updated this post.
I didn’t updated the way the the Google Analytics script is launched. I’m aware that there is some code duplication, but I wanted to have the same output than what Google recommended to have.