Merge pull request #6 from datagovau/api-analytics-event-tracking

Google Analytics Event Tracking for CKAN API
This commit is contained in:
David Read 2014-11-05 11:38:26 +00:00
commit 0febe11dc5
5 changed files with 205 additions and 11 deletions

View File

@ -18,6 +18,9 @@ Features
resource downloads will be displayed as Events in the Google Analytics
reporting interface.
* Adds Google Analytics Event Tracking to some API calls so that usage of the
API can be reported on via Google Analytics.
* Adds Google Analytics Event Tracking to group links on the home page,
user profile links, editing and saving user profiles, etc.

View File

@ -1,12 +1,151 @@
import logging
from ckan.lib.base import BaseController, c, render
from ckan.lib.base import BaseController, c, render, request
import dbutil
import urllib
import urllib2
import logging
import ckan.logic as logic
import hashlib
import Queue
import threading
from pylons import config
from webob.multidict import UnicodeMultiDict
from paste.util.multidict import MultiDict
from ckan.controllers.api import ApiController
log = logging.getLogger('ckanext.googleanalytics')
class GAController(BaseController):
def view(self):
# get package objects corresponding to popular GA content
c.top_packages = dbutil.get_top_packages(limit=10)
c.top_resources = dbutil.get_top_resources(limit=10)
return render('summary.html')
class AnalyticsPostThread(threading.Thread):
"""Threaded Url POST"""
def __init__(self, queue):
threading.Thread.__init__(self)
self.queue = queue
def run(self):
while True:
# grabs host from queue
data_dict = self.queue.get()
data = urllib.urlencode(data_dict)
log.debug("Sending API event to Google Analytics: " + data)
# send analytics
urllib2.urlopen(
"http://www.google-analytics.com/collect",
data,
# timeout in seconds
# https://docs.python.org/2/library/urllib2.html#urllib2.urlopen
10)
# signals to queue job is done
self.queue.task_done()
class GAApiController(ApiController):
# intercept API calls to record via google analytics
analytics_queue = Queue.Queue()
def __init__(self):
# spawn a pool of 5 threads, and pass them queue instance
for i in range(5):
t = AnalyticsPostThread(self.analytics_queue)
t.setDaemon(True)
t.start()
def _post_analytics(
self, user, request_obj_type, request_function, request_id):
if config.get('googleanalytics.id'):
data_dict = {
"v": 1,
"tid": config.get('googleanalytics.id'),
"cid": hashlib.md5(user).hexdigest(),
# customer id should be obfuscated
"t": "event",
"dh": c.environ['HTTP_HOST'],
"dp": c.environ['PATH_INFO'],
"dr": c.environ.get('HTTP_REFERER', ''),
"ec": "CKAN API Request",
"ea": request_obj_type+request_function,
"el": request_id,
}
self.analytics_queue.put(data_dict)
def action(self, logic_function, ver=None):
try:
function = logic.get_action(logic_function)
side_effect_free = getattr(function, 'side_effect_free', False)
request_data = self._get_request_data(
try_url_params=side_effect_free)
if isinstance(request_data, dict):
id = request_data.get('id', '')
if 'q' in request_data:
id = request_data['q']
if 'query' in request_data:
id = request_data['query']
self._post_analytics(c.user, logic_function, '', id)
except Exception, e:
log.debug(e)
pass
return ApiController.action(self, logic_function, ver)
def list(self, ver=None, register=None,
subregister=None, id=None):
self._post_analytics(c.user,
register +
("_"+str(subregister) if subregister else ""),
"list",
id)
return ApiController.list(self, ver, register, subregister, id)
def show(self, ver=None, register=None,
subregister=None, id=None, id2=None):
self._post_analytics(c.user,
register +
("_"+str(subregister) if subregister else ""),
"show",
id)
return ApiController.show(self, ver, register, subregister, id, id2)
def update(self, ver=None, register=None,
subregister=None, id=None, id2=None):
self._post_analytics(c.user,
register +
("_"+str(subregister) if subregister else ""),
"update",
id)
return ApiController.update(self, ver, register, subregister, id, id2)
def delete(self, ver=None, register=None,
subregister=None, id=None, id2=None):
self._post_analytics(c.user,
register +
("_"+str(subregister) if subregister else ""),
"delete",
id)
return ApiController.delete(self, ver, register, subregister, id, id2)
def search(self, ver=None, register=None):
id = None
try:
params = MultiDict(self._get_search_params(request.params))
if 'q' in params.keys():
id = params['q']
if 'query' in params.keys():
id = params['query']
except ValueError, e:
log.debug(str(e))
pass
self._post_analytics(c.user, register, "search", id)

View File

@ -8,7 +8,7 @@ this.ckan.module('google-analytics', function(jQuery, _) {
jQuery('a.resource-url-analytics').on('click', function() {
var resource_url = encodeURIComponent(jQuery(this).prop('href'));
if (resource_url) {
_gaq.push(['_trackEvent', 'Resource', 'Download', resource_url]);
ga('send', 'event', 'Resource', 'Download', resource_url);
}
});
}

View File

@ -8,6 +8,7 @@ import pylons
import ckan.lib.helpers as h
import ckan.plugins as p
import gasnippet
from routes.mapper import SubMapper, Mapper as _Mapper
log = logging.getLogger('ckanext.googleanalytics')
@ -67,6 +68,58 @@ class GoogleAnalyticsPlugin(p.SingletonPlugin):
else:
p.toolkit.add_template_directory(config, 'templates')
def before_map(self, map):
'''Add new routes that this extension's controllers handle.
See IRoutes.
'''
# Helpers to reduce code clutter
GET = dict(method=['GET'])
PUT = dict(method=['PUT'])
POST = dict(method=['POST'])
DELETE = dict(method=['DELETE'])
GET_POST = dict(method=['GET', 'POST'])
# intercept API calls that we want to capture analytics on
register_list = [
'package',
'dataset',
'resource',
'tag',
'group',
'related',
'revision',
'licenses',
'rating',
'user',
'activity'
]
register_list_str = '|'.join(register_list)
# /api ver 3 or none
with SubMapper(map, controller='ckanext.googleanalytics.controller:GAApiController', path_prefix='/api{ver:/3|}',
ver='/3') as m:
m.connect('/action/{logic_function}', action='action',
conditions=GET_POST)
# /api ver 1, 2, 3 or none
with SubMapper(map, controller='ckanext.googleanalytics.controller:GAApiController', path_prefix='/api{ver:/1|/2|/3|}',
ver='/1') as m:
m.connect('/search/{register}', action='search')
# /api/rest ver 1, 2 or none
with SubMapper(map, controller='ckanext.googleanalytics.controller:GAApiController', path_prefix='/api{ver:/1|/2|}',
ver='/1', requirements=dict(register=register_list_str)
) as m:
m.connect('/rest/{register}', action='list', conditions=GET)
m.connect('/rest/{register}', action='create', conditions=POST)
m.connect('/rest/{register}/{id}', action='show', conditions=GET)
m.connect('/rest/{register}/{id}', action='update', conditions=PUT)
m.connect('/rest/{register}/{id}', action='update', conditions=POST)
m.connect('/rest/{register}/{id}', action='delete', conditions=DELETE)
return map
def after_map(self, map):
'''Add new routes that this extension's controllers handle.

View File

@ -1,11 +1,10 @@
<script type="text/javascript">
var _gaq = _gaq || [];
_gaq.push(['_setAccount', '{{googleanalytics_id}}']);
_gaq.push(['_setDomainName', '{{googleanalytics_domain}}']);
_gaq.push(['_trackPageview']);
(function() {
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
})();
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
ga('create', '{{googleanalytics_id}}', '{{googleanalytics_domain}}');
ga('set', 'anonymizeIp', true);
ga('send', 'pageview');
</script>