Updated analytical

* Added Chartbeat.
* Added settings context processor.
* Added identification.
* Prepared events.
* Updated tests.
This commit is contained in:
Joost Cassee 2011-01-25 00:13:44 +01:00
parent 005cb2da40
commit 39c205f546
37 changed files with 910 additions and 235 deletions

View file

@ -4,6 +4,7 @@ django-analytical
The django-analytical application integrates various analytics services
into a Django_ project. Currently supported services:
* `Chartbeat`_ -- traffic analysis
* `Clicky`_ -- traffic analysis
* `Crazy Egg`_ -- visual click tracking
* `Google Analytics`_ traffic analysis
@ -23,6 +24,7 @@ Joshua Krall's all-purpose analytics front-end for Rails. The work on
Crazy Egg was made possible by `Bateau Knowledge`_.
.. _Django: http://www.djangoproject.com/
.. _Chartbeat: http://www.chartbeat.com/
.. _Clicky: http://getclicky.com/
.. _`Crazy Egg`: http://www.crazyegg.com/
.. _`Google Analytics`: http://www.google.com/analytics/

View file

@ -1,10 +1,11 @@
"""
========================================
Analytics service integration for Django
========================================
The django-clicky application integrates Clicky_ analytics into a
Django_ project.
The django-analytical application integrates analytics services into a
Django_ project. See the ``docs`` directory for more information.
.. _Django: http://www.djangoproject.com/
"""
__author__ = "Joost Cassee"
@ -12,3 +13,10 @@ __email__ = "joost@cassee.net"
__version__ = "0.1.0alpha"
__copyright__ = "Copyright (C) 2011 Joost Cassee"
__license__ = "MIT License"
try:
from collections import namedtuple
except ImportError:
namedtuple = lambda name, fields: lambda *values: values
Property = namedtuple('Property', ['num', 'name', 'value'])

View file

@ -0,0 +1,33 @@
"""
Context processors for django-analytical.
"""
from django.conf import settings
IMPORT_SETTINGS = [
'ANALYTICAL_INTERNAL_IPS',
'ANALYTICAL_SERVICES',
'CHARTBEAT_USER_ID',
'CLICKY_SITE_ID',
'CRAZY_EGG_ACCOUNT_NUMBER',
'GOOGLE_ANALYTICS_PROPERTY_ID',
'KISS_INSIGHTS_ACCOUNT_NUMBER',
'KISS_INSIGHTS_SITE_CODE',
'KISS_METRICS_API_KEY',
'MIXPANEL_TOKEN',
'OPTIMIZELY_ACCOUNT_NUMBER',
]
def settings(request):
"""
Import all django-analytical settings into the template context.
"""
vars = {}
for setting in IMPORT_SETTINGS:
try:
vars[setting] = getattr(settings, setting)
except AttributeError:
pass
return vars

View file

@ -12,20 +12,21 @@ from django.core.exceptions import ImproperlyConfigured
_log = logging.getLogger(__name__)
DEFAULT_SERVICES = [
'analytical.services.chartbeat.ChartbeatService',
'analytical.services.clicky.ClickyService',
'analytical.services.crazyegg.CrazyEggService',
'analytical.services.crazy_egg.CrazyEggService',
'analytical.services.google_analytics.GoogleAnalyticsService',
'analytical.services.kissinsights.KissInsightsService',
'analytical.services.kissmetrics.KissMetricsService',
'analytical.services.kiss_insights.KissInsightsService',
'analytical.services.kiss_metrics.KissMetricsService',
'analytical.services.mixpanel.MixpanelService',
'analytical.services.optimizely.OptimizelyService',
]
enabled_services = None
def get_enabled_services(reload=False):
def get_enabled_services():
global enabled_services
if enabled_services is None or reload:
if enabled_services is None:
enabled_services = load_services()
return enabled_services
@ -39,19 +40,7 @@ def load_services():
autoload = True
for path in service_paths:
try:
module, attr = path.rsplit('.', 1)
try:
mod = import_module(module)
except ImportError, e:
raise ImproperlyConfigured(
'error importing analytical service %s: "%s"'
% (module, e))
try:
service = getattr(mod, attr)()
except AttributeError:
raise ImproperlyConfigured(
'module "%s" does not define service "%s"'
% (module, attr))
service = _load_service(path)
enabled_services.append(service)
except ImproperlyConfigured, e:
if autoload:
@ -60,3 +49,18 @@ def load_services():
else:
raise
return enabled_services
def _load_service(path):
module, attr = path.rsplit('.', 1)
try:
mod = import_module(module)
except ImportError, e:
raise ImproperlyConfigured(
'error importing analytical service %s: "%s"' % (module, e))
try:
service = getattr(mod, attr)()
except (AttributeError, TypeError):
raise ImproperlyConfigured(
'module "%s" does not define callable service "%s"'
% (module, attr))
return service

View file

@ -5,6 +5,12 @@ from django.core.exceptions import ImproperlyConfigured
from django.conf import settings
HTML_COMMENT = "<!-- %(message):%(sep)s%(html)s%(sep)s-->"
JS_COMMENT = "*/ %(message):%(sep)s%(html)s%(sep)s*/"
IDENTITY_CONTEXT_KEY = 'analytical_identity'
class AnalyticalService(object):
"""
Analytics service.
@ -14,8 +20,6 @@ class AnalyticalService(object):
func_name = "render_%s" % location
func = getattr(self, func_name)
html = func(context)
if self.is_initialized(context):
pass
return html
def render_head_top(self, context):
@ -30,6 +34,9 @@ class AnalyticalService(object):
def render_body_bottom(self, context):
return ""
def render_event(self, name, properties):
return ""
def get_required_setting(self, setting, value_re, invalid_msg):
try:
value = getattr(settings, setting)
@ -37,5 +44,45 @@ class AnalyticalService(object):
raise ImproperlyConfigured("%s setting: not found" % setting)
value = str(value)
if not value_re.search(value):
raise ImproperlyConfigured("%s setting: %s" % (value, invalid_msg))
raise ImproperlyConfigured("%s setting: %s: '%s'"
% (setting, invalid_msg, value))
return value
def get_identity(self, context):
try:
return context[IDENTITY_CONTEXT_KEY]
except KeyError:
pass
if getattr(settings, 'ANALYTICAL_AUTO_IDENTIFY', True):
try:
try:
user = context['user']
except KeyError:
request = context['request']
user = request.user
if user.is_authenticated():
return user.username
except (KeyError, AttributeError):
pass
return None
def get_events(self, context):
return context.get('analytical_events', {})
def get_properties(self, context):
return context.get('analytical_properties', {})
def _html_comment(self, html, message=""):
return self._comment(HTML_COMMENT, html, message)
def _js_comment(self, html, message=""):
return self._comment(JS_COMMENT, html, message)
def _comment(self, format, html, message):
if not message:
message = "Disabled"
if message.find('\n') > -1:
sep = '\n'
else:
sep = ' '
return format % {'message': message, 'html': html, 'sep': sep}

View file

@ -0,0 +1,66 @@
"""
Chartbeat service.
"""
import re
from django.contrib.sites.models import Site, RequestSite
from django.core.exceptions import ImproperlyConfigured
from analytical.services.base import AnalyticalService
USER_ID_RE = re.compile(r'^\d{5}$')
INIT_CODE = """<script type="text/javascript">var _sf_startpt=(new Date()).getTime()</script>"""
SETUP_CODE = """
<script type="text/javascript">
var _sf_async_config={uid:%(user_id)s,domain:"%(domain)s"};
(function(){
function loadChartbeat() {
window._sf_endpt=(new Date()).getTime();
var e = document.createElement('script');
e.setAttribute('language', 'javascript');
e.setAttribute('type', 'text/javascript');
e.setAttribute('src',
(("https:" == document.location.protocol) ? "https://a248.e.akamai.net/chartbeat.download.akamai.com/102508/" : "http://static.chartbeat.com/") +
"js/chartbeat.js");
document.body.appendChild(e);
}
var oldonload = window.onload;
window.onload = (typeof window.onload != 'function') ?
loadChartbeat : function() { oldonload(); loadChartbeat(); };
})();
</script>
"""
DOMAIN_CONTEXT_KEY = 'chartbeat_domain'
class ChartbeatService(AnalyticalService):
def __init__(self):
self.user_id = self.get_required_setting(
'CHARTBEAT_USER_ID', USER_ID_RE,
"must be a string containing an five-digit number")
def render_head_top(self, context):
return INIT_CODE
def render_body_bottom(self, context):
return SETUP_CODE % {'user_id': self.user_id,
'domain': self._get_domain(context)}
def _get_domain(self, context):
try:
return context[DOMAIN_CONTEXT_KEY]
except KeyError:
pass
try:
return Site.objects.get_current().domain
except ImproperlyConfigured:
pass
try:
request = context['request']
return RequestSite(request).domain
except (KeyError, AttributeError):
raise KeyError("could not find access either '%s' or 'request' "
"in the template context and 'django.contrib.sites' is "
"not in INSTALLED_APPS" % DOMAIN_CONTEXT_KEY)

View file

@ -4,14 +4,17 @@ Clicky service.
import re
from django.utils import simplejson
from analytical.services.base import AnalyticalService
SITE_ID_RE = re.compile(r'^\d{8}$')
TRACKING_CODE = """
SETUP_CODE = """
<script type="text/javascript">
var clicky = { log: function(){ return; }, goal: function(){ return; }};
var clicky_site_id = %(site_id)s;
var clicky_custom = %(custom)s;
(function() {
var s = document.createElement('script');
s.type = 'text/javascript';
@ -22,14 +25,23 @@ TRACKING_CODE = """
</script>
<noscript><p><img alt="Clicky" width="1" height="1" src="http://in.getclicky.com/%(site_id)sns.gif" /></p></noscript>
"""
CUSTOM_CONTEXT_KEY = 'clicky_custom'
class ClickyService(AnalyticalService):
KEY = 'clicky'
def __init__(self):
self.site_id = self.get_required_setting('CLICKY_SITE_ID', SITE_ID_RE,
"must be a string containing an eight-digit number")
def render_body_bottom(self, context):
return TRACKING_CODE % {'site_id': self.site_id}
custom = {
'session': {
'username': self.get_identity(context),
}
}
custom.update(context.get(CUSTOM_CONTEXT_KEY, {}))
return SETUP_CODE % {'site_id': self.site_id,
'custom': simplejson.dumps(custom)}
def _convert_properties(self):
pass

View file

@ -8,23 +8,35 @@ from analytical.services.base import AnalyticalService
DEBUG_CODE = """
<script type="text/javascript">
if(typeof(console) !== 'undefined' && console != null) {
console.log('Analytical: rendering analytical_%(location)s tag');
%s
}
</script>
"""
LOG_CODE_ANONYMOUS = """
console.log('Analytical: rendering analytical_%(location)s tag');
"""
LOG_CODE_IDENTIFIED = """
console.log('Analytical: rendering analytical_%(location)s tag for user %(identity)s');
"""
class ConsoleService(AnalyticalService):
KEY = 'console'
def render_head_top(self, context):
return DEBUG_CODE % {'location': 'head_top'}
return self._render_code('head_top', context)
def render_head_bottom(self, context):
return DEBUG_CODE % {'location': 'head_bottom'}
return self._render_code('head_bottom', context)
def render_body_top(self, context):
return DEBUG_CODE % {'location': 'body_top'}
return self._render_code('body_top', context)
def render_body_bottom(self, context):
return DEBUG_CODE % {'location': 'body_bottom'}
return self._render_code('body_bottom', context)
def _render_code(self, location, context):
vars = {'location': location, 'identity': self.get_identity(context)}
if vars['identity'] is None:
debug_code = DEBUG_CODE % LOG_CODE_ANONYMOUS
else:
debug_code = DEBUG_CODE % LOG_CODE_IDENTIFIED
return debug_code % vars

View file

@ -8,17 +8,24 @@ from analytical.services.base import AnalyticalService
ACCOUNT_NUMBER_RE = re.compile(r'^\d{8}$')
TRACK_CODE = """<script type="text/javascript" src="//dnn506yrbagrg.cloudfront.net/pages/scripts/%(account_nr_1)s/%(account_nr_2)s.js"</script>"""
SETUP_CODE = """<script type="text/javascript" src="//dnn506yrbagrg.cloudfront.net/pages/scripts/%(account_nr_1)s/%(account_nr_2)s.js"</script>"""
USERVAR_CODE = "CE2.set(%(varnr)d, '%(value)s');"
USERVAR_CONTEXT_VAR = 'crazy_egg_uservars'
class CrazyEggService(AnalyticalService):
KEY = 'crazy_egg'
def __init__(self):
self.account_nr = self.get_required_setting('CRAZY_EGG_ACCOUNT_NUMBER',
ACCOUNT_NUMBER_RE,
"must be a string containing an eight-digit number")
def render_body_bottom(self, context):
return TRACK_CODE % {'account_nr_1': self.account_nr[:4],
html = SETUP_CODE % {'account_nr_1': self.account_nr[:4],
'account_nr_2': self.account_nr[4:]}
uservars = context.get(USERVAR_CONTEXT_VAR, {})
if uservars:
js = "".join(USERVAR_CODE % {'varnr': varnr, 'value': value}
for (varnr, value) in uservars.items())
html = '%s\n<script type="text/javascript">%s</script>' \
% (html, js)
return html

View file

@ -8,12 +8,12 @@ from analytical.services.base import AnalyticalService
PROPERTY_ID_RE = re.compile(r'^UA-\d+-\d+$')
TRACKING_CODE = """
SETUP_CODE = """
<script type="text/javascript">
var _gaq = _gaq || [];
_gaq.push(['_setAccount', '%(property_id)s']);
_gaq.push(['_trackPageview']);
%(commands)s
(function() {
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
@ -23,15 +23,36 @@ TRACKING_CODE = """
</script>
"""
TRACK_CODE = "_gaq.push(['_trackPageview']);"
CUSTOM_VARS_CONTEXT_KEY = "google_analytics_custom_vars"
CUSTOM_VAR_CODE = "_gaq.push(['_setCustomVar', %(index)d, '%(name)s', " \
"'%(value)s', %(scope)d]);"
class GoogleAnalyticsService(AnalyticalService):
KEY = 'google_analytics'
def __init__(self):
self.property_id = self.get_required_setting(
'GOOGLE_ANALYTICS_PROPERTY_ID', PROPERTY_ID_RE,
"must be a string looking like 'UA-XXXXXX-Y'")
def render_head_bottom(self, context):
return TRACKING_CODE % {'property_id': self.property_id}
commands = self._get_custom_var_commands(context)
commands.append(TRACK_CODE)
return SETUP_CODE % {'property_id': self.property_id,
'commands': " ".join(commands)}
def _get_custom_var_commands(self, context):
commands = []
vardefs = context.get(CUSTOM_VARS_CONTEXT_KEY, [])
for vardef in vardefs:
index = vardef[0]
if not 1 <= index <= 5:
raise ValueError("Google Analytics custom variable index must "
"be between 1 and 5: %s" % index)
name = vardef[1]
value = vardef[2]
if len(vardef) >= 4:
scope = vardef[3]
else:
scope = 2
commands.append(CUSTOM_VAR_CODE % locals())
return commands

View file

@ -0,0 +1,39 @@
"""
KISSinsights service.
"""
import re
from analytical.services.base import AnalyticalService
ACCOUNT_NUMBER_RE = re.compile(r'^\d{5}$')
SITE_CODE_RE = re.compile(r'^[\d\w]{3}$')
SETUP_CODE = """
<script type="text/javascript">var _kiq = _kiq || []; %(commands)s</script>
<script type="text/javascript" src="//s3.amazonaws.com/ki.js/%(account_number)s/%(site_code)s.js" async="true"></script>
"""
IDENTIFY_CODE = "_kiq.push(['identify', '%s']);"
SHOW_SURVEY_CODE = "_kiq.push(['showSurvey', %s]);"
SHOW_SURVEY_CONTEXT_KEY = 'kiss_insights_show_survey'
class KissInsightsService(AnalyticalService):
def __init__(self):
self.account_number = self.get_required_setting(
'KISS_INSIGHTS_ACCOUNT_NUMBER', ACCOUNT_NUMBER_RE,
"must be a string containing an five-digit number")
self.site_code = self.get_required_setting('KISS_INSIGHTS_SITE_CODE',
SITE_CODE_RE, "must be a string containing three characters")
def render_body_top(self, context):
commands = []
identity = self.get_identity(context)
if identity is not None:
commands.append(IDENTIFY_CODE % identity)
try:
commands.append(SHOW_SURVEY_CODE
% context[SHOW_SURVEY_CONTEXT_KEY])
except KeyError:
pass
return SETUP_CODE % {'account_number': self.account_number,
'site_code': self.site_code, 'commands': " ".join(commands)}

View file

@ -4,13 +4,16 @@ KISSmetrics service.
import re
from django.utils import simplejson
from analytical.services.base import AnalyticalService
API_KEY_RE = re.compile(r'^[0-9a-f]{40}$')
TRACKING_CODE = """
SETUP_CODE = """
<script type="text/javascript">
var _kmq = _kmq || [];
%(commands)s
function _kms(u){
setTimeout(function(){
var s = document.createElement('script');
@ -25,15 +28,24 @@ TRACKING_CODE = """
_kms('//doug1izaerwt3.cloudfront.net/%(api_key)s.1.js');
</script>
"""
IDENTIFY_CODE = "_kmq.push(['identify', '%s']);"
JS_EVENT_CODE = "_kmq.push(['record', '%(name)s', %(properties)s]);"
class KissMetricsService(AnalyticalService):
KEY = 'kissmetrics'
def __init__(self):
self.api_key = self.get_required_setting('KISSMETRICS_API_KEY',
self.api_key = self.get_required_setting('KISS_METRICS_API_KEY',
API_KEY_RE,
"must be a string containing a 40-digit hexadecimal number")
def render_head_top(self, context):
return TRACKING_CODE % {'api_key': self.api_key}
commands = []
identity = self.get_identity(context)
if identity is not None:
commands.append(IDENTIFY_CODE % identity)
return SETUP_CODE % {'api_key': self.api_key,
'commands': commands}
def render_event(self, name, properties):
return JS_EVENT_CODE % {'name': name,
'properties': simplejson.dumps(properties)}

View file

@ -1,30 +0,0 @@
"""
KISSinsights service.
"""
import re
from analytical.services.base import AnalyticalService
ACCOUNT_NUMBER_RE = re.compile(r'^\d{5}$')
SITE_CODE_RE = re.compile(r'^[\d\w]{3}$')
TRACKING_CODE = """
<script type="text/javascript">var _kiq = _kiq || [];</script>
<script type="text/javascript" src="//s3.amazonaws.com/ki.js/%(account_number)s/%(site_code)s.js" async="true"></script>
"""
class KissInsightsService(AnalyticalService):
KEY = 'kissinsights'
def __init__(self):
self.account_number = self.get_required_setting(
'KISSINSIGHTS_ACCOUNT_NUMBER', ACCOUNT_NUMBER_RE,
"must be a string containing an five-digit number")
self.site_code = self.get_required_setting('KISSINSIGHTS_SITE_CODE',
SITE_CODE_RE, "must be a string containing three characters")
def render_body_top(self, context):
return TRACKING_CODE % {'account_number': self.account_number,
'site_code': self.site_code}

View file

@ -4,35 +4,42 @@ Mixpanel service.
import re
from django.utils import simplejson
from analytical.services.base import AnalyticalService
MIXPANEL_TOKEN_RE = re.compile(r'^[0-9a-f]{32}$')
TRACKING_CODE = """
<script type='text/javascript'>
var mp_protocol = (('https:' == document.location.protocol) ? 'https://' : 'http://');
document.write(unescape('%%3Cscript src="' + mp_protocol + 'api.mixpanel.com/site_media/js/api/mixpanel.js" type="text/javascript"%%3E%%3C/script%%3E'));
</script>
<script type='text/javascript'>
try {
var mpmetrics = new MixpanelLib('%(token)s');
} catch(err) {
null_fn = function () {};
var mpmetrics = {track: null_fn, track_funnel: null_fn,
register: null_fn, register_once: null_fn,
register_funnel: null_fn};
}
SETUP_CODE = """
<script type="text/javascript">
var mpq = [];
mpq.push(['init', '%(token)s']);
%(commands)s
(function() {
var mp = document.createElement("script"); mp.type = "text/javascript"; mp.async = true;
mp.src = (document.location.protocol == 'https:' ? 'https:' : 'http:') + "//api.mixpanel.com/site_media/js/api/mixpanel.js";
var s = document.getElementsByTagName("script")[0]; s.parentNode.insertBefore(mp, s);
})();
</script>
"""
IDENTIFY_CODE = "mpq.push(['identify', '%s']);"
EVENT_CODE = "mpq.push(['track', '%(name)s', %(properties)s]);"
class MixpanelService(AnalyticalService):
KEY = 'mixpanel'
def __init__(self):
self.token = self.get_required_setting('MIXPANEL_TOKEN',
MIXPANEL_TOKEN_RE,
"must be a string containing a 32-digit hexadecimal number")
def render_body_bottom(self, context):
return TRACKING_CODE % {'token': self.token}
def render_head_bottom(self, context):
commands = []
identity = self.get_identity(context)
if identity is not None:
commands.append(IDENTIFY_CODE % identity)
return SETUP_CODE % {'token': self.token,
'commands': " ".join(commands)}
def render_event(self, name, properties):
return EVENT_CODE % {'name': name,
'properties': simplejson.dumps(properties)}

View file

@ -8,16 +8,14 @@ from analytical.services.base import AnalyticalService
ACCOUNT_NUMBER_RE = re.compile(r'^\d{7}$')
TRACKING_CODE = """<script src="//cdn.optimizely.com/js/%(account_number)s.js"></script>"""
SETUP_CODE = """<script src="//cdn.optimizely.com/js/%(account_number)s.js"></script>"""
class OptimizelyService(AnalyticalService):
KEY = 'optimizely'
def __init__(self):
self.account_number = self.get_required_setting(
'OPTIMIZELY_ACCOUNT_NUMBER', ACCOUNT_NUMBER_RE,
"must be a string containing an seven-digit number")
def render_head_top(self, context):
return TRACKING_CODE % {'account_number': self.account_number}
return SETUP_CODE % {'account_number': self.account_number}

View file

@ -5,12 +5,14 @@ from __future__ import absolute_import
from django import template
from django.conf import settings
from django.template import Node, TemplateSyntaxError
from django.template import Node, TemplateSyntaxError, Variable
from analytical.services import get_enabled_services
DISABLE_CODE = "<!-- Analytical disabled on internal IP address\n%s\n-->"
HTML_COMMENT_CODE = "<!-- Analytical disabled on internal IP address\n%s\n-->"
JS_COMMENT_CODE = "/* %s */"
SCRIPT_CODE = """<script type="text/javascript">%s</script>"""
register = template.Library()
@ -25,7 +27,7 @@ def _location_tag(location):
return tag
for l in ['head_top', 'head_bottom', 'body_top', 'body_bottom']:
register.tag('analytical_%s' % l, _location_tag(l))
register.tag('analytical_setup_%s' % l, _location_tag(l))
class AnalyticalNode(Node):
@ -36,13 +38,13 @@ class AnalyticalNode(Node):
getattr(settings, 'INTERNAL_IPS', ()))
def render(self, context):
html = "".join([self._render_service(service, context)
result = "".join([self._render_service(service, context)
for service in get_enabled_services()])
if not html:
if not result:
return ""
# if self._is_internal_ip(context):
# return DISABLE_CODE % html
return html
if self._is_internal_ip(context):
return HTML_COMMENT_CODE % result
return result
def _render_service(self, service, context):
func = getattr(service, self.render_func_name)
@ -56,3 +58,42 @@ class AnalyticalNode(Node):
return remote_ip in self.internal_ips
except KeyError, AttributeError:
return False
def event(parser, token):
bits = token.split_contents()
if len(bits) < 2:
raise TemplateSyntaxError("'%s' tag takes at least one argument"
% bits[0])
properties = _parse_properties(bits[0], bits[2:])
return EventNode(bits[1], properties)
register.tag('event', event)
class EventNode(Node):
def __init__(self, name, properties):
self.name = name
self.properties = properties
def render(self, context):
props = dict((var, Variable(val).resolve(context))
for var, val in self.properties)
result = "".join([service.render_js_event(props)
for service in get_enabled_services()])
if not result:
return ""
if self._is_internal_ip(context):
return JS_COMMENT_CODE % result
return result
def _parse_properties(tag_name, bits):
properties = []
for bit in bits:
try:
properties.append(bit.split('=', 1))
except IndexError:
raise TemplateSyntaxError("'%s' tag argument must be of the form "
" property=value: '%s'" % (tag_name, bit))
return properties

View file

@ -2,11 +2,13 @@
Tests for the Analytical analytics services.
"""
from analytical.tests.services.test_base import *
from analytical.tests.services.test_chartbeat import *
from analytical.tests.services.test_clicky import *
from analytical.tests.services.test_console import *
from analytical.tests.services.test_crazy_egg import *
from analytical.tests.services.test_google_analytics import *
from analytical.tests.services.test_kissinsights import *
from analytical.tests.services.test_kissmetrics import *
from analytical.tests.services.test_kiss_insights import *
from analytical.tests.services.test_kiss_metrics import *
from analytical.tests.services.test_mixpanel import *
from analytical.tests.services.test_optimizely import *

View file

@ -0,0 +1,74 @@
"""
Tests for the base service.
"""
import re
from django.contrib.auth.models import User, AnonymousUser
from django.core.exceptions import ImproperlyConfigured
from django.http import HttpRequest
from django.test import TestCase
from analytical.services.base import AnalyticalService
from analytical.tests.utils import TestSettingsManager
class DummyService(AnalyticalService):
def render_test(self, context):
return context
class BaseServiceTestCase(TestCase):
"""
Tests for the base service.
"""
def setUp(self):
self.settings_manager = TestSettingsManager()
self.service = DummyService()
def tearDown(self):
self.settings_manager.revert()
def test_render(self):
r = self.service.render('test', 'foo')
self.assertEqual('foo', r)
def test_get_required_setting(self):
self.settings_manager.set(TEST='test')
r = self.service.get_required_setting('TEST', re.compile('es'), 'fail')
self.assertEqual('test', r)
def test_get_required_setting_missing(self):
self.settings_manager.delete('TEST')
self.assertRaises(ImproperlyConfigured,
self.service.get_required_setting, 'TEST', re.compile('es'),
'fail')
def test_get_required_setting_wrong(self):
self.settings_manager.set(TEST='test')
self.assertRaises(ImproperlyConfigured,
self.service.get_required_setting, 'TEST', re.compile('foo'),
'fail')
def test_get_identity_none(self):
context = {}
self.assertEqual(None, self.service.get_identity(context))
def test_get_identity_authenticated(self):
context = {'user': User(username='test')}
self.assertEqual('test', self.service.get_identity(context))
def test_get_identity_authenticated_request(self):
req = HttpRequest()
req.user = User(username='test')
context = {'request': req}
self.assertEqual('test', self.service.get_identity(context))
def test_get_identity_anonymous(self):
context = {'user': AnonymousUser()}
self.assertEqual(None, self.service.get_identity(context))
def test_get_identity_non_user(self):
context = {'user': object()}
self.assertEqual(None, self.service.get_identity(context))

View file

@ -0,0 +1,66 @@
"""
Tests for the Chartbeat service.
"""
from django.conf import settings
from django.contrib.sites.models import Site
from django.core.exceptions import ImproperlyConfigured
from django.http import HttpRequest
from django.test import TestCase
from analytical.services.chartbeat import ChartbeatService
from analytical.tests.utils import TestSettingsManager
class ChartbeatTestCase(TestCase):
"""
Tests for the Chartbeat service.
"""
def setUp(self):
self.settings_manager = TestSettingsManager()
self.settings_manager.set(CHARTBEAT_USER_ID='12345')
self.service = ChartbeatService()
def tearDown(self):
self.settings_manager.revert()
def test_empty_locations(self):
self.assertEqual(self.service.render_head_bottom({}), "")
self.assertEqual(self.service.render_body_top({}), "")
def test_no_user_id(self):
self.settings_manager.delete('CHARTBEAT_USER_ID')
self.assertRaises(ImproperlyConfigured, ChartbeatService)
def test_wrong_user_id(self):
self.settings_manager.set(CHARTBEAT_USER_ID='1234')
self.assertRaises(ImproperlyConfigured, ChartbeatService)
self.settings_manager.set(CHARTBEAT_USER_ID='123456')
self.assertRaises(ImproperlyConfigured, ChartbeatService)
def test_rendering_init(self):
r = self.service.render_head_top({})
self.assertTrue('var _sf_startpt=(new Date()).getTime()' in r, r)
def test_rendering_setup(self):
r = self.service.render_body_bottom({'chartbeat_domain': "test.com"})
self.assertTrue('var _sf_async_config={uid:12345,domain:"test.com"};'
in r, r)
def test_rendering_setup_request_domain(self):
req = HttpRequest()
req.META['HTTP_HOST'] = 'test.com'
r = self.service.render_body_bottom({'request': req})
self.assertTrue('var _sf_async_config={uid:12345,domain:"test.com"};'
in r, r)
def test_rendering_setup_site(self):
installed_apps = list(settings.INSTALLED_APPS)
installed_apps.append('django.contrib.sites')
self.settings_manager.set(INSTALLED_APPS=installed_apps)
site = Site.objects.create(domain="test.com", name="test")
self.settings_manager.set(SITE_ID=site.id)
r = self.service.render_body_bottom({})
self.assertTrue('var _sf_async_config={uid:12345,domain:"test.com"};'
in r, r)

View file

@ -2,6 +2,9 @@
Tests for the Clicky service.
"""
import re
from django.contrib.auth.models import User
from django.core.exceptions import ImproperlyConfigured
from django.test import TestCase
@ -31,7 +34,7 @@ class ClickyTestCase(TestCase):
self.settings_manager.delete('CLICKY_SITE_ID')
self.assertRaises(ImproperlyConfigured, ClickyService)
def test_wrong_id(self):
def test_wrong_site_id(self):
self.settings_manager.set(CLICKY_SITE_ID='1234567')
self.assertRaises(ImproperlyConfigured, ClickyService)
self.settings_manager.set(CLICKY_SITE_ID='123456789')
@ -42,3 +45,16 @@ class ClickyTestCase(TestCase):
self.assertTrue('var clicky_site_id = 12345678;' in r, r)
self.assertTrue('src="http://in.getclicky.com/12345678ns.gif"' in r,
r)
def test_identify(self):
self.settings_manager.set(ANALYTICAL_AUTO_IDENTIFY=True)
r = self.service.render_body_bottom({'user': User(username='test')})
self.assertTrue(
'var clicky_custom = {"session": {"username": "test"}};' in r,
r)
def test_custom(self):
custom = {'var1': 'val1', 'var2': 'val2'}
r = self.service.render_body_bottom({'clicky_custom': custom})
self.assertTrue(re.search('var clicky_custom = {.*'
'"var1": "val1", "var2": "val2".*};', r), r)

View file

@ -2,6 +2,7 @@
Tests for the console debugging service.
"""
from django.contrib.auth.models import User
from django.test import TestCase
from analytical.services.console import ConsoleService
@ -18,15 +19,27 @@ class ConsoleTestCase(TestCase):
def test_render_head_top(self):
r = self.service.render_head_top({})
self.assertTrue('rendering analytical_head_top tag' in r, r)
r = self.service.render_head_top({'user': User(username='test')})
self.assertTrue('rendering analytical_head_top tag for user test'
in r, r)
def test_render_head_bottom(self):
r = self.service.render_head_bottom({})
self.assertTrue('rendering analytical_head_bottom tag' in r, r)
r = self.service.render_head_bottom({'user': User(username='test')})
self.assertTrue('rendering analytical_head_bottom tag for user test'
in r, r)
def test_render_body_top(self):
r = self.service.render_body_top({})
self.assertTrue('rendering analytical_body_top tag' in r, r)
r = self.service.render_body_top({'user': User(username='test')})
self.assertTrue('rendering analytical_body_top tag for user test'
in r, r)
def test_render_body_bottom(self):
r = self.service.render_body_bottom({})
self.assertTrue('rendering analytical_body_bottom tag' in r, r)
r = self.service.render_body_bottom({'user': User(username='test')})
self.assertTrue('rendering analytical_body_bottom tag for user test'
in r, r)

View file

@ -31,7 +31,7 @@ class CrazyEggTestCase(TestCase):
self.settings_manager.delete('CRAZY_EGG_ACCOUNT_NUMBER')
self.assertRaises(ImproperlyConfigured, CrazyEggService)
def test_wrong_id(self):
def test_wrong_account_number(self):
self.settings_manager.set(CRAZY_EGG_ACCOUNT_NUMBER='1234567')
self.assertRaises(ImproperlyConfigured, CrazyEggService)
self.settings_manager.set(CRAZY_EGG_ACCOUNT_NUMBER='123456789')
@ -40,3 +40,9 @@ class CrazyEggTestCase(TestCase):
def test_rendering(self):
r = self.service.render_body_bottom({})
self.assertTrue('/1234/5678.js' in r, r)
def test_uservars(self):
context = {'crazy_egg_uservars': {1: 'foo', 2: 'bar'}}
r = self.service.render_body_bottom(context)
self.assertTrue("CE2.set(1, 'foo');" in r, r)
self.assertTrue("CE2.set(2, 'bar');" in r, r)

View file

@ -31,10 +31,26 @@ class GoogleAnalyticsTestCase(TestCase):
self.settings_manager.delete('GOOGLE_ANALYTICS_PROPERTY_ID')
self.assertRaises(ImproperlyConfigured, GoogleAnalyticsService)
def test_wrong_id(self):
def test_wrong_property_id(self):
self.settings_manager.set(GOOGLE_ANALYTICS_PROPERTY_ID='wrong')
self.assertRaises(ImproperlyConfigured, GoogleAnalyticsService)
def test_rendering(self):
r = self.service.render_head_bottom({})
self.assertTrue("_gaq.push(['_setAccount', 'UA-123456-7']);" in r, r)
self.assertTrue("_gaq.push(['_trackPageview']);" in r, r)
def test_custom_vars(self):
context = {'google_analytics_custom_vars': [
(1, 'test1', 'foo'),
(5, 'test2', 'bar', 1),
]}
r = self.service.render_head_bottom(context)
self.assertTrue("_gaq.push(['_setCustomVar', 1, 'test1', 'foo', 2]);"
in r, r)
self.assertTrue("_gaq.push(['_setCustomVar', 5, 'test2', 'bar', 1]);"
in r, r)
self.assertRaises(ValueError, self.service.render_head_bottom,
{'google_analytics_custom_vars': [(0, 'test', 'test')]})
self.assertRaises(ValueError, self.service.render_head_bottom,
{'google_analytics_custom_vars': [(6, 'test', 'test')]})

View file

@ -2,10 +2,11 @@
Tests for the KISSinsights service.
"""
from django.contrib.auth.models import User
from django.core.exceptions import ImproperlyConfigured
from django.test import TestCase
from analytical.services.kissinsights import KissInsightsService
from analytical.services.kiss_insights import KissInsightsService
from analytical.tests.utils import TestSettingsManager
@ -16,8 +17,8 @@ class KissInsightsTestCase(TestCase):
def setUp(self):
self.settings_manager = TestSettingsManager()
self.settings_manager.set(KISSINSIGHTS_ACCOUNT_NUMBER='12345')
self.settings_manager.set(KISSINSIGHTS_SITE_CODE='abc')
self.settings_manager.set(KISS_INSIGHTS_ACCOUNT_NUMBER='12345')
self.settings_manager.set(KISS_INSIGHTS_SITE_CODE='abc')
self.service = KissInsightsService()
def tearDown(self):
@ -29,25 +30,34 @@ class KissInsightsTestCase(TestCase):
self.assertEqual(self.service.render_body_bottom({}), "")
def test_no_account_number(self):
self.settings_manager.delete('KISSINSIGHTS_ACCOUNT_NUMBER')
self.settings_manager.delete('KISS_INSIGHTS_ACCOUNT_NUMBER')
self.assertRaises(ImproperlyConfigured, KissInsightsService)
def test_no_site_code(self):
self.settings_manager.delete('KISSINSIGHTS_SITE_CODE')
self.settings_manager.delete('KISS_INSIGHTS_SITE_CODE')
self.assertRaises(ImproperlyConfigured, KissInsightsService)
def test_wrong_account_number(self):
self.settings_manager.set(KISSINSIGHTS_ACCOUNT_NUMBER='1234')
self.settings_manager.set(KISS_INSIGHTS_ACCOUNT_NUMBER='1234')
self.assertRaises(ImproperlyConfigured, KissInsightsService)
self.settings_manager.set(KISSINSIGHTS_ACCOUNT_NUMBER='123456')
self.settings_manager.set(KISS_INSIGHTS_ACCOUNT_NUMBER='123456')
self.assertRaises(ImproperlyConfigured, KissInsightsService)
def test_wrong_site_id(self):
self.settings_manager.set(KISSINSIGHTS_SITE_CODE='ab')
self.settings_manager.set(KISS_INSIGHTS_SITE_CODE='ab')
self.assertRaises(ImproperlyConfigured, KissInsightsService)
self.settings_manager.set(KISSINSIGHTS_SITE_CODE='abcd')
self.settings_manager.set(KISS_INSIGHTS_SITE_CODE='abcd')
self.assertRaises(ImproperlyConfigured, KissInsightsService)
def test_rendering(self):
r = self.service.render_body_top({})
self.assertTrue("//s3.amazonaws.com/ki.js/12345/abc.js" in r, r)
def test_identify(self):
self.settings_manager.set(ANALYTICAL_AUTO_IDENTIFY=True)
r = self.service.render_body_top({'user': User(username='test')})
self.assertTrue("_kiq.push(['identify', 'test']);" in r, r)
def test_show_survey(self):
r = self.service.render_body_top({'kiss_insights_show_survey': 1234})
self.assertTrue("_kiq.push(['showSurvey', 1234]);" in r, r)

View file

@ -2,10 +2,11 @@
Tests for the KISSmetrics service.
"""
from django.contrib.auth.models import User
from django.core.exceptions import ImproperlyConfigured
from django.test import TestCase
from analytical.services.kissmetrics import KissMetricsService
from analytical.services.kiss_metrics import KissMetricsService
from analytical.tests.utils import TestSettingsManager
@ -16,8 +17,8 @@ class KissMetricsTestCase(TestCase):
def setUp(self):
self.settings_manager = TestSettingsManager()
self.settings_manager.set(KISSMETRICS_API_KEY='0123456789abcdef0123456'
'789abcdef01234567')
self.settings_manager.set(KISS_METRICS_API_KEY='0123456789abcdef012345'
'6789abcdef01234567')
self.service = KissMetricsService()
def tearDown(self):
@ -29,18 +30,29 @@ class KissMetricsTestCase(TestCase):
self.assertEqual(self.service.render_body_bottom({}), "")
def test_no_api_key(self):
self.settings_manager.delete('KISSMETRICS_API_KEY')
self.settings_manager.delete('KISS_METRICS_API_KEY')
self.assertRaises(ImproperlyConfigured, KissMetricsService)
def test_wrong_api_key(self):
self.settings_manager.set(KISSMETRICS_API_KEY='0123456789abcdef0123456'
'789abcdef0123456')
self.settings_manager.set(KISS_METRICS_API_KEY='0123456789abcdef012345'
'6789abcdef0123456')
self.assertRaises(ImproperlyConfigured, KissMetricsService)
self.settings_manager.set(KISSMETRICS_API_KEY='0123456789abcdef0123456'
'789abcdef012345678')
self.settings_manager.set(KISS_METRICS_API_KEY='0123456789abcdef012345'
'6789abcdef012345678')
self.assertRaises(ImproperlyConfigured, KissMetricsService)
def test_rendering(self):
r = self.service.render_head_top({})
self.assertTrue("//doug1izaerwt3.cloudfront.net/0123456789abcdef012345"
"6789abcdef01234567.1.js" in r, r)
def test_identify(self):
self.settings_manager.set(ANALYTICAL_AUTO_IDENTIFY=True)
r = self.service.render_head_top({'user': User(username='test')})
self.assertTrue("_kmq.push(['identify', 'test']);" in r, r)
def test_event(self):
r = self.service.render_event('test_event', {'prop1': 'val1',
'prop2': 'val2'})
self.assertEqual(r, "_kmq.push(['record', 'test_event', "
'{"prop1": "val1", "prop2": "val2"}]);')

View file

@ -2,6 +2,7 @@
Tests for the Mixpanel service.
"""
from django.contrib.auth.models import User
from django.core.exceptions import ImproperlyConfigured
from django.test import TestCase
@ -25,8 +26,8 @@ class MixpanelTestCase(TestCase):
def test_empty_locations(self):
self.assertEqual(self.service.render_head_top({}), "")
self.assertEqual(self.service.render_head_bottom({}), "")
self.assertEqual(self.service.render_body_top({}), "")
self.assertEqual(self.service.render_body_bottom({}), "")
def test_no_token(self):
self.settings_manager.delete('MIXPANEL_TOKEN')
@ -41,6 +42,18 @@ class MixpanelTestCase(TestCase):
self.assertRaises(ImproperlyConfigured, MixpanelService)
def test_rendering(self):
r = self.service.render_body_bottom({})
self.assertTrue("MixpanelLib('0123456789abcdef0123456789abcdef')" in r,
r = self.service.render_head_bottom({})
self.assertTrue(
"mpq.push(['init', '0123456789abcdef0123456789abcdef']);" in r,
r)
def test_identify(self):
self.settings_manager.set(ANALYTICAL_AUTO_IDENTIFY=True)
r = self.service.render_head_bottom({'user': User(username='test')})
self.assertTrue("mpq.push(['identify', 'test']);" in r, r)
def test_event(self):
r = self.service.render_event('test_event', {'prop1': 'val1',
'prop2': 'val2'})
self.assertEqual(r, "mpq.push(['track', 'test_event', "
'{"prop1": "val1", "prop2": "val2"}]);')

View file

@ -18,30 +18,18 @@ class GetEnabledServicesTestCase(TestCase):
def setUp(self):
services.enabled_services = None
services.load_services = self._dummy_load_services_1
services.load_services = lambda: 'test'
def tearDown(self):
services.enabled_services = None
services.load_services = load_services
def test_no_reload(self):
def test_get_enabled_services(self):
result = services.get_enabled_services()
self.assertEqual(result, 'test1')
services.load_services = self._dummy_load_services_2
self.assertEqual(result, 'test')
services.load_services = lambda: 'test2'
result = services.get_enabled_services()
self.assertEqual(result, 'test1')
def test_reload(self):
result = services.get_enabled_services()
self.assertEqual(result, 'test1')
services.load_services = self._dummy_load_services_2
result = services.get_enabled_services(reload=True)
self.assertEqual(result, 'test2')
def _dummy_load_services_1(self):
return 'test1'
def _dummy_load_services_2(self):
return 'test2'
self.assertEqual(result, 'test')
class LoadServicesTestCase(TestCase):
@ -53,6 +41,7 @@ class LoadServicesTestCase(TestCase):
self.settings_manager = TestSettingsManager()
self.settings_manager.delete('ANALYTICAL_SERVICES')
self.settings_manager.delete('CLICKY_SITE_ID')
self.settings_manager.delete('CHARTBEAT_USER_ID')
self.settings_manager.delete('CRAZY_EGG_ACCOUNT_NUMBER')
self.settings_manager.delete('GOOGLE_ANALYTICS_PROPERTY_ID')
self.settings_manager.delete('KISSINSIGHTS_ACCOUNT_NUMBER')
@ -60,9 +49,11 @@ class LoadServicesTestCase(TestCase):
self.settings_manager.delete('KISSMETRICS_API_KEY')
self.settings_manager.delete('MIXPANEL_TOKEN')
self.settings_manager.delete('OPTIMIZELY_ACCOUNT_NUMBER')
services.enabled_services = None
def tearDown(self):
self.settings_manager.revert()
services.enabled_services = None
def test_no_services(self):
self.assertEqual(load_services(), [])

View file

@ -2,8 +2,11 @@
Tests for the template tags.
"""
from django.http import HttpRequest
from django import template
from django.test import TestCase
from analytical import services
from analytical.tests.utils import TestSettingsManager
@ -14,6 +17,74 @@ class TemplateTagsTestCase(TestCase):
def setUp(self):
self.settings_manager = TestSettingsManager()
self.settings_manager.set(ANALYTICAL_SERVICES=[
'analytical.services.console.ConsoleService'])
services.enabled_services = None
def tearDown(self):
self.settings_manager.revert()
services.enabled_services = None
def render_location_tag(self, location, context=None):
if context is None: context = {}
t = template.Template(
"{%% load analytical %%}{%% analytical_setup_%s %%}"
% location)
return t.render(template.Context(context))
def test_location_tags(self):
for l in ['head_top', 'head_bottom', 'body_top', 'body_bottom']:
r = self.render_location_tag(l)
self.assertTrue('rendering analytical_%s tag' % l in r, r)
def test_render_internal_ip(self):
self.settings_manager.set(ANALYTICAL_INTERNAL_IPS=['1.1.1.1'])
req = HttpRequest()
req.META['REMOTE_ADDR'] = '1.1.1.1'
for l in ['head_top', 'head_bottom', 'body_top', 'body_bottom']:
r = self.render_location_tag(l, {'request': req})
self.assertTrue('<!-- Analytical disabled on internal IP address'
in r, r)
def test_render_internal_ip_fallback(self):
self.settings_manager.set(INTERNAL_IPS=['1.1.1.1'])
req = HttpRequest()
req.META['REMOTE_ADDR'] = '1.1.1.1'
for l in ['head_top', 'head_bottom', 'body_top', 'body_bottom']:
r = self.render_location_tag(l, {'request': req})
self.assertTrue('<!-- Analytical disabled on internal IP address'
in r, r)
def test_render_internal_ip_forwarded_for(self):
self.settings_manager.set(ANALYTICAL_INTERNAL_IPS=['1.1.1.1'])
req = HttpRequest()
req.META['HTTP_X_FORWARDED_FOR'] = '1.1.1.1'
for l in ['head_top', 'head_bottom', 'body_top', 'body_bottom']:
r = self.render_location_tag(l, {'request': req})
self.assertTrue('<!-- Analytical disabled on internal IP address'
in r, r)
def test_render_different_internal_ip(self):
self.settings_manager.set(ANALYTICAL_INTERNAL_IPS=['1.1.1.1'])
req = HttpRequest()
req.META['REMOTE_ADDR'] = '2.2.2.2'
for l in ['head_top', 'head_bottom', 'body_top', 'body_bottom']:
r = self.render_location_tag(l, {'request': req})
self.assertFalse('<!-- Analytical disabled on internal IP address'
in r, r)
def test_render_internal_ip_empty(self):
self.settings_manager.set(ANALYTICAL_INTERNAL_IPS=['1.1.1.1'])
self.settings_manager.delete('ANALYTICAL_SERVICES')
self.settings_manager.delete('CLICKY_SITE_ID')
self.settings_manager.delete('CRAZY_EGG_ACCOUNT_NUMBER')
self.settings_manager.delete('GOOGLE_ANALYTICS_PROPERTY_ID')
self.settings_manager.delete('KISSINSIGHTS_ACCOUNT_NUMBER')
self.settings_manager.delete('KISSINSIGHTS_SITE_CODE')
self.settings_manager.delete('KISSMETRICS_API_KEY')
self.settings_manager.delete('MIXPANEL_TOKEN')
self.settings_manager.delete('OPTIMIZELY_ACCOUNT_NUMBER')
req = HttpRequest()
req.META['REMOTE_ADDR'] = '1.1.1.1'
for l in ['head_top', 'head_bottom', 'body_top', 'body_bottom']:
self.assertEqual(self.render_location_tag(l, {'request': req}), "")

View file

@ -4,6 +4,7 @@
# directory.
import sys, os
sys.path.append(os.path.join(os.path.abspath('.'), '.ext'))
sys.path.append(os.path.dirname(os.path.abspath('.')))
import analytical
@ -26,7 +27,10 @@ master_doc = 'index'
add_function_parentheses = True
pygments_style = 'sphinx'
intersphinx_mapping = {'http://docs.python.org/': None}
intersphinx_mapping = {
'http://docs.python.org/2.6': None,
'http://docs.djangoproject.com/en/1.2': 'http://docs.djangoproject.com/en/1.2/_objects/',
}
# -- Options for HTML output ---------------------------------------------------

View file

@ -10,6 +10,7 @@ into a Django_ project.
:Download: http://pypi.python.org/pypi/django-analytical/
:Source: http://github.com/jcassee/django-analytical
Overview
========
@ -22,14 +23,13 @@ the different analytics services behind a generic interface. It is
designed to make the common case easy while allowing advanced users to
customize tracking.
Features
--------
* Easy installation. See the :doc:`quick`.
* Supports many services. See :doc:`services/index`.
* Automatically identifies logged-in users (not implemented yet)
* Disables tracking on internal IP addresses (not implemented yet)
The application provides four generic template tags that are added to
the top and bottom of the head and body section of the base template.
Configured services will be enabled automatically by adding Javascript
code at these locations. The installation will follow the
recommendations from the analytics services, using an asynchronous
version of the code if possible. See :doc:`services/index` for detailed
information about each individual analytics service.
Contents
@ -38,7 +38,7 @@ Contents
.. toctree::
:maxdepth: 2
quick
install
services/index
settings
history

View file

@ -1,5 +1,6 @@
Installation and global configuration
=====================================
==============================
Installation and configuration
==============================
Integration of your analytics service is very simple. There are four
steps: installing the package, adding it to the list of installed Django
@ -9,11 +10,11 @@ the identifiers for the services you use to the project settings.
#. `Installing the Python package`_
#. `Installing the Django application`_
#. `Adding the template tags to the base template`_
#. `Configuring global settings`_
#. `Configuring the application`_
Installing the Python package
-----------------------------
=============================
To install django-analytical the ``analytical`` package must be added to
the Python path. You can install it directly from PyPI using
@ -21,7 +22,7 @@ the Python path. You can install it directly from PyPI using
$ easy_install django-analytical
You can also install directly from source. Download either the latest
You can also install directly from source. Download either the latest
stable version from PyPI_ or any release from GitHub_, or use Git to
get the development code::
@ -37,7 +38,7 @@ Then install by running the setup script::
Installing the Django application
---------------------------------
=================================
After you install django-analytical, add the ``analytical`` Django
application to the list of installed applications in the ``settings.py``
@ -51,7 +52,7 @@ file of your project::
Adding the template tags to the base template
---------------------------------------------
=============================================
Because every analytics service has uses own specific Javascript code
that should be added to the top or bottom of either the head or body
@ -63,23 +64,77 @@ Your base template should look like this::
<!DOCTYPE ... >
<html>
<head>
{% analytical_head_top %}
{% analytical_setup_head_top %}
...
{% analytical_head_bottom %}
{% analytical_setup_head_bottom %}
</head>
<body>
{% analytical_body_top %}
{% analytical_setup_body_top %}
...
{% analytical_body_bottom %}
{% analytical_setup_body_bottom %}
</body>
</html>
Configuring global settings
---------------------------
Configuring the application
===========================
The next step is to :doc:`configure the services <services/index>`.
Without configuration, the template tags all render the empty string.
You must enable at least one service, and optionally configure other
django-analytical features.
Enabling services
-----------------
By default, only configured analytics services are installed by the
template tags. You can also use the :data:`ANALYTICAL_SERVICES` setting
to specify the used services explicitly. Services are configured in the
project ``settings.py`` file. The settings required to enable each
service are listed here. See the service documentation for details.
* :doc:`Clicky <services/clicky>`::
CLICKY_SITE_ID = '12345678'
* :doc:`Crazy Egg <services/crazy_egg>`::
CRAZY_EGG_ACCOUNT_NUMBER = '12345678'
* :doc:`Google Analytics <services/google_analytics>`::
GOOGLE_ANALYTICS_PROPERTY_ID = 'UA-1234567-8'
* :doc:`KISSinsights <services/kiss_insights>`::
KISS_INSIGHTS_ACCOUNT_NUMBER = '12345'
KISS_INSIGHTS_SITE_CODE = 'abc'
* :doc:`KISSmetrics <services/kiss_metrics>`::
KISS_METRICS_API_KEY = '0123456789abcdef0123456789abcdef01234567'
* :doc:`Mixpanel <services/mixpanel>`::
MIXPANEL_TOKEN = '0123456789abcdef0123456789abcdef'
* :doc:`Optimizely <services/optimizely>`::
OPTIMIZELY_ACCOUNT_NUMBER = '1234567'
Configuring behavior
--------------------
By default, django-analytical will comment out the service
initialization code if the client IP address is detected as one from the
:data:`ANALYTICAL_INTERNAL_IPS` setting, which is set to
:data:`INTERNAL_IPS` by default.
Also, if the visitor is a logged in user and the user is accessible in
the template context, the username is passed to the analytics services
that support identifying users. See :data:`ANALYTICAL_AUTO_IDENTIFY`.

View file

@ -1,57 +0,0 @@
Quick Start Guide
=================
If you do not need any advanced analytics tracking, installing
django-analytical is very simple. To install django-analytical the
``analytical`` package must be added to the Python path. You can
install it directly from PyPI using ``easy_install``::
$ easy_install django-analytical
After you install django-analytical, add the ``analytical`` Django
application to the list of installed applications in the ``settings.py``
file of your project::
INSTALLED_APPS = [
...
'analytical',
...
]
Now add the django-analytical template tags to your base template::
{% load analytical %}
<!DOCTYPE ... >
<html>
<head>
{% analytical_head_top %}
...
{% analytical_head_bottom %}
</head>
<body>
{% analytical_body_top %}
...
{% analytical_body_bottom %}
</body>
</html>
Finally, configure the analytics services you use in the project
``settings.py`` file. This is a list of the settings required for the
different services::
CLICKY_SITE_ID = 'xxxxxxxx'
CRAZY_EGG_ACCOUNT_NUMBER = 'xxxxxxxx'
GOOGLE_ANALYTICS_ACCOUNT_NUMBER = 'UA-xxxxxx-x'
KISSINSIGHTS_ACCOUNT_NUMBER = 'xxxxx'
KISSINSIGHTS_ACCOUNT_NUMBER = 'xxx'
KISSINSIGHTS_ACCOUNT_NUMBER = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
MIXPANEL_TOKEN = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
OPTIMIZELY_ACCOUNT_NUMBER = 'xxxxxx'
Your analytics services are now installed. Take a look at the rest of
the documentation for more information about further configuration and
customization of the various services.

View file

@ -0,0 +1,31 @@
Chartbeat -- traffic analysis
=============================
Chartbeat_ provides real-time analytics to websites and blogs. It shows
visitors, load times, and referring sites on a minute-by-minute basis.
The service also provides alerts the second your website crashes or
slows to a crawl.
.. _Chartbeat: http://www.chartbeat.com/
The Chartbeat service adds code both to the top of the head section and
the bottom of the body section.
Required settings
-----------------
.. data:: CHARTBEAT_USER_ID
The User ID::
CHARTBEAT_USER_ID = '12345'
You can find the User ID by visiting the Chartbeat `Add New Site`_
page. The second code snippet contains a line that looks like this::
var _sf_async_config={uid:XXXXX,domain:"YYYYYYYYYY"};
Here, ``XXXXX`` is your User ID.
.. _`Add New Site`: http://chartbeat.com/code/

69
docs/settings.rst Normal file
View file

@ -0,0 +1,69 @@
========
Settings
========
Here's a full list of all available settings, in alphabetical order, and
their default values.
.. data:: ANALYTICAL_AUTO_IDENTIFY
Default: ``True``
Automatically identify logged in users by their username.
.. note::
The template tags can only access the visitor username if the
Django user is present in the template context either as the
``user`` variable, or as an attribute on the HTTP request in the
``request`` variable. Use a
:class:`~django.template.RequestContext` to render your
templates and add
``'django.contrib.auth.context_processors.auth'`` or
``'django.core.context_processors.request'`` to the list of
context processors in the :data:`TEMPLATE_CONTEXT_PROCESSORS`
setting. (The first of these is added by default.)
Alternatively, add one of the variables to the context yourself
when you render the template.
.. data:: ANALYTICAL_INTERNAL_IPS
Default: :data:`INTERNAL_IPS`
A list or tuple of internal IP addresses. Tracking code will be
commented out for visitors from any of these addresses.
Example::
ANALYTICAL_INTERNAL_IPS = ['192.168.1.45', '192.168.1.57']
.. note::
The template tags can only access the visitor IP address if the
HTTP request is present in the template context as the
``request`` variable. For this reason, the
:data:`ANALYTICAL_INTERNAL_IPS` settings only works if you add
this variable to the context yourself when you render the
template, or you use the ``RequestContext`` and add
``'django.core.context_processors.request'`` to the list of
context processors in the ``TEMPLATE_CONTEXT_PROCESSORS``
setting.
.. data:: ANALYTICAL_SERVICES
Default: all included services that have been configured correctly
A list or tuple of analytics services to use. If this setting is
used and one of the services is not configured correctly, an
:exc:`ImproperlyConfigured` exception is raised when the services
are first loaded.
Example::
ANALYTICAL_SERVICES = [
'analytical.services.crazy_egg.CrazyEggService',
'analytical.services.google_analytics.GoogleAnalyticsService',
]

View file

@ -1,4 +1,5 @@
from distutils.core import setup, Command
import os
cmdclass = {}
@ -33,6 +34,9 @@ class TestCommand(Command):
cmdclass['test'] = TestCommand
def read(fname):
return open(os.path.join(os.path.dirname(__file__), fname)).read()
import analytical
setup(
@ -40,7 +44,7 @@ setup(
version = analytical.__version__,
license = analytical.__license__,
description = 'Analytics services for Django projects',
long_description = analytical.__doc__,
long_description = read('README.rst'),
author = analytical.__author__,
author_email = analytical.__email__,
packages = [