Create project

This commit is contained in:
Joost Cassee 2011-01-21 02:01:40 +01:00
commit 1fa0d7760d
47 changed files with 1545 additions and 0 deletions

9
.gitignore vendored Normal file
View file

@ -0,0 +1,9 @@
/.*
!/.gitignore
/build
/dist
/MANIFEST
*.pyc
*.pyo

19
LICENSE.txt Normal file
View file

@ -0,0 +1,19 @@
Copyright (C) 2011 Joost Cassee
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.

1
MANIFEST.in Normal file
View file

@ -0,0 +1 @@
include LICENSE.txt

36
README.rst Normal file
View file

@ -0,0 +1,36 @@
django-analytical
-----------------
The django-analytical application integrates various analytics services
into a Django_ project. Currently supported services:
* `Clicky`_ -- traffic analysis
* `Crazy Egg`_ -- visual click tracking
* `Google Analytics`_ traffic analysis
* `KISSinsights`_ -- feedback surveys
* `KISSmetrics`_ -- funnel analysis
* `Mixpanel`_ -- event tracking
* `Optimizely`_ -- A/B testing
The documentation can be found in the ``docs`` directory or `read
online`_. The source code and issue tracker are `hosted on GitHub`_.
Copyright (C) 2011 Joost Cassee <joost@cassee.net>. This software is
licensed under the MIT License (see LICENSE.txt).
This application was inspired by and uses ideas from Analytical_,
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/
.. _Clicky: http://getclicky.com/
.. _`Crazy Egg`: http://www.crazyegg.com/
.. _`Google Analytics`: http://www.google.com/analytics/
.. _KISSinsights: http://www.kissinsights.com/
.. _KISSmetrics: http://www.kissmetrics.com/
.. _Mixpanel: http://www.mixpanel.com/
.. _Optimizely: http://www.optimizely.com/
.. _`read online`: http://packages.python.org/django-analytical/
.. _`hosted on GitHub`: http://www.github.com/jcassee/django-analytical
.. _Analytical: https://github.com/jkrall/analytical
.. _`Bateau Knowledge`: http://www.bateauknowledge.nl/

14
analytical/__init__.py Normal file
View file

@ -0,0 +1,14 @@
"""
========================================
Analytics service integration for Django
========================================
The django-clicky application integrates Clicky_ analytics into a
Django_ project.
"""
__author__ = "Joost Cassee"
__email__ = "joost@cassee.net"
__version__ = "0.1.0alpha"
__copyright__ = "Copyright (C) 2011 Joost Cassee"
__license__ = "MIT License"

0
analytical/models.py Normal file
View file

View file

@ -0,0 +1,62 @@
"""
Analytics services.
"""
import logging
from django.conf import settings
from django.utils.importlib import import_module
from django.core.exceptions import ImproperlyConfigured
_log = logging.getLogger(__name__)
DEFAULT_SERVICES = [
'analytical.services.clicky.ClickyService',
'analytical.services.crazyegg.CrazyEggService',
'analytical.services.google_analytics.GoogleAnalyticsService',
'analytical.services.kissinsights.KissInsightsService',
'analytical.services.kissmetrics.KissMetricsService',
'analytical.services.mixpanel.MixpanelService',
'analytical.services.optimizely.OptimizelyService',
]
enabled_services = None
def get_enabled_services(reload=False):
global enabled_services
if enabled_services is None or reload:
enabled_services = load_services()
return enabled_services
def load_services():
enabled_services = []
try:
service_paths = settings.ANALYTICAL_SERVICES
autoload = False
except AttributeError:
service_paths = DEFAULT_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))
enabled_services.append(service)
except ImproperlyConfigured, e:
if autoload:
_log.debug('not loading analytical service "%s": %s',
path, e)
else:
raise
return enabled_services

View file

@ -0,0 +1,41 @@
"""
Base analytical service.
"""
from django.core.exceptions import ImproperlyConfigured
from django.conf import settings
class AnalyticalService(object):
"""
Analytics service.
"""
def render(self, location, context):
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):
return ""
def render_head_bottom(self, context):
return ""
def render_body_top(self, context):
return ""
def render_body_bottom(self, context):
return ""
def get_required_setting(self, setting, value_re, invalid_msg):
try:
value = getattr(settings, setting)
except AttributeError:
raise ImproperlyConfigured("%s setting: not found" % setting)
value = str(value)
if not value_re.search(value):
raise ImproperlyConfigured("%s setting: %s" % (value, invalid_msg))
return value

View file

@ -0,0 +1,35 @@
"""
Clicky service.
"""
import re
from analytical.services.base import AnalyticalService
SITE_ID_RE = re.compile(r'^\d{8}$')
TRACKING_CODE = """
<script type="text/javascript">
var clicky = { log: function(){ return; }, goal: function(){ return; }};
var clicky_site_id = %(site_id)s;
(function() {
var s = document.createElement('script');
s.type = 'text/javascript';
s.async = true;
s.src = ( document.location.protocol == 'https:' ? 'https://static.getclicky.com/js' : 'http://static.getclicky.com/js' );
( document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0] ).appendChild( s );
})();
</script>
<noscript><p><img alt="Clicky" width="1" height="1" src="http://in.getclicky.com/%(site_id)sns.gif" /></p></noscript>
"""
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}

View file

@ -0,0 +1,30 @@
"""
Console debugging service.
"""
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');
}
</script>
"""
class ConsoleService(AnalyticalService):
KEY = 'console'
def render_head_top(self, context):
return DEBUG_CODE % {'location': 'head_top'}
def render_head_bottom(self, context):
return DEBUG_CODE % {'location': 'head_bottom'}
def render_body_top(self, context):
return DEBUG_CODE % {'location': 'body_top'}
def render_body_bottom(self, context):
return DEBUG_CODE % {'location': 'body_bottom'}

View file

@ -0,0 +1,24 @@
"""
Crazy Egg service.
"""
import re
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>"""
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],
'account_nr_2': self.account_nr[4:]}

View file

@ -0,0 +1,37 @@
"""
Google Analytics service.
"""
import re
from analytical.services.base import AnalyticalService
PROPERTY_ID_RE = re.compile(r'^UA-\d+-\d+$')
TRACKING_CODE = """
<script type="text/javascript">
var _gaq = _gaq || [];
_gaq.push(['_setAccount', '%(property_id)s']);
_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);
})();
</script>
"""
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}

View file

@ -0,0 +1,30 @@
"""
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

@ -0,0 +1,39 @@
"""
KISSmetrics service.
"""
import re
from analytical.services.base import AnalyticalService
API_KEY_RE = re.compile(r'^[0-9a-f]{40}$')
TRACKING_CODE = """
<script type="text/javascript">
var _kmq = _kmq || [];
function _kms(u){
setTimeout(function(){
var s = document.createElement('script');
s.type = 'text/javascript';
s.async = true;
s.src = u;
var f = document.getElementsByTagName('script')[0];
f.parentNode.insertBefore(s, f);
}, 1);
}
_kms('//i.kissmetrics.com/i.js');
_kms('//doug1izaerwt3.cloudfront.net/%(api_key)s.1.js');
</script>
"""
class KissMetricsService(AnalyticalService):
KEY = 'kissmetrics'
def __init__(self):
self.api_key = self.get_required_setting('KISSMETRICS_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}

View file

@ -0,0 +1,38 @@
"""
Mixpanel service.
"""
import re
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};
}
</script>
"""
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}

View file

@ -0,0 +1,23 @@
"""
Optimizely service.
"""
import re
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>"""
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}

View file

View file

@ -0,0 +1,58 @@
"""
Analytical template tags.
"""
from __future__ import absolute_import
from django import template
from django.conf import settings
from django.template import Node, TemplateSyntaxError
from analytical.services import get_enabled_services
DISABLE_CODE = "<!-- Analytical disabled on internal IP address\n%s\n-->"
register = template.Library()
def _location_tag(location):
def tag(parser, token):
bits = token.split_contents()
if len(bits) > 1:
raise TemplateSyntaxError("'%s' tag takes no arguments" % bits[0])
return AnalyticalNode(location)
return tag
for l in ['head_top', 'head_bottom', 'body_top', 'body_bottom']:
register.tag('analytical_%s' % l, _location_tag(l))
class AnalyticalNode(Node):
def __init__(self, location):
self.location = location
self.render_func_name = "render_%s" % self.location
self.internal_ips = getattr(settings, 'ANALYTICAL_INTERNAL_IPS',
getattr(settings, 'INTERNAL_IPS', ()))
def render(self, context):
html = "".join([self._render_service(service, context)
for service in get_enabled_services()])
if not html:
return ""
# if self._is_internal_ip(context):
# return DISABLE_CODE % html
return html
def _render_service(self, service, context):
func = getattr(service, self.render_func_name)
return func(context)
def _is_internal_ip(self, context):
try:
request = context['request']
remote_ip = request.META.get('HTTP_X_FORWARDED_FOR',
request.META.get('REMOTE_ADDR', ''))
return remote_ip in self.internal_ips
except KeyError, AttributeError:
return False

View file

@ -0,0 +1,8 @@
"""
Tests for django-analytical.
"""
from analytical.tests.test_services import *
from analytical.tests.test_template_tags import *
from analytical.tests.services import *

View file

@ -0,0 +1,12 @@
"""
Tests for the Analytical analytics services.
"""
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_mixpanel import *
from analytical.tests.services.test_optimizely import *

View file

@ -0,0 +1,44 @@
"""
Tests for the Clicky service.
"""
from django.core.exceptions import ImproperlyConfigured
from django.test import TestCase
from analytical.services.clicky import ClickyService
from analytical.tests.utils import TestSettingsManager
class ClickyTestCase(TestCase):
"""
Tests for the Clicky service.
"""
def setUp(self):
self.settings_manager = TestSettingsManager()
self.settings_manager.set(CLICKY_SITE_ID='12345678')
self.service = ClickyService()
def tearDown(self):
self.settings_manager.revert()
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({}), "")
def test_no_site_id(self):
self.settings_manager.delete('CLICKY_SITE_ID')
self.assertRaises(ImproperlyConfigured, ClickyService)
def test_wrong_id(self):
self.settings_manager.set(CLICKY_SITE_ID='1234567')
self.assertRaises(ImproperlyConfigured, ClickyService)
self.settings_manager.set(CLICKY_SITE_ID='123456789')
self.assertRaises(ImproperlyConfigured, ClickyService)
def test_rendering(self):
r = self.service.render_body_bottom({})
self.assertTrue('var clicky_site_id = 12345678;' in r, r)
self.assertTrue('src="http://in.getclicky.com/12345678ns.gif"' in r,
r)

View file

@ -0,0 +1,32 @@
"""
Tests for the console debugging service.
"""
from django.test import TestCase
from analytical.services.console import ConsoleService
class ConsoleTestCase(TestCase):
"""
Tests for the console debugging service.
"""
def setUp(self):
self.service = ConsoleService()
def test_render_head_top(self):
r = self.service.render_head_top({})
self.assertTrue('rendering analytical_head_top tag' 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)
def test_render_body_top(self):
r = self.service.render_body_top({})
self.assertTrue('rendering analytical_body_top tag' 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)

View file

@ -0,0 +1,42 @@
"""
Tests for the Crazy Egg service.
"""
from django.core.exceptions import ImproperlyConfigured
from django.test import TestCase
from analytical.services.crazy_egg import CrazyEggService
from analytical.tests.utils import TestSettingsManager
class CrazyEggTestCase(TestCase):
"""
Tests for the Crazy Egg service.
"""
def setUp(self):
self.settings_manager = TestSettingsManager()
self.settings_manager.set(CRAZY_EGG_ACCOUNT_NUMBER='12345678')
self.service = CrazyEggService()
def tearDown(self):
self.settings_manager.revert()
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({}), "")
def test_no_account_number(self):
self.settings_manager.delete('CRAZY_EGG_ACCOUNT_NUMBER')
self.assertRaises(ImproperlyConfigured, CrazyEggService)
def test_wrong_id(self):
self.settings_manager.set(CRAZY_EGG_ACCOUNT_NUMBER='1234567')
self.assertRaises(ImproperlyConfigured, CrazyEggService)
self.settings_manager.set(CRAZY_EGG_ACCOUNT_NUMBER='123456789')
self.assertRaises(ImproperlyConfigured, CrazyEggService)
def test_rendering(self):
r = self.service.render_body_bottom({})
self.assertTrue('/1234/5678.js' in r, r)

View file

@ -0,0 +1,40 @@
"""
Tests for the Google Analytics service.
"""
from django.core.exceptions import ImproperlyConfigured
from django.test import TestCase
from analytical.services.google_analytics import GoogleAnalyticsService
from analytical.tests.utils import TestSettingsManager
class GoogleAnalyticsTestCase(TestCase):
"""
Tests for the Google Analytics service.
"""
def setUp(self):
self.settings_manager = TestSettingsManager()
self.settings_manager.set(GOOGLE_ANALYTICS_PROPERTY_ID='UA-123456-7')
self.service = GoogleAnalyticsService()
def tearDown(self):
self.settings_manager.revert()
def test_empty_locations(self):
self.assertEqual(self.service.render_head_top({}), "")
self.assertEqual(self.service.render_body_top({}), "")
self.assertEqual(self.service.render_body_bottom({}), "")
def test_no_property_id(self):
self.settings_manager.delete('GOOGLE_ANALYTICS_PROPERTY_ID')
self.assertRaises(ImproperlyConfigured, GoogleAnalyticsService)
def test_wrong_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)

View file

@ -0,0 +1,53 @@
"""
Tests for the KISSinsights service.
"""
from django.core.exceptions import ImproperlyConfigured
from django.test import TestCase
from analytical.services.kissinsights import KissInsightsService
from analytical.tests.utils import TestSettingsManager
class KissInsightsTestCase(TestCase):
"""
Tests for the KISSinsights service.
"""
def setUp(self):
self.settings_manager = TestSettingsManager()
self.settings_manager.set(KISSINSIGHTS_ACCOUNT_NUMBER='12345')
self.settings_manager.set(KISSINSIGHTS_SITE_CODE='abc')
self.service = KissInsightsService()
def tearDown(self):
self.settings_manager.revert()
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_bottom({}), "")
def test_no_account_number(self):
self.settings_manager.delete('KISSINSIGHTS_ACCOUNT_NUMBER')
self.assertRaises(ImproperlyConfigured, KissInsightsService)
def test_no_site_code(self):
self.settings_manager.delete('KISSINSIGHTS_SITE_CODE')
self.assertRaises(ImproperlyConfigured, KissInsightsService)
def test_wrong_account_number(self):
self.settings_manager.set(KISSINSIGHTS_ACCOUNT_NUMBER='1234')
self.assertRaises(ImproperlyConfigured, KissInsightsService)
self.settings_manager.set(KISSINSIGHTS_ACCOUNT_NUMBER='123456')
self.assertRaises(ImproperlyConfigured, KissInsightsService)
def test_wrong_site_id(self):
self.settings_manager.set(KISSINSIGHTS_SITE_CODE='ab')
self.assertRaises(ImproperlyConfigured, KissInsightsService)
self.settings_manager.set(KISSINSIGHTS_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)

View file

@ -0,0 +1,46 @@
"""
Tests for the KISSmetrics service.
"""
from django.core.exceptions import ImproperlyConfigured
from django.test import TestCase
from analytical.services.kissmetrics import KissMetricsService
from analytical.tests.utils import TestSettingsManager
class KissMetricsTestCase(TestCase):
"""
Tests for the KISSmetrics service.
"""
def setUp(self):
self.settings_manager = TestSettingsManager()
self.settings_manager.set(KISSMETRICS_API_KEY='0123456789abcdef0123456'
'789abcdef01234567')
self.service = KissMetricsService()
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({}), "")
self.assertEqual(self.service.render_body_bottom({}), "")
def test_no_api_key(self):
self.settings_manager.delete('KISSMETRICS_API_KEY')
self.assertRaises(ImproperlyConfigured, KissMetricsService)
def test_wrong_api_key(self):
self.settings_manager.set(KISSMETRICS_API_KEY='0123456789abcdef0123456'
'789abcdef0123456')
self.assertRaises(ImproperlyConfigured, KissMetricsService)
self.settings_manager.set(KISSMETRICS_API_KEY='0123456789abcdef0123456'
'789abcdef012345678')
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)

View file

@ -0,0 +1,46 @@
"""
Tests for the Mixpanel service.
"""
from django.core.exceptions import ImproperlyConfigured
from django.test import TestCase
from analytical.services.mixpanel import MixpanelService
from analytical.tests.utils import TestSettingsManager
class MixpanelTestCase(TestCase):
"""
Tests for the Mixpanel service.
"""
def setUp(self):
self.settings_manager = TestSettingsManager()
self.settings_manager.set(
MIXPANEL_TOKEN='0123456789abcdef0123456789abcdef')
self.service = MixpanelService()
def tearDown(self):
self.settings_manager.revert()
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({}), "")
def test_no_token(self):
self.settings_manager.delete('MIXPANEL_TOKEN')
self.assertRaises(ImproperlyConfigured, MixpanelService)
def test_wrong_token(self):
self.settings_manager.set(
MIXPANEL_TOKEN='0123456789abcdef0123456789abcde')
self.assertRaises(ImproperlyConfigured, MixpanelService)
self.settings_manager.set(
MIXPANEL_TOKEN='0123456789abcdef0123456789abcdef0')
self.assertRaises(ImproperlyConfigured, MixpanelService)
def test_rendering(self):
r = self.service.render_body_bottom({})
self.assertTrue("MixpanelLib('0123456789abcdef0123456789abcdef')" in r,
r)

View file

@ -0,0 +1,42 @@
"""
Tests for the Optimizely service.
"""
from django.core.exceptions import ImproperlyConfigured
from django.test import TestCase
from analytical.services.optimizely import OptimizelyService
from analytical.tests.utils import TestSettingsManager
class OptimizelyTestCase(TestCase):
"""
Tests for the Optimizely service.
"""
def setUp(self):
self.settings_manager = TestSettingsManager()
self.settings_manager.set(OPTIMIZELY_ACCOUNT_NUMBER='1234567')
self.service = OptimizelyService()
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({}), "")
self.assertEqual(self.service.render_body_bottom({}), "")
def test_no_account_number(self):
self.settings_manager.delete('OPTIMIZELY_ACCOUNT_NUMBER')
self.assertRaises(ImproperlyConfigured, OptimizelyService)
def test_wrong_account_number(self):
self.settings_manager.set(OPTIMIZELY_ACCOUNT_NUMBER='123456')
self.assertRaises(ImproperlyConfigured, OptimizelyService)
self.settings_manager.set(OPTIMIZELY_ACCOUNT_NUMBER='12345678')
self.assertRaises(ImproperlyConfigured, OptimizelyService)
def test_rendering(self):
self.assertEqual(self.service.render_head_top({}),
'<script src="//cdn.optimizely.com/js/1234567.js"></script>')

View file

@ -0,0 +1,14 @@
"""
django-analytical testing settings.
"""
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': ':memory:',
}
}
INSTALLED_APPS = [
'analytical',
]

View file

@ -0,0 +1,89 @@
"""
Tests for the services package.
"""
from django.core.exceptions import ImproperlyConfigured
from django.test import TestCase
from analytical import services
from analytical.services import load_services
from analytical.tests.utils import TestSettingsManager
from analytical.services.google_analytics import GoogleAnalyticsService
class GetEnabledServicesTestCase(TestCase):
"""
Tests for get_enabled_services.
"""
def setUp(self):
services.enabled_services = None
services.load_services = self._dummy_load_services_1
def tearDown(self):
services.load_services = load_services
def test_no_reload(self):
result = services.get_enabled_services()
self.assertEqual(result, 'test1')
services.load_services = self._dummy_load_services_2
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'
class LoadServicesTestCase(TestCase):
"""
Tests for load_services.
"""
def setUp(self):
self.settings_manager = TestSettingsManager()
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')
def tearDown(self):
self.settings_manager.revert()
def test_no_services(self):
self.assertEqual(load_services(), [])
def test_enabled_service(self):
self.settings_manager.set(GOOGLE_ANALYTICS_PROPERTY_ID='UA-1234567-8')
results = load_services()
self.assertEqual(len(results), 1, results)
self.assertTrue(isinstance(results[0], GoogleAnalyticsService),
results)
def test_explicit_service(self):
self.settings_manager.set(ANALYTICAL_SERVICES=[
'analytical.services.google_analytics.GoogleAnalyticsService'])
self.settings_manager.set(GOOGLE_ANALYTICS_PROPERTY_ID='UA-1234567-8')
results = load_services()
self.assertEqual(len(results), 1, results)
self.assertTrue(isinstance(results[0], GoogleAnalyticsService),
results)
def test_explicit_service_misconfigured(self):
self.settings_manager.set(ANALYTICAL_SERVICES=[
'analytical.services.google_analytics.GoogleAnalyticsService'])
self.assertRaises(ImproperlyConfigured, load_services)

View file

@ -0,0 +1,19 @@
"""
Tests for the template tags.
"""
from django.test import TestCase
from analytical.tests.utils import TestSettingsManager
class TemplateTagsTestCase(TestCase):
"""
Tests for the template tags.
"""
def setUp(self):
self.settings_manager = TestSettingsManager()
def tearDown(self):
self.settings_manager.revert()

64
analytical/tests/utils.py Normal file
View file

@ -0,0 +1,64 @@
"""
Testing utilities.
"""
from django.conf import settings
from django.core.management import call_command
from django.db.models import loading
from django.test.simple import run_tests as django_run_tests
def run_tests():
"""
Use the Django test runner to run the tests.
"""
django_run_tests([], verbosity=1, interactive=True)
class TestSettingsManager(object):
"""
From: http://www.djangosnippets.org/snippets/1011/
A class which can modify some Django settings temporarily for a
test and then revert them to their original values later.
Automatically handles resyncing the DB if INSTALLED_APPS is
modified.
"""
NO_SETTING = ('!', None)
def __init__(self):
self._original_settings = {}
def set(self, **kwargs):
for k, v in kwargs.iteritems():
self._original_settings.setdefault(k, getattr(settings, k,
self.NO_SETTING))
setattr(settings, k, v)
if 'INSTALLED_APPS' in kwargs:
self.syncdb()
def delete(self, *args):
for k in args:
try:
self._original_settings.setdefault(k, getattr(settings, k,
self.NO_SETTING))
delattr(settings, k)
except AttributeError:
pass # setting did not exist
def syncdb(self):
loading.cache.loaded = False
call_command('syncdb', verbosity=0, interactive=False)
def revert(self):
for k,v in self._original_settings.iteritems():
if v == self.NO_SETTING:
if hasattr(settings, k):
delattr(settings, k)
else:
setattr(settings, k, v)
if 'INSTALLED_APPS' in self._original_settings:
self.syncdb()
self._original_settings = {}

44
docs/conf.py Normal file
View file

@ -0,0 +1,44 @@
# -*- coding: utf-8 -*-
#
# This file is execfile()d with the current directory set to its containing
# directory.
import sys, os
sys.path.append(os.path.dirname(os.path.abspath('.')))
import analytical
# -- General configuration -----------------------------------------------------
project = u'django-analytical'
copyright = u'2011, Joost Cassee <joost@cassee.net>'
release = analytical.__version__
# The short X.Y version.
version = release.rsplit('.', 1)[0]
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx']
templates_path = ['.templates']
source_suffix = '.rst'
master_doc = 'index'
add_function_parentheses = True
pygments_style = 'sphinx'
intersphinx_mapping = {'http://docs.python.org/': None}
# -- Options for HTML output ---------------------------------------------------
html_theme = 'default'
html_static_path = ['.static']
htmlhelp_basename = 'analyticaldoc'
# -- Options for LaTeX output --------------------------------------------------
latex_documents = [
('index', 'django-analytical.tex', u'Documentation for django-analytical',
u'Joost Cassee', 'manual'),
]

23
docs/history.rst Normal file
View file

@ -0,0 +1,23 @@
History and credits
===================
Changelog
---------
0.1.0
First project release.
Credits
-------
django-analytical was written by `Joost Cassee`_. The project source
code is hosted generously hosted by GitHub_.
This application was inspired by and uses ideas from Analytical_,
Joshua Krall's all-purpose analytics front-end for Rails. The work on
Crazy Egg was made possible by `Bateau Knowledge`_.
.. _`Joost Cassee`: mailto:joost@cassee.net
.. _GitHub: http://github.com/
.. _Analytical: https://github.com/jkrall/analytical
.. _`Bateau Knowledge`: http://www.bateauknowledge.nl/

44
docs/index.rst Normal file
View file

@ -0,0 +1,44 @@
========================================
Analytics service integration for Django
========================================
The django-analytical application integrates various analytics services
into a Django_ project.
.. _Django: http://www.djangoproject.com/
:Download: http://pypi.python.org/pypi/django-analytical/
:Source: http://github.com/jcassee/django-analytical
Overview
========
If your want to integrating an analytics service into a Django project,
you need to add Javascript tracking code to the project templates.
Unfortunately, every services has its own specific installation
instructions. Furthermore, you need to specify your unique identifiers
which would end up in templates. This application hides the details of
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)
Contents
========
.. toctree::
:maxdepth: 2
quick
install
services/index
history

85
docs/install.rst Normal file
View file

@ -0,0 +1,85 @@
Installation and global configuration
=====================================
Integration of your analytics service is very simple. There are four
steps: installing the package, adding it to the list of installed Django
applications, adding the template tags to your base template, and adding
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`_
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
``easy_install``::
$ easy_install django-analytical
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::
$ git clone https://github.com/jcassee/django-analytical.git
.. _PyPI: http://pypi.python.org/pypi/django-analytical/
.. _GitHub: http://github.com/jcassee/django-analytical
Then install by running the setup script::
$ cd django-analytical
$ python setup.py install
Installing the Django application
---------------------------------
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',
...
]
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
of every HTML page, the django-analytical provides four general-purpose
tags that will render the code needed for the services you are using.
Your base template should look like this::
{% load analytical %}
<!DOCTYPE ... >
<html>
<head>
{% analytical_head_top %}
...
{% analytical_head_bottom %}
</head>
<body>
{% analytical_body_top %}
...
{% analytical_body_bottom %}
</body>
</html>
Configuring global settings
---------------------------
The next step is to :doc:`configure the services <services/index>`.

57
docs/quick.rst Normal file
View file

@ -0,0 +1,57 @@
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.

22
docs/services/clicky.rst Normal file
View file

@ -0,0 +1,22 @@
Clicky -- traffic analysis
==========================
Clicky_ is an online web analytics tool. It is similar to Google
Analytics in that it provides statistics on who is visiting your website
and what they are doing. Clicky provides its data in real time and is
designed to be very easy to use.
.. _Clicky: http://getclicky.com/
Required settings
-----------------
.. data:: CLICKY_SITE_ID
The Clicky site identifier, or Site ID::
CLICKY_SITE_ID = '12345678'
You can find the Site ID in the Info tab of the website Preferences
page on your Clicky account.

View file

@ -0,0 +1,21 @@
Crazy Egg -- visual click tracking
==================================
`Crazy Egg`_ is an easy to use hosted web application that visualizes
website clicks using heatmaps. It allows you to discover the areas of
web pages that are most important to your visitors.
.. _`Crazy Egg`: http://www.crazyegg.com/
Required settings
-----------------
.. data:: CRAZY_EGG_ACCOUNT_NUMBER
Your Crazy Egg account number::
CRAZY_EGG_ACCOUNT_NUMBER = '12345678'
You can find the account number by clicking the link named "What's my
code?" in the dashboard of your Crazy Egg account.

View file

@ -0,0 +1,21 @@
Google Analytics -- traffic analysis
====================================
`Google Analytics`_ is the well-known web analytics service from
Google. The product is aimed more at marketers than webmasters or
technologists, supporting integration with AdWords and other e-commence
features.
.. _`Google Analytics`: http://www.google.com/analytics/
Required settings
-----------------
.. data:: GOOGLE_ANALYTICS_PROPERTY_ID
The Google Analytics web property ID::
GOOGLE_ANALYTICS_PROPERTY_ID = 'UA-123456-1'
You can find the web property ID on the overview page of your account.

10
docs/services/index.rst Normal file
View file

@ -0,0 +1,10 @@
Services
========
A number of analytics services is supported.
.. toctree::
:maxdepth: 1
:glob:
*

View file

@ -0,0 +1,30 @@
KISSinsights -- feedback surveys
================================
KISSinsights_ provides unobtrusive surveys that pop up from the bottom
right-hand corner of your website. Asking specific questions gets you
the targeted, actionable feedback you need to make your site better.
.. _KISSinsights: http://www.kissinsights.com/
Required settings
-----------------
.. data:: KISSINSIGHTS_ACCOUNT_NUMBER
The KISSinsights account number::
KISSINSIGHTS_ACCOUNT_NUMBER = '12345'
.. data:: KISSINSIGHTS_SITE_CODE
The KISSinsights website code::
KISSINSIGHTS_SITE_CODE = 'abc'
You can find the account number and website code by visiting the code
installation page of the website you want to place the surveys on. You
will see some HTML code with a Javascript tag with a ``src`` attribute
containing ``//s3.amazonaws.com/ki.js/XXXXX/YYY.js``. Here ``XXXXX`` is
the account number and ``YYY`` the website code.

View file

@ -0,0 +1,22 @@
KISSmetrics -- funnel analysis
==============================
KISSmetrics_ is an easy to implement analytics solution that provides a
powerful visual representation of your customer lifecycle. Discover how
many visitors go from your landing page to pricing to sign up, and how
many drop out at each stage.
.. _KISSmetrics: http://www.kissmetrics.com/
Required settings
-----------------
.. data:: KISSMETRICS_API_KEY
The website API key::
KISSMETRICS_API_KEY = '1234567890abcdef1234567890abcdef12345678'
You can find the website API key by visiting the website `Product
center` on your KISSmetrics dashboard.

View file

@ -0,0 +1,20 @@
Mixpanel -- event tracking
==========================
Mixpanel_ tracks events and actions to see what features users are using
the most and how they are trending. You could use it for real-time
analysis of visitor retention or funnels.
.. _Mixpanel: http://www.mixpanel.com/
Required settings
-----------------
.. data:: MIXPANEL_TOKEN
The website project token ::
MIXPANEL_TOKEN = '1234567890abcdef1234567890abcdef'
You can find the project token on the Mixpanel `projects` page.

View file

@ -0,0 +1,25 @@
Optimizely -- A/B testing
=========================
Optimizely_ is an easy way to implement A/B testing. Try different
decisions, images, layouts, and copy without touching your website code
and see exactly how your experiments are affecting pagevieuws,
retention and sales.
.. _Optimizely: http://www.optimizely.com/
Required settings
-----------------
.. data:: OPTIMIZELY_ACCOUNT_NUMBER
The website project token ::
OPTIMIZELY_ACCOUNT_NUMBER = '1234567'
You can find your account number by clicking the `Implementation` link
in the top right-hand corner of the Optimizely website. A pop-up
window will appear containing HTML code looking like this:
``<script src="//cdn.optimizely.com/js/XXXXXXX.js"></script>``.
The number ``XXXXXXX`` is your account number.

7
setup.cfg Normal file
View file

@ -0,0 +1,7 @@
[build_sphinx]
source-dir = docs
build-dir = build/docs
all_files = 1
[upload_sphinx]
upload-dir = build/docs/html

67
setup.py Normal file
View file

@ -0,0 +1,67 @@
from distutils.core import setup, Command
cmdclass = {}
try:
from sphinx.setup_command import BuildDoc
cmdclass['build_sphinx'] = BuildDoc
except ImportError:
pass
try:
from sphinx_pypi_upload import UploadDoc
cmdclass['upload_sphinx'] = UploadDoc
except ImportError:
pass
class TestCommand(Command):
user_options = []
def initialize_options(self):
pass
def finalize_options(self):
pass
def run(self):
import os
os.environ['DJANGO_SETTINGS_MODULE'] = 'analytical.tests.settings'
from analytical.tests.utils import run_tests
run_tests()
cmdclass['test'] = TestCommand
import analytical
setup(
name = 'django-analytical',
version = analytical.__version__,
license = analytical.__license__,
description = 'Analytics services for Django projects',
long_description = analytical.__doc__,
author = analytical.__author__,
author_email = analytical.__email__,
packages = [
'analytical',
'analytical.templatetags',
'analytical.tests',
],
keywords = ['django', 'analytics]'],
classifiers = [
'Development Status :: 5 - Production/Stable',
'Environment :: Web Environment',
'Framework :: Django',
'Intended Audience :: Developers',
'License :: OSI Approved :: MIT License',
'Operating System :: OS Independent',
'Programming Language :: Python',
'Topic :: Internet :: WWW/HTTP',
'Topic :: Software Development :: Libraries :: Python Modules',
],
platforms = ['any'],
url = 'http://github.com/jcassee/django-analytical',
download_url = 'http://github.com/jcassee/django-analytical/archives/master',
cmdclass = cmdclass,
)