Simplify analytics tag loading

The general-purpose tag loading code was much too generic and complex,
so this commit hard-codes the supported services.  Also adds a tutorial
to the documentation.

This commit is not tested.
This commit is contained in:
Joost Cassee 2011-01-28 18:01:09 +01:00
parent a7db456359
commit 3db3bf4b0e
18 changed files with 269 additions and 182 deletions

View file

@ -10,7 +10,7 @@ Django_ project. See the ``docs`` directory for more information.
__author__ = "Joost Cassee"
__email__ = "joost@cassee.net"
__version__ = "0.1.0"
__version__ = "0.2.0alpha"
__copyright__ = "Copyright (C) 2011 Joost Cassee"
__license__ = "MIT License"

View file

@ -1,5 +1,5 @@
"""
Analytical template tags.
Analytical template tags and filters.
"""
import logging
@ -10,34 +10,46 @@ from django.core.exceptions import ImproperlyConfigured
from django.template import Node, TemplateSyntaxError
from django.utils.importlib import import_module
DEFAULT_SERVICES = [
'analytical.templatetags.chartbeat.service',
'analytical.templatetags.clicky.service',
'analytical.templatetags.crazy_egg.service',
'analytical.templatetags.google_analytics.service',
'analytical.templatetags.hubspot.service',
'analytical.templatetags.kiss_insights.service',
'analytical.templatetags.kiss_metrics.service',
'analytical.templatetags.mixpanel.service',
'analytical.templatetags.optimizely.service',
]
LOCATIONS = ['head_top', 'head_bottom', 'body_top', 'body_bottom']
from analytical.templatetags import chartbeat, clicky, crazy_egg, \
google_analytics, hubspot, kiss_insights, kiss_metrics, mixpanel, \
optimizely
_log = logging.getLogger(__name__)
TAG_NODES = {
'head_top': [
chartbeat.ChartbeatTopNode, # Chartbeat should come first
kiss_metrics.KissMetricsNode,
optimizely.OptimizelyNode,
],
'head_bottom': [
google_analytics.GoogleAnalyticsNode,
mixpanel.MixpanelNode,
],
'body_top': [
kiss_insights.KissInsightsNode,
],
'body_bottom': [
clicky.ClickyNode,
crazy_egg.CrazyEggNode,
hubspot.HubSpotNode,
chartbeat.ChartbeatBottomNode, # Chartbeat should come last
],
}
logger = logging.getLogger(__name__)
register = template.Library()
def _location_tag(location):
def tag(parser, token):
def analytical_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
return analytical_tag
for loc in LOCATIONS:
for loc in TAG_NODES.keys():
register.tag('analytical_%s' % loc, _location_tag(loc))
@ -50,64 +62,16 @@ class AnalyticalNode(Node):
def _load_template_nodes():
try:
service_paths = settings.ANALYTICAL_SERVICES
autoload = False
except AttributeError:
service_paths = DEFAULT_SERVICES
autoload = True
services = _get_services(service_paths)
location_nodes = dict((loc, []) for loc in LOCATIONS)
for location in LOCATIONS:
node_tuples = []
for service in services:
node_tuple = service.get(location)
if node_tuple is not None:
if not isinstance(node_tuple, tuple):
node_tuple = (node_tuple, None)
node_tuples[location].append(node_tuple)
location_nodes[location] = _get_nodes(node_tuples, autoload)
location_nodes = {}
for location, node_classes in TAG_NODES.items():
location_nodes[location] = []
for node_class in node_classes:
try:
node = node_class()
except ImproperlyConfigured, e:
logger.debug("not loading analytical service '%s': %s",
node_class.name, e)
location_nodes.append(node)
return location_nodes
def _get_nodes(node_tuples, autoload):
nodes = []
node_sort_key = lambda n: {'first': -1, None: 0, 'last': 1}[n[1]]
for node_tuple in sorted(node_tuples, key=node_sort_key):
node_cls = node_tuple[0]
try:
nodes.append(node_cls())
except ImproperlyConfigured, e:
if autoload:
_log.debug("not loading analytical service '%s': %s",
node_cls.__module__, e)
continue
else:
raise
return nodes
def _get_services(paths, autoload):
services = []
for path in paths:
mod_name, attr_name = path.rsplit('.', 1)
try:
mod = import_module(mod_name)
except ImportError, e:
if autoload:
_log.exception(e)
continue
else:
raise
try:
service = getattr(mod, attr_name)
except AttributeError, e:
if autoload:
_log.debug("not loading analytical service '%s': "
"module '%s' does not provide attribute '%s'",
path, mod_name, attr_name)
continue
else:
raise
services.append(service)
return services
template_nodes = _load_template_nodes()

View file

@ -56,9 +56,11 @@ def chartbeat_top(parser, token):
return ChartbeatTopNode()
class ChartbeatTopNode(Node):
name = 'Chartbeat top code'
def render(self, context):
if is_internal_ip(context):
return disable_html(INIT_CODE, 'Chartbeat')
return disable_html(INIT_CODE, self.name)
return INIT_CODE
@ -77,6 +79,8 @@ def chartbeat_bottom(parser, token):
return ChartbeatBottomNode()
class ChartbeatBottomNode(Node):
name = 'Chartbeat bottom code'
def __init__(self):
self.user_id = self.get_required_setting(
'CHARTBEAT_USER_ID', USER_ID_RE,
@ -94,11 +98,5 @@ class ChartbeatBottomNode(Node):
config['domain'] = domain
html = SETUP_CODE % {'config': simplejson.dumps(config)}
if is_internal_ip(context):
html = disable_html(html, 'Chartbeat')
html = disable_html(html, self.name)
return html
service = {
'head_top': (ChartbeatTopNode, 'first'),
'body_bottom': (ChartbeatBottomNode, 'last'),
}

View file

@ -49,6 +49,8 @@ def clicky(parser, token):
return ClickyNode()
class ClickyNode(Node):
name = 'Clicky'
def __init__(self):
self.site_id = get_required_setting('CLICKY_SITE_ID', SITE_ID_RE,
"must be a string containing an eight-digit number")
@ -66,10 +68,5 @@ class ClickyNode(Node):
html = TRACKING_CODE % {'site_id': self.site_id,
'custom': simplejson.dumps(custom)}
if is_internal_ip(context):
html = disable_html(html, 'Clicky')
html = disable_html(html, self.name)
return html
service = {
'body_bottom': ClickyNode,
}

View file

@ -34,6 +34,8 @@ def crazy_egg(parser, token):
return CrazyEggNode()
class CrazyEggNode(Node):
name = 'Crazy Egg'
def __init__(self):
self.account_nr = self.get_required_setting('CRAZY_EGG_ACCOUNT_NUMBER',
ACCOUNT_NUMBER_RE,
@ -50,10 +52,5 @@ class CrazyEggNode(Node):
html = '%s\n<script type="text/javascript">%s</script>' \
% (html, js)
if is_internal_ip(context):
html = disable_html(html, 'Crazy Egg')
html = disable_html(html, self.name)
return html
service = {
'body_bottom': CrazyEggNode,
}

View file

@ -52,6 +52,8 @@ def google_analytics(parser, token):
return GoogleAnalyticsNode()
class GoogleAnalyticsNode(Node):
name = 'Google Analytics'
def __init__(self):
self.property_id = self.get_required_setting(
'GOOGLE_ANALYTICS_PROPERTY_ID', PROPERTY_ID_RE,
@ -62,7 +64,7 @@ class GoogleAnalyticsNode(Node):
html = SETUP_CODE % {'property_id': self.property_id,
'commands': " ".join(commands)}
if is_internal_ip(context):
html = disable_html(html, 'Google Analytics')
html = disable_html(html, self.name)
return html
def _get_custom_var_commands(self, context):
@ -79,8 +81,3 @@ class GoogleAnalyticsNode(Node):
scope = SCOPE_PAGE
commands.append(CUSTOM_VAR_CODE % locals())
return commands
service = {
'head_bottom': GoogleAnalyticsNode,
}

View file

@ -41,6 +41,8 @@ def hubspot(parser, token):
return HubSpotNode()
class HubSpotNode(Node):
name = 'HubSpot'
def __init__(self):
self.site_id = get_required_setting('HUPSPOT_PORTAL_ID',
PORTAL_ID_RE, "must be a (string containing a) number")
@ -51,10 +53,5 @@ class HubSpotNode(Node):
html = TRACKING_CODE % {'portal_id': self.portal_id,
'domain': self.domain}
if is_internal_ip(context):
html = disable_html(html, 'HubSpot')
html = disable_html(html, self.name)
return html
service = {
'body_bottom': HubSpotNode,
}

View file

@ -39,6 +39,8 @@ def kiss_insights(parser, token):
return KissInsightsNode()
class KissInsightsNode(Node):
name = 'KISSinsights'
def __init__(self):
self.account_number = self.get_required_setting(
'KISS_INSIGHTS_ACCOUNT_NUMBER', ACCOUNT_NUMBER_RE,
@ -59,8 +61,3 @@ class KissInsightsNode(Node):
html = SETUP_CODE % {'account_number': self.account_number,
'site_code': self.site_code, 'commands': " ".join(commands)}
return html
service = {
'body_top': KissInsightsNode,
}

View file

@ -53,6 +53,8 @@ def kiss_metrics(parser, token):
return KissMetricsNode()
class KissMetricsNode(Node):
name = 'KISSmetrics'
def __init__(self):
self.api_key = self.get_required_setting('KISS_METRICS_API_KEY',
API_KEY_RE,
@ -72,10 +74,5 @@ class KissMetricsNode(Node):
html = TRACKING_CODE % {'api_key': self.api_key,
'commands': " ".join(commands)}
if is_internal_ip(context):
html = disable_html(html, 'Mixpanel')
html = disable_html(html, self.name)
return html
service = {
'head_top': KissMetricsNode,
}

View file

@ -46,6 +46,8 @@ def mixpanel(parser, token):
return MixpanelNode()
class MixpanelNode(Node):
name = 'Mixpanel'
def __init__(self):
self.token = self.get_required_setting('MIXPANEL_TOKEN',
MIXPANEL_TOKEN_RE,
@ -65,10 +67,5 @@ class MixpanelNode(Node):
html = TRACKING_CODE % {'token': self.token,
'commands': " ".join(commands)}
if is_internal_ip(context):
html = disable_html(html, 'Mixpanel')
html = disable_html(html, self.name)
return html
service = {
'head_bottom': MixpanelNode,
}

View file

@ -33,6 +33,8 @@ def optimizely(parser, token):
return OptimizelyNode()
class OptimizelyNode(Node):
name = 'Optimizely'
def __init__(self):
self.account_number = self.get_required_setting(
'OPTIMIZELY_ACCOUNT_NUMBER', ACCOUNT_NUMBER_RE,
@ -41,10 +43,5 @@ class OptimizelyNode(Node):
def render(self, context):
html = SETUP_CODE % {'account_number': self.account_number}
if is_internal_ip(context):
html = disable_html(html, 'Optimizely')
html = disable_html(html, self.name)
return html
service = {
'head_top': OptimizelyNode,
}

View file

@ -1,17 +1,24 @@
===================
History and credits
===================
Changelog
---------
=========
0.2.0
-----
* Added support for the HubSpot service.
* Added template tags for individual services.
0.1.0
First project release.
-----
* First project release.
Credits
-------
=======
django-analytical was written by `Joost Cassee`_. The project source
code is hosted generously hosted by GitHub_.
code is 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

View file

@ -16,20 +16,18 @@ 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.
Of course, every service has its own specific installation instructions.
Furthermore, you need to include your unique identifiers, which then end
up in the templates. This application hides the details of the
different analytics services behind a generic interface, and keeps
personal information and configuration out of the templates. Its goal
is to make basic usage set-up very simple, while allowing advanced users
to customize tracking. Each service is set-up as recommended by the
services themselves, using an asynchronous version of the Javascript
code if possible.
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.
To get a feel of how django-analytics works, first check out the
:doc:`tutorial`.
Contents
@ -38,7 +36,27 @@ Contents
.. toctree::
:maxdepth: 2
tutorial
install
services/index
services
settings
history
Helping out
===========
If you want to help out with development of django-analytical, by
posting detailed bug reports, suggesting new features or other analytics
services to support, or doing some development work yourself, please use
the `GitHub project page`_:
* Use the `issue tracker`_ to discuss bugs and features.
* If you want to do the work yourself, great! Clone the repository, make
changes and send a pull request. Please create a new issue first so
that we can discuss it, and keep people from stepping on each others
toes.
* Of course, you can always send ideas and patches to joost@cassee.net.
.. _`GitHub project page`: http://github.com/jcassee/django-analytical
.. _`issue tracker`: http://github.com/jcassee/django-analytical/issues

View file

@ -4,8 +4,8 @@ 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
applications, adding the template tags to your base template, and adding
the identifiers for the services you use to the project settings.
applications, adding the template tags to your base template, and
configuring the services you use in the project settings.
#. `Installing the Python package`_
#. `Installing the Django application`_
@ -13,6 +13,8 @@ the identifiers for the services you use to the project settings.
#. `Configuring the application`_
.. _installing-the-package:
Installing the Python package
=============================
@ -31,16 +33,18 @@ get the development code::
.. _PyPI: http://pypi.python.org/pypi/django-analytical/
.. _GitHub: http://github.com/jcassee/django-analytical
Then install by running the setup script::
Then install the package by running the setup script::
$ cd django-analytical
$ python setup.py install
.. _installing-the-application:
Installing the Django application
=================================
After you install django-analytical, add the ``analytical`` Django
After you installed django-analytical, add the ``analytical`` Django
application to the list of installed applications in the ``settings.py``
file of your project::
@ -51,14 +55,16 @@ file of your project::
]
.. _adding-the-template-tags:
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::
Because every analytics service uses own specific Javascript code that
should be added to the top or bottom of either the head or body of the
HTML page, django-analytical provides four general-purpose template tags
that will render the code needed for the services you are using. Your
base template should look like this::
{% load analytical %}
<!DOCTYPE ... >
@ -79,6 +85,12 @@ Your base template should look like this::
</body>
</html>
Instead of using the general-purpose tags, you can also just use the
tags for the analytics service(s) you are using. See :ref:`services`
for documentation on using individual services.
.. _configuration:
Configuring the application
===========================

View file

@ -1,3 +1,6 @@
.. _services:
========
Services
========
@ -7,4 +10,4 @@ A number of analytics services is supported.
:maxdepth: 1
:glob:
*
services/*

View file

@ -99,11 +99,12 @@ be computed from the HTTP request, you can also set them in a context
processor that you add to the :data:`TEMPLATE_CONTEXT_PROCESSORS` list
in :file:`settings.py`::
def segment_on_ip_proto(request):
addr = request.META.get('HTTP_X_FORWARDED_FOR',
request.META.get('REMOTE_ADDR', ''))
proto = 'ipv6' if ':' in addr else 'ipv4'
return {'crazy_egg_var3': proto}
def track_admin_role(request):
if request.user.is_staff():
role = 'staff'
else:
role = 'visitor'
return {'crazy_egg_var3': role}
Just remember that if you set the same context variable in the
:class:`~django.template.context.RequestContext` constructor and in a

View file

@ -1,3 +1,5 @@
.. _identifying-visitors:
========
Settings
========
@ -6,6 +8,7 @@ Here's a full list of all available settings, in alphabetical order, and
their default values.
.. data:: ANALYTICAL_AUTO_IDENTIFY
Default: ``True``
@ -50,20 +53,3 @@ their default values.
``'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',
]

122
docs/tutorial.rst Normal file
View file

@ -0,0 +1,122 @@
.. _tutorial:
========
Tutorial
========
In this tutorial you will learn how to install and configure
django-analytical for basic tracking. Suppose you want to use two
different analytics services on your Django website:
* :doc:`Clicky <services/clicky>` for detailed traffic analysis
* :doc:`Crazy Egg <services/crazy_egg>` to see where visitors click on your pages
At the end of this tutorial, the project will track visitors on both
Clicky and Crazy Egg, identify authenticated users and add extra
tracking data to segment mouse clicks on Crazy Egg based on whether
visitors are using IPv4 or IPv6.
Installation
============
To get started with django-analytical, the package must first be
installed. You can download and install the latest stable package from
the Python Package Index automatically by using ``easy_install``::
$ easy_install django-analytical
For more ways to install django-analytical, see
:ref:`installing-the-package`.
After you install django-analytical, you need to add it to the list of
installed applications in the ``settings.py`` file of your project::
INSTALLED_APPS = [
...
'analytical',
...
]
Now add the general-purpose 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, you need to configure the Clicky Site ID and the Crazy Egg
account number. Add the following to your project :file:`settings.py`
file::
CLICKY_SITE_ID = 'xxxxxxxx'
CRAZY_EGG_ACCOUNT_NUMBER = 'xxxxxxxx'
The analytics services are now installed. If you run Django with these
changes, both Clicky and Crazy Egg will be tracking your visitors.
Identifying authenticated users
===============================
Some analytics services, such as Clicky, can identify and track
individual visitors. If django-analytical tags detect that the current
user is authenticated, they will automatically include code to send the
username to services that support this feature. This only works if the
template context has the current user in the ``user`` or
``request.user`` context variable. If you use a
:class:`~django.template.RequestContext` to render templates (which is
recommended anyway) and have the
:class:`django.contrib.auth.context_processors.auth` context processor
in the :data:`TEMPLATE_CONTEXT_PROCESSORS` setting (which is default),
then this identification works without having to make any changes.
For more detailed information on automatic identification, and how to
disable or override it, see :ref:`identifying-visitors`.
Adding custom tracking data
===========================
You want to track whether visitors are using IPv4 or IPv6. (Maybe you
are running a website on the IPv6 transition?) This means including
the visitor IP protocol version as custom data with the tracking code.
The easiest way to do this is by using a context processor::
def track_ip_proto(request):
addr = request.META.get('HTTP_X_FORWARDED_FOR', '')
if not addr:
addr = request.META.get('REMOTE_ADDR', '')
if ':' in addr:
proto = 'ipv6'
else:
proto = 'ipv4' # assume IPv4 if no information
return {'crazy_egg_var1': proto}
Use a :class:`~django.template.RequestContext` when rendering templates
and add the ``'track_ip_proto'`` to :data:`TEMPLATE_CONTEXT_PROCESSORS`.
In Crazy Egg, you can now select *User Var1* in the overlay or confetti
views to see whether visitors using IPv4 behave differently from those
using IPv6.
----
This concludes the tutorial. For information about setting up,
configuring and customizing the different analytics services, see
:ref:`services`.