Monday, November 24, 2008

More Google Analytics in SAP Portal with jQuery

One of the challenges with SAP Portal and integrating Google Analytics is it's tendency to create a lot of links that pop content open in a new window. Since you don't have access to the code that creates these URLs it causes a wee bit of a headache when you look to determine what items are being clicked on in the KM, or where your users are linking out of the portal to certain other applications.

We can resolve some of this by using a javascript library to scrape the HTML page and insert some onclick events that will allow the items to be tracked.

How can we accomplish this?

There are two steps:

First, add access to your favorite javascript library inside the Google Analytics code. I've chosen jQuery, although you could easily use other libraries. You can do this through the ga-split-1.js file that was outlined earlier. Don't forget to change the name of the file if need be so it is not cached in users browsers.


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"));
document.write(unescape("%3Cscript src='http://ajax.googleapis.com/ajax/libs/jquery/1.2.6/jquery.min.js' type='text/javascript'%3E%3C/script%3E"));


By pulling jQuery from Google, we're increasing our load time, since we won't have to wait on connections to the Portal. The risk is low, since it's Google. In addition, if jQuery isn't found, we just won't track certain types of links. We'd still get the key pack clicking information.

The second part is to actually use jQuery to track stuff. You might be able to use the jQuery GA plugin, but in my case, I decided to write my own javascript based upon the plugin to do the trick. I would keep this code in a separate file and load it after you've initialized the pageTracker within your PortalComponent:


function ga_decorateLink(u){
var trackingURL = '';
if(u.indexOf('://') == -1 && u.indexOf('mailto:') != 0){
// no protocol or mailto - internal link - check extension
var ext = u.split('.')[u.split('.').length - 1];
var exts = ['pdf','doc','xls','csv','jpg','gif', 'mp3','swf','txt','ppt','zip','gz','dmg','xml']
for(i = 0; i < exts.length; i++){
if(ext == exts[i]){
// Likely grabbing an item from KM, etc.
trackingURL = '/downloads/' + u;
break;
}
}
} else {
if(u.indexOf('mailto:') == 0){
// mailto link - decorate
trackingURL = '/mailto/' + u.substring(7);
} else {
// complete URL - check domain
var regex = /([^:\/]+)*(?::\/\/)*([^:\/]+)(:[0-9]+)*\/?/i;
var linkparts = regex.exec(u);
var urlparts = regex.exec(location.href);
if(linkparts[2] != urlparts[2]) trackingURL = '/external/' + u; /*leaving the portal*/
}
}
return trackingURL;
}

// Since you've initialized pageTracker in each Portal page, we're skipping that here.
// just wait until the entire page loads
$(document).ready(function(){
$('a').each(function(){
var u = $(this).attr('href');

if(typeof(u) != 'undefined'){
var newLink = decorateLink(u);
if(newLink.length){
$(this).click(function(){
$.pageTracker._trackPageview(newLink);
});
}
}
});
});


If you're using the defualt framework, be aware that you will not be able to track each and every link. Javascript cannot dive into iframes on the page. Since it can't do that, you'll be unable to track each and every link, unless you can use embedded for that particular iView which will eliminate the iframes.

Some fair warning here. This is from memory. I am no longer working with SAP Portal, so there is a good chance I've forgotten something here. However, it did work on my last day working with Portal...at least the version at that gig. If you run into problems, please fix them and share them. Don't hold onto it. Share it with the rest of the SAP community, post it on your blog, or submit it to SDN for inclusion in their hosted materials. At the very least, post a solution in the SDN forums so that others can use this. When you get it working, it's pretty darned cool!

Friday, November 21, 2008

An AIR based Woot-Off tracker

I've been going through some Flex training this week. It's an interesting tool, and pretty easy to make a quick application. Unfortunately, the training has been a bit robotic in terms of being very prescriptive on how to perform somewhat elementary programming. So it was time to take a break and actually try to attempt something that would be useful....or at least somewhat useful.

Since we had a Woot-Off yesterday, I decided to use Flex to write a Woot-Off tracker. A handy little AIR application to see when a new item appears. It's a simple single windowed application that polls Woot's API every 30 seconds, groks the RSSish feed, and displays information about the item being sold.

In addition, it provides handy information regarding the status of the sale in terms of percentage of items sold and a button to purchase the product, or manually check Woot for an update. If the percentage sold is above 94%, it ramps up the polling process to check Woot every second, since you never know when the BOC will appear.

The application is hardly complete. It lacks any style or substance in terms of look and feel. It also neglects the ability to run in the system tray (ala Twhirl or Tweetdeck) and update the user that an item might be selling out soon or that a new item is available. Right now it simply runs on the screen.

Of course, the trickiest part of this application is the need to run a Proxy service to hit an external URL. Due to Flash's security model, and the lack of a crossdomain.xml file at Woot, you need to have a local service running that will act as a proxy. A quick Java servlet and the very very lightweight Winstone servlet container. Ideally, you would launch this app with a little batch script that spun up your Servlet based proxy and then spun up the AIR app.

So let's walk through the source. That way, all of you out there who've actually done a lot of Flex and look at this and let me know what a BOC it is. :D First we'll look at the AIR app, and finally the Java based Proxy.


<?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute"
creationComplete="init()" width="500" height="370" xmlns:utils="flash.utils.*">
<mx:Script>
<![CDATA[
import flash.net.navigateToURL;
import flash.utils.Timer;
import mx.formatters.Formatter;
import mx.formatters.NumberFormatter;
import mx.collections.ArrayCollection;
import mx.controls.Image;
import mx.rpc.events.ResultEvent;

private var START_QUICK_POLL_PERCENT:Number = 0.94;

// a 30 second and a 1 second Timer
private var wootPing:Timer = new Timer(30000, 1000000000);
private var wootEndPing:Timer = new Timer(1000, 1000000000);
private var checks:int = 0;

[Bindable] private var wootItemText:String = "No Woot Found...Yet";
[Bindable] private var wootItemPrice:String = "$10,000,000.00";
[Bindable] private var wootItemPercent:String = "0% Sold";
[Bindable] private var wootItemLink:String = "http://www.woot.com";
[Bindable] private var checkText:String= "Checked\n0 Times";
[Bindable] private var itemImgURL:String = "";

// Setup the Timers, and start the default timer
private function init():void {
wootPing.addEventListener(Event.ACTIVATE, wootHandler);
wootPing.addEventListener(TimerEvent.TIMER, wootHandler);
wootPing.start();
wootEndPing.addEventListener(TimerEvent.TIMER, wootHandler);
}

// Handles the purchase button to open your browser
private function openWootWindow(event:MouseEvent):void {
var u:URLRequest = new URLRequest(wootItemLink);
flash.net.navigateToURL(u, "_blank");
}

// Generally use the Event to handle updating the app
private function wootHandler(event:Event):void {
getWoot();
}

// Hits the API.
private function getWoot():void {
wootService.send();
checkCount.text = "Checked\n"+ ++checks + " times";
}

// Updates all of the items when the HTTPService completes
private function wootResultHandler(event:ResultEvent):void {
wootItemText = wootService.lastResult.rss.channel.item.title;
wootItemPrice = wootService.lastResult.rss.channel.item.price;
wootItemLink = wootService.lastResult.rss.channel.item.purchaseurl;
wootDesc.htmlText = wootService.lastResult.rss.channel.item.description;
wootImage.source = wootService.lastResult.rss.channel.item.thumbnailimage;
// Determine if we need to do percentage checking.
if (!(new Boolean(wootService.lastResult.rss.channel.item.wootoff))) {
wootItemPercent = "This is not a Woot-Off";
} else {
var percentNum:Number = new Number(wootService.lastResult.rss.channel.item.soldoutpercentage);
wootItemPercent = new String(percentNum/100 + "% Sold");
// Do we need to start checking more often?
if (!wootEndPing.running && percentNum > START_QUICK_POLL_PERCENT) {
wootEndPing.start();
} else if (wootEndPing.running && percentNum < START_QUICK_POLL_PERCENT) {
wootEndPing.stop();
}
}
}
]]>
</mx:Script>
<mx:HTTPService url="http://localhost:8080/WootProxy"
id="wootService" result="wootResultHandler(event)" />

<mx:VBox left="5" right="5" top="5" bottom="5">
<mx:Label id="itemText" text="{wootItemText}" fontSize="12" fontWeight="bold"/>
<mx:Canvas width="100%">
<mx:Button toolTip="Click to purchase" label="Purchase" click="openWootWindow(event)" y="152" x="0"/>
<mx:TextArea height="300" id="wootDesc" left="150" right="0" />
<mx:Image id="wootImage" width="142" height="116" left="0" top="0" >
<mx:source>http://upload.wikimedia.org/wikipedia/en/1/16/Wootlogo.png</mx:source>
</mx:Image>
<mx:Label id="checkCount" text="{checkText}" x="0" y="212" height="52" width="142"/>
<mx:Button toolTip="Click to load Woot" label="Check" click="getWoot()" y="182" />
<mx:Label id="itemPrice" text="{wootItemPrice}" y="124" fontStyle="italic" fontSize="12" x="0" width="142"/>
<mx:Label id="itemPercent" text="{wootItemPercent}" y="272" x="0" width="142"/>
</mx:Canvas>
</mx:VBox>
</mx:WindowedApplication>

And finally the Proxy:

/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package org.woot.tracker;

import java.io.BufferedReader;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
*
* @author student
*/
public class WootProxy extends HttpServlet {

static final long serialVersionUID = 1L;

/**
* Processes requests for both HTTP <code>GET</code> and <code>POST</code> methods.
* @param request servlet request
* @param response servlet response
* @throws ServletException if a servlet-specific error occurs
* @throws IOException if an I/O error occurs
*/
protected void processRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {

String contentObj = "http://www.woot.com/salerss.aspx";

URL content = null;

if (null == contentObj) {
throw new ServletException("The destination url must be specified for ProxyHttpService");
}

try {
content = new URL(contentObj);
} catch (MalformedURLException e) {
throw new ServletException(contentObj + " is a malformed url.");
}
HttpURLConnection contentCon = null;
try {
contentCon = (HttpURLConnection) content.openConnection();
} catch (IOException exception) {
throw new ServletException("Problem opening " + contentObj + ": " + exception.toString());
}

// Get the content type from the URLConnection and set it on the response.
String contentType = contentCon.getContentType();
response.setContentType(contentType);

// Get and read the input stream.
StringBuffer buffer = new StringBuffer();

BufferedReader din =
new BufferedReader(new InputStreamReader(contentCon.getInputStream()));

String s;
while ((s = din.readLine()) != null) {
buffer.append(s);
}
din.close();

// Now write the bytes out to the client.
byte[] contentBytes = buffer.toString().getBytes();
OutputStream out = response.getOutputStream();
out.write(contentBytes, 0, contentBytes.length);
out.flush();
out.close();
}

// <editor-fold defaultstate="collapsed" desc="HttpServlet methods. Click on the + sign on the left to edit the code.">
/**
* Handles the HTTP <code>GET</code> method.
* @param request servlet request
* @param response servlet response
* @throws ServletException if a servlet-specific error occurs
* @throws IOException if an I/O error occurs
*/
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
processRequest(request, response);
}

/**
* Handles the HTTP <code>POST</code> method.
* @param request servlet request
* @param response servlet response
* @throws ServletException if a servlet-specific error occurs
* @throws IOException if an I/O error occurs
*/
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
processRequest(request, response);
}

/**
* Returns a short description of the servlet.
* @return a String containing servlet description
*/
@Override
public String getServletInfo() {
return "Short description";
}// </editor-fold>
}
There it is, enjoy :)

ShareThis