All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.dmurph.tracking.JGoogleAnalyticsTracker Maven / Gradle / Ivy

There is a newer version: 0.40.13
Show newest version
/**
 * Copyright (c) 2010 Daniel Murphy, Stefan Brozinski
 * 
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 * 
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
/**
 * Created at Jul 20, 2010, 4:04:22 AM
 */
package com.dmurph.tracking;

import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.Proxy.Type;
import java.net.SocketAddress;
import java.net.URL;
import java.util.LinkedList;
import java.util.Scanner;
import java.util.regex.MatchResult;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


/**
 * Common tracking calls are implemented as methods, but if you want to control
 * what data to send, then use {@link #makeCustomRequest(AnalyticsRequestData)}.
 * If you are making custom calls, the only requirements are:
 * 
    *
  • If you are tracking an event, * {@link AnalyticsRequestData#setEventCategory(String)} and * {@link AnalyticsRequestData#setEventAction(String)} must both be populated.
  • *
  • If you are not tracking an event, * {@link AnalyticsRequestData#setPageURL(String)} must be populated
  • *
* See the * Google Troubleshooting Guide for more info on the tracking parameters (although it doesn't seem to be fully updated). *

* The tracker can operate in three modes: *

    *
  • synchronous mode: The HTTP request is sent to GA immediately, before the track * method returns. * This may slow your application down if GA doesn't respond fast. *
  • multi-thread mode: Each track method call creates a new short-lived thread that sends * the HTTP request to GA in the background and terminates. *
  • single-thread mode (the default): The track method stores the request in a FIFO and returns * immediately. A single long-lived background thread consumes the FIFO content and sends the HTTP * requests to GA. *
*

*

* To halt the background thread safely, use the call {@link #stopBackgroundThread(long)}, where the parameter is the * timeout to wait for any remaining queued tracking calls to be made. Keep in mind that if new tracking requests are made * after the thread is stopped, they will just be stored in the queue, and will not be sent to GA until the thread is started again with * {@link #startBackgroundThread()} (This is assuming you are in single-threaded mode to begin with). *

* @author Daniel Murphy, Stefan Brozinski */ public class JGoogleAnalyticsTracker { public static enum DispatchMode { /** * Each tracking call will wait until the http request * completes before returning */ SYNCHRONOUS, /** * Each tracking call spawns a new thread to make the http request */ MULTI_THREAD, /** * Each tracking request is added to a queue, and a single dispatch thread makes the requests. */ SINGLE_THREAD } private static Logger logger = LoggerFactory.getLogger(JGoogleAnalyticsTracker.class); private static final ThreadGroup asyncThreadGroup = new ThreadGroup("Async Google Analytics Threads"); private static long asyncThreadsRunning = 0; private static Proxy proxy = Proxy.NO_PROXY; private static LinkedList fifo = new LinkedList(); private static Thread backgroundThread = null; // the thread used in 'queued' mode. private static boolean backgroundThreadMayRun = false; static { asyncThreadGroup.setMaxPriority(Thread.MIN_PRIORITY); asyncThreadGroup.setDaemon(true); } public static enum GoogleAnalyticsVersion { V_4_7_2 } private GoogleAnalyticsVersion gaVersion; private AnalyticsConfigData configData; private IGoogleAnalyticsURLBuilder builder; private DispatchMode mode; private boolean enabled; public JGoogleAnalyticsTracker(AnalyticsConfigData argConfigData, GoogleAnalyticsVersion argVersion) { this(argConfigData, argVersion, DispatchMode.SINGLE_THREAD); } public JGoogleAnalyticsTracker(AnalyticsConfigData argConfigData, GoogleAnalyticsVersion argVersion, DispatchMode argMode){ gaVersion = argVersion; configData = argConfigData; createBuilder(); enabled = true; setDispatchMode(argMode); } /** * Sets the dispatch mode * @see DispatchMode * @param argMode the mode to to put the tracker in. If this is null, the tracker * defaults to {@link DispatchMode#SINGLE_THREAD} */ public void setDispatchMode(DispatchMode argMode){ if(argMode == null){ argMode = DispatchMode.SINGLE_THREAD; } if(argMode == DispatchMode.SINGLE_THREAD){ startBackgroundThread(); } mode = argMode; } /** * Gets the current dispatch mode. Default is {@link DispatchMode#SINGLE_THREAD}. * @see DispatchMode * @return */ public DispatchMode getDispatchMode(){ return mode; } /** * Convenience method to check if the tracker is in synchronous mode. * @return */ public boolean isSynchronous(){ return mode == DispatchMode.SYNCHRONOUS; } /** * Convenience method to check if the tracker is in single-thread mode * @return */ public boolean isSingleThreaded(){ return mode == DispatchMode.SINGLE_THREAD; } /** * Convenience method to check if the tracker is in multi-thread mode * @return */ public boolean isMultiThreaded(){ return mode == DispatchMode.MULTI_THREAD; } /** * Resets the session cookie. */ public void resetSession() { builder.resetSession(); } /** * Sets if the api dispatches tracking requests. * * @param argEnabled */ public void setEnabled(boolean argEnabled) { enabled = argEnabled; } /** * If the api is dispatching tracking requests (default of true). * * @return */ public boolean isEnabled() { return enabled; } /** * Define the proxy to use for all GA tracking requests. *

* Call this static method early (before creating any tracking requests). * * @param argProxy The proxy to use */ public static void setProxy(Proxy argProxy) { proxy = (argProxy != null) ? argProxy : Proxy.NO_PROXY; } /** * Define the proxy to use for all GA tracking requests. *

* Call this static method early (before creating any tracking requests). * * @param proxyAddr "addr:port" of the proxy to use; may also be given as URL ("http://addr:port/"). */ public static void setProxy(String proxyAddr) { if (proxyAddr != null) { Scanner s = new Scanner(proxyAddr); // Split into "proxyAddr:proxyPort". proxyAddr = null; int proxyPort = 8080; try { s.findInLine("(http://|)([^:/]+)(:|)([0-9]*)(/|)"); MatchResult m = s.match(); if (m.groupCount() >= 2) { proxyAddr = m.group(2); } if ((m.groupCount() >= 4) && (!m.group(4).isEmpty())) { proxyPort = Integer.parseInt(m.group(4)); } } finally { s.close(); } if (proxyAddr != null) { SocketAddress sa = new InetSocketAddress(proxyAddr, proxyPort); setProxy(new Proxy(Type.HTTP, sa)); } } } /** * Wait for background tasks to complete. *

* This works in queued and asynchronous mode. * * @param timeoutMillis The maximum number of milliseconds to wait. */ public static void completeBackgroundTasks(long timeoutMillis) { boolean fifoEmpty = false; boolean asyncThreadsCompleted = false; long absTimeout = System.currentTimeMillis() + timeoutMillis; while (System.currentTimeMillis() < absTimeout) { synchronized (fifo) { fifoEmpty = (fifo.size() == 0); } synchronized (JGoogleAnalyticsTracker.class) { asyncThreadsCompleted = (asyncThreadsRunning == 0); } if (fifoEmpty && asyncThreadsCompleted) { break; } try { Thread.sleep(100); } catch (InterruptedException e) { break; } } } /** * Tracks a page view. * * @param argPageURL * required, Google won't track without it. Ex: * "org/me/javaclass.java", or anything you want as * the page url. * @param argPageTitle * content title * @param argHostName * the host name for the url */ public void trackPageView(String argPageURL, String argPageTitle, String argHostName) { trackPageViewFromReferrer(argPageURL, argPageTitle, argHostName, "http://www.dmurph.com", "/"); } /** * Tracks a page view. * * @param argPageURL * required, Google won't track without it. Ex: * "org/me/javaclass.java", or anything you want as * the page url. * @param argPageTitle * content title * @param argHostName * the host name for the url * @param argReferrerSite * site of the referrer. ex, www.dmurph.com * @param argReferrerPage * page of the referrer. ex, /mypage.php */ public void trackPageViewFromReferrer(String argPageURL, String argPageTitle, String argHostName, String argReferrerSite, String argReferrerPage) { if (argPageURL == null) { throw new IllegalArgumentException("Page URL cannot be null, Google will not track the data."); } AnalyticsRequestData data = new AnalyticsRequestData(); data.setHostName(argHostName); data.setPageTitle(argPageTitle); data.setPageURL(argPageURL); data.setReferrer(argReferrerSite, argReferrerPage); makeCustomRequest(data); } /** * Tracks a page view. * * @param argPageURL * required, Google won't track without it. Ex: * "org/me/javaclass.java", or anything you want as * the page url. * @param argPageTitle * content title * @param argHostName * the host name for the url * @param argSearchSource * source of the search engine. ex: google * @param argSearchKeywords * the keywords of the search. ex: java google analytics tracking * utility */ public void trackPageViewFromSearch(String argPageURL, String argPageTitle, String argHostName, String argSearchSource, String argSearchKeywords) { if (argPageURL == null) { throw new IllegalArgumentException("Page URL cannot be null, Google will not track the data."); } AnalyticsRequestData data = new AnalyticsRequestData(); data.setHostName(argHostName); data.setPageTitle(argPageTitle); data.setPageURL(argPageURL); data.setSearchReferrer(argSearchSource, argSearchKeywords); makeCustomRequest(data); } /** * Tracks an event. To provide more info about the page, use * {@link #makeCustomRequest(AnalyticsRequestData)}. * * @param argCategory * @param argAction */ public void trackEvent(String argCategory, String argAction) { trackEvent(argCategory, argAction, null, null); } /** * Tracks an event. To provide more info about the page, use * {@link #makeCustomRequest(AnalyticsRequestData)}. * * @param argCategory * @param argAction * @param argLabel */ public void trackEvent(String argCategory, String argAction, String argLabel) { trackEvent(argCategory, argAction, argLabel, null); } /** * Tracks an event. To provide more info about the page, use * {@link #makeCustomRequest(AnalyticsRequestData)}. * * @param argCategory * required * @param argAction * required * @param argLabel * optional * @param argValue * optional */ public void trackEvent(String argCategory, String argAction, String argLabel, Integer argValue) { AnalyticsRequestData data = new AnalyticsRequestData(); data.setEventCategory(argCategory); data.setEventAction(argAction); data.setEventLabel(argLabel); data.setEventValue(argValue); makeCustomRequest(data); } /** * Makes a custom tracking request based from the given data. * * @param argData * @throws NullPointerException * if argData is null or if the URL builder is null */ public synchronized void makeCustomRequest(AnalyticsRequestData argData) { if (!enabled) { logger.debug("Ignoring tracking request, enabled is false"); return; } if (argData == null) { throw new NullPointerException("Data cannot be null"); } if (builder == null) { throw new NullPointerException("Class was not initialized"); } final String url = builder.buildURL(argData); switch(mode){ case MULTI_THREAD: Thread t = new Thread(asyncThreadGroup, "AnalyticsThread-" + asyncThreadGroup.activeCount()) { public void run() { synchronized (JGoogleAnalyticsTracker.class) { asyncThreadsRunning++; } try { dispatchRequest(url); } finally { synchronized (JGoogleAnalyticsTracker.class) { asyncThreadsRunning--; } } } }; t.setDaemon(true); t.start(); break; case SYNCHRONOUS: dispatchRequest(url); break; default: // in case it's null, we default to the single-thread synchronized (fifo) { fifo.addLast(url); fifo.notify(); } if(!backgroundThreadMayRun){ logger.error("A tracker request has been added to the queue but the background thread isn't running.", url); } break; } } private static void dispatchRequest(String argURL) { try { URL url = new URL(argURL); HttpURLConnection connection = (HttpURLConnection)url.openConnection(proxy); connection.setRequestMethod("GET"); connection.setInstanceFollowRedirects(true); connection.connect(); int responseCode = connection.getResponseCode(); if (responseCode != HttpURLConnection.HTTP_OK) { logger.error("JGoogleAnalyticsTracker: Error requesting url '{}', received response code {}", argURL, responseCode); } else { logger.debug("JGoogleAnalyticsTracker: Tracking success for url '{}'", new Object[]{argURL}); } } catch (Exception e) { logger.error("Error making tracking request", e); } } private void createBuilder() { switch (gaVersion) { case V_4_7_2: builder = new GoogleAnalyticsV4_7_2(configData); break; default: builder = new GoogleAnalyticsV4_7_2(configData); break; } } /** * If the background thread for 'queued' mode is not running, start it now. */ private synchronized static void startBackgroundThread() { if (backgroundThread == null) { backgroundThreadMayRun = true; backgroundThread = new Thread(asyncThreadGroup, "AnalyticsBackgroundThread") { public void run() { logger.debug("AnalyticsBackgroundThread started"); while (backgroundThreadMayRun) { try { String url = null; synchronized (fifo) { if (fifo.isEmpty()) { fifo.wait(); } if (!fifo.isEmpty()) { // Get a reference to the oldest element in the FIFO, but leave it in the FIFO until it is processed. url = fifo.getFirst(); } } if (url != null) { try { dispatchRequest(url); } finally { // Now that we have completed the HTTP request to GA, remove the element from the FIFO. synchronized (fifo) { fifo.removeFirst(); } } } } catch (Exception e) { logger.error("Got exception from dispatch thread",e); } } } }; // Don't prevent the application from terminating. // Use completeBackgroundTasks() before exit if you want to ensure that all pending GA requests are sent. backgroundThread.setDaemon(true); backgroundThread.start(); } } /** * Stop the long-lived background thread. *

* This method is needed for debugging purposes only. Calling it in an application is not really * required: The background thread will terminate automatically when the application exits. * * @param timeoutMillis If nonzero, wait for thread completion before returning. */ public static void stopBackgroundThread(long timeoutMillis) { backgroundThreadMayRun = false; synchronized (fifo) { fifo.notify(); } if ((backgroundThread != null) && (timeoutMillis > 0)) { try { backgroundThread.join(timeoutMillis); } catch (InterruptedException e) { } backgroundThread = null; } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy