diff --git a/README.rst b/README.rst index c880dc9..36d5aec 100644 --- a/README.rst +++ b/README.rst @@ -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. diff --git a/ckanext/googleanalytics/controller.py b/ckanext/googleanalytics/controller.py index e226db8..e804054 100644 --- a/ckanext/googleanalytics/controller.py +++ b/ckanext/googleanalytics/controller.py @@ -1,12 +1,116 @@ 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 plugin +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 GAApiController(ApiController): + # intercept API calls to record via google analytics + 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, + } + plugin.GoogleAnalyticsPlugin.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) diff --git a/ckanext/googleanalytics/fanstatic_library/googleanalytics_event_tracking.js b/ckanext/googleanalytics/fanstatic_library/googleanalytics_event_tracking.js index 10a50b3..60bcb5c 100644 --- a/ckanext/googleanalytics/fanstatic_library/googleanalytics_event_tracking.js +++ b/ckanext/googleanalytics/fanstatic_library/googleanalytics_event_tracking.js @@ -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); } }); } diff --git a/ckanext/googleanalytics/plugin.py b/ckanext/googleanalytics/plugin.py index febcee8..7e9655d 100644 --- a/ckanext/googleanalytics/plugin.py +++ b/ckanext/googleanalytics/plugin.py @@ -8,13 +8,42 @@ import pylons import ckan.lib.helpers as h import ckan.plugins as p import gasnippet +from routes.mapper import SubMapper, Mapper as _Mapper + +import urllib2 + +import threading +import Queue log = logging.getLogger('ckanext.googleanalytics') - class GoogleAnalyticsException(Exception): pass +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 GoogleAnalyticsPlugin(p.SingletonPlugin): p.implements(p.IConfigurable, inherit=True) @@ -23,6 +52,8 @@ class GoogleAnalyticsPlugin(p.SingletonPlugin): p.implements(p.IConfigurer, inherit=True) p.implements(p.ITemplateHelpers) + analytics_queue = Queue.Queue() + def configure(self, config): '''Load config settings for this extension from config file. @@ -55,6 +86,13 @@ class GoogleAnalyticsPlugin(p.SingletonPlugin): if not converters.asbool(config.get('ckan.legacy_templates', 'false')): p.toolkit.add_resource('fanstatic_library', 'ckanext-googleanalytics') + # 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 update_config(self, config): '''Change the CKAN (Pylons) environment configuration. @@ -67,6 +105,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. diff --git a/ckanext/googleanalytics/templates/googleanalytics/snippets/googleanalytics_header.html b/ckanext/googleanalytics/templates/googleanalytics/snippets/googleanalytics_header.html index 863a1e6..1486393 100644 --- a/ckanext/googleanalytics/templates/googleanalytics/snippets/googleanalytics_header.html +++ b/ckanext/googleanalytics/templates/googleanalytics/snippets/googleanalytics_header.html @@ -1,11 +1,10 @@