Package net.ftb.tracking.google

Source Code of net.ftb.tracking.google.JGoogleAnalyticsTracker

/**
* 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 net.ftb.tracking.google;

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 net.ftb.log.Logger;

/**
* 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:
* <ul>
* <li>If you are tracking an event,
* {@link AnalyticsRequestData#setEventCategory(String)} and
* {@link AnalyticsRequestData#setEventAction(String)} must both be populated.</li>
* <li>If you are not tracking an event,
* {@link AnalyticsRequestData#setPageURL(String)} must be populated</li>
* </ul>
* See the <a href=http://code.google.com/intl/en-US/apis/analytics/docs/tracking/gaTrackingTroubleshooting.html#gifParameters>
* Google Troubleshooting Guide</a> for more info on the tracking parameters (although it doesn't seem to be fully updated).
* <p>
* The tracker can operate in three modes:
* <ul>
* <li>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.
* <li>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.
* <li>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.
* </ul> 
* </p>
* <p>
* 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).
* </p>
* @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 final ThreadGroup asyncThreadGroup = new ThreadGroup("Async Google Analytics Threads");
    private static long asyncThreadsRunning = 0;
    private static Proxy proxy = Proxy.NO_PROXY;
    private static LinkedList<String> fifo = new LinkedList<String>();
    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 GoogleAnalytics 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.
     * <p>
     * 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.
     * <p>
     * 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.
     * <p>
     * This works in queued and asynchronous mode.
     *
     * @param timeoutMillis  The maximum number of milliseconds to wait.
     */
    public static void completeBackgroundTasks (long timeoutMillis) {
        boolean fifoEmpty;
        boolean asyncThreadsCompleted;
        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:
     *            <code>"org/me/javaclass.java"</code>, 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:
     *            <code>"org/me/javaclass.java"</code>, 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:
     *            <code>"org/me/javaclass.java"</code>, 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.logInfo("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()) {
                @Override
                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.logError("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.setRequestProperty("Cache-Control", "no-transform");
            connection.setRequestMethod("GET");
            connection.setInstanceFollowRedirects(true);
            connection.connect();
            int responseCode = connection.getResponseCode();
            if (responseCode != HttpURLConnection.HTTP_OK) {
                Logger.logError("JGoogleAnalyticsTracker: Error requesting url '{}', received response code {}" + argURL + responseCode);
            }
            //      else {
            //        Logger.logInfo("JGoogleAnalyticsTracker: Tracking success");
            //      }
        } catch (Exception e) {
            Logger.logError("Error making tracking request", e);
        }
    }

    private void createBuilder () {
        switch (gaVersion) {
        case V_4_7_2:
            builder = new GoogleAnalytics(configData);
            break;
        default:
            builder = new GoogleAnalytics(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") {
                @Override
                public void run () {
                    Logger.logInfo("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.logError("Got exception from dispatch thread", e);
                        }
                    }
                }
            };
            backgroundThread.setDaemon(true);
            backgroundThread.start();
        }
    }

    /**
     * Stop the long-lived background thread.
     * <p>
     * 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;
        }
    }
}
TOP

Related Classes of net.ftb.tracking.google.JGoogleAnalyticsTracker

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.