Add support for the Woopra service

Also bump the version to 0.7.0.
This commit is contained in:
Joost Cassee 2011-03-15 16:45:14 +01:00
parent e4a53def30
commit babfad16ac
10 changed files with 388 additions and 8 deletions

View file

@ -1,9 +1,10 @@
Version 0.7.0
-------------
* Fixed Crazy Egg template ``<script>`` tag.
* Added support for the Woopra service.
* Added chat window text customization to Olark.
* Renamed ``MIXPANEL_TOKEN`` setting to ``MIXPANEL_API_TOKEN`` for
compatibility with Wes Winham's mixpanel-celery_ package.
* Fixed the ``<script>`` tag for Crazy Egg.
.. _mixpanel-celery: https://github.com/winhamwr/mixpanel-celery

View file

@ -31,6 +31,7 @@ Currently supported services:
* `Optimizely`_ A/B testing
* `Performable`_ web analytics and landing pages
* `Reinvigorate`_ visitor tracking
* `Woopra`_ web analytics
The documentation can be found in the ``docs`` directory or `read
online`_. The source code and issue tracker are generously `hosted by
@ -56,6 +57,7 @@ an issue to discuss your plans.
.. _Optimizely: http://www.optimizely.com/
.. _Performable: http://www.performable.com/
.. _Reinvigorate: http://www.reinvigorate.com/
.. _Woopra: http://www.woopra.com/
.. _`read online`: http://packages.python.org/django-analytical/
.. _`hosted by GitHub`: http://github.com/jcassee/django-analytical
.. _`issue tracker`: http://github.com/jcassee/django-analytical/issues

View file

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

View file

@ -27,6 +27,7 @@ TAG_MODULES = [
'analytical.optimizely',
'analytical.performable',
'analytical.reinvigorate',
'analytical.woopra',
]

View file

@ -0,0 +1,99 @@
"""
Clicky template tags and filters.
"""
from __future__ import absolute_import
import re
from django.conf import settings
from django.template import Library, Node, TemplateSyntaxError
from django.utils import simplejson
from analytical.utils import get_identity, get_user_from_context, \
is_internal_ip, disable_html, get_required_setting
DOMAIN_RE = re.compile(r'^\S+$')
TRACKING_CODE = """
<script type="text/javascript">
var woo_settings = %(settings)s;
var woo_visitor = %(visitor)s;
(function(){
var wsc=document.createElement('script');
wsc.type='text/javascript';
wsc.src=document.location.protocol+'//static.woopra.com/js/woopra.js';
wsc.async=true;
var ssc = document.getElementsByTagName('script')[0];
ssc.parentNode.insertBefore(wsc, ssc);
})();
</script>
"""
register = Library()
@register.tag
def woopra(parser, token):
"""
Woopra tracking template tag.
Renders Javascript code to track page visits. You must supply
your Woopra domain in the ``WOOPRA_DOMAIN`` setting.
"""
bits = token.split_contents()
if len(bits) > 1:
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
return WoopraNode()
class WoopraNode(Node):
def __init__(self):
self.domain = get_required_setting('WOOPRA_DOMAIN', DOMAIN_RE,
"must be a domain name")
def render(self, context):
settings = self._get_settings(context)
visitor = self._get_visitor(context)
html = TRACKING_CODE % {
'settings': simplejson.dumps(settings),
'visitor': simplejson.dumps(visitor),
}
if is_internal_ip(context, 'WOOPRA'):
html = disable_html(html, 'Woopra')
return html
def _get_settings(self, context):
vars = {'domain': self.domain}
try:
vars['idle_timeout'] = str(settings.WOOPRA_IDLE_TIMEOUT)
except AttributeError:
pass
return vars
def _get_visitor(self, context):
vars = {}
for dict_ in context:
for var, val in dict_.items():
if var.startswith('woopra_'):
vars[var[7:]] = val
if 'name' not in vars and 'email' not in vars:
user = get_user_from_context(context)
if user is not None:
vars['name'] = get_identity(context, 'woopra',
self._identify, user)
if user.email:
vars['email'] = user.email
return vars
def _identify(self, user):
name = user.get_full_name()
if not name:
name = user.username
return name
def contribute_to_analytical(add_node):
WoopraNode() # ensure properly configured
add_node('head_bottom', WoopraNode)

View file

@ -15,4 +15,5 @@ from analytical.tests.test_tag_olark import *
from analytical.tests.test_tag_optimizely import *
from analytical.tests.test_tag_performable import *
from analytical.tests.test_tag_reinvigorate import *
from analytical.tests.test_tag_woopra import *
from analytical.tests.test_utils import *

View file

@ -0,0 +1,84 @@
"""
Tests for the Woopra template tags and filters.
"""
from django.contrib.auth.models import User
from django.http import HttpRequest
from django.template import Context
from analytical.templatetags.woopra import WoopraNode
from analytical.tests.utils import TagTestCase
from analytical.utils import AnalyticalException
class WoopraTagTestCase(TagTestCase):
"""
Tests for the ``woopra`` template tag.
"""
def setUp(self):
super(WoopraTagTestCase, self).setUp()
self.settings_manager.set(WOOPRA_DOMAIN='example.com')
def test_tag(self):
r = self.render_tag('woopra', 'woopra')
self.assertTrue('var woo_settings = {"domain": "example.com"};' in r, r)
def test_node(self):
r = WoopraNode().render(Context({}))
self.assertTrue('var woo_settings = {"domain": "example.com"};' in r, r)
def test_no_domain(self):
self.settings_manager.set(WOOPRA_DOMAIN='this is not a domain')
self.assertRaises(AnalyticalException, WoopraNode)
def test_wrong_domain(self):
self.settings_manager.delete('WOOPRA_DOMAIN')
self.assertRaises(AnalyticalException, WoopraNode)
def test_idle_timeout(self):
self.settings_manager.set(WOOPRA_IDLE_TIMEOUT=1234)
r = WoopraNode().render(Context({}))
self.assertTrue('var woo_settings = {"domain": "example.com", '
'"idle_timeout": "1234"};' in r, r)
def test_custom(self):
r = WoopraNode().render(Context({'woopra_var1': 'val1',
'woopra_var2': 'val2'}))
self.assertTrue('var woo_visitor = {"var1": "val1", "var2": "val2"};'
in r, r)
def test_identify_name_and_email(self):
self.settings_manager.set(ANALYTICAL_AUTO_IDENTIFY=True)
r = WoopraNode().render(Context({'user': User(username='test',
first_name='Firstname', last_name='Lastname',
email="test@example.com")}))
self.assertTrue('var woo_visitor = {"name": "Firstname Lastname", '
'"email": "test@example.com"};' in r, r)
def test_identify_username_no_email(self):
self.settings_manager.set(ANALYTICAL_AUTO_IDENTIFY=True)
r = WoopraNode().render(Context({'user': User(username='test')}))
self.assertTrue('var woo_visitor = {"name": "test"};' in r, r)
def test_no_identify_when_explicit_name(self):
self.settings_manager.set(ANALYTICAL_AUTO_IDENTIFY=True)
r = WoopraNode().render(Context({'woopra_name': 'explicit',
'user': User(username='implicit')}))
self.assertTrue('var woo_visitor = {"name": "explicit"};' in r, r)
def test_no_identify_when_explicit_email(self):
self.settings_manager.set(ANALYTICAL_AUTO_IDENTIFY=True)
r = WoopraNode().render(Context({'woopra_email': 'explicit',
'user': User(username='implicit')}))
self.assertTrue('var woo_visitor = {"email": "explicit"};' 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'
context = Context({'request': req})
r = WoopraNode().render(context)
self.assertTrue(r.startswith(
'<!-- Woopra disabled on internal IP address'), r)
self.assertTrue(r.endswith('-->'), r)

View file

@ -27,7 +27,26 @@ def get_required_setting(setting, value_re, invalid_msg):
return value
def get_identity(context, prefix=None, identity_func=None):
def get_user_from_context(context):
"""
Get the user instance from the template context, if possible.
If the context does not contain a `request` or `user` attribute,
`None` is returned.
"""
try:
return context['user']
except KeyError:
pass
try:
request = context['request']
return request.user
except (KeyError, AttributeError):
pass
return None
def get_identity(context, prefix=None, identity_func=None, user=None):
"""
Get the identity of a logged in user from a template context.
@ -47,11 +66,8 @@ def get_identity(context, prefix=None, identity_func=None):
pass
if getattr(settings, 'ANALYTICAL_AUTO_IDENTIFY', True):
try:
try:
user = context['user']
except KeyError:
request = context['request']
user = request.user
if user is None:
user = get_user_from_context(context)
if user.is_authenticated():
if identity_func is not None:
return identity_func(user)

View file

@ -149,6 +149,10 @@ settings required to enable each service are listed here:
REINVIGORATE_TRACKING_ID = '12345-abcdefghij'
* :doc:`Woopra <services/woopra>`::
WOOPRA_DOMAIN = 'abcde.com'
----

172
docs/services/woopra.rst Normal file
View file

@ -0,0 +1,172 @@
===========================
Woopra -- website analytics
===========================
Woopra_ generates live detailed statistics about the visitors to your
website. You can watch your visitors navigate live and interact with
them via chat. The service features notifications, campaigns, funnels
and more.
.. _Woopra: http://www.woopra.com/
Installation
============
To start using the Woopra integration, you must have installed the
django-analytical package and have added the ``analytical`` application
to :const:`INSTALLED_APPS` in your project :file:`settings.py` file.
See :doc:`../install` for details.
Next you need to add the Woopra template tag to your templates. This
step is only needed if you are not using the generic
:ttag:`analytical.*` tags. If you are, skip to
:ref:`woopra-configuration`.
The Woopra tracking code is inserted into templates using a template
tag. Load the :mod:`woopra` template tag library and insert the
:ttag:`woopra` tag. Because every page that you want to track must
have the tag, it is useful to add it to your base template. Insert
the tag at the bottom of the HTML head::
{% load woopra %}
<html>
<head>
...
{% woopra %}
</head>
...
Because Javascript code is asynchronous, putting the tag in the head
section increases the chances that a page view is going to be tracked
before the visitor leaves the page. See for details the `Asynchronous
JavaScript Developers Guide`_ on the Woopra website.
.. _`Asynchronous JavaScript Developers Guide`: http://www.woopra.com/docs/async/
.. _woopra-configuration:
Configuration
=============
Before you can use the Woopra integration, you must first set the
website domain. You can also customize the data that Woopra tracks and
identify authenticated users.
Setting the domain
------------------
A Woopra account is tied to a website domain. Set
:const:`WOOPRA_DOMAIN` in the project :file:`settings.py` file::
WOOPRA_DOMAIN = 'XXXXXXXX.XXX'
If you do not set a domain, the tracking code will not be rendered.
(In theory, the django-analytical application could get the website
domain from the current ``Site`` or the ``request`` object, but this
setting also works as a sign that the Woopra integration should be
enabled for the :ttag:`analytical.*` template tags.)
Visitor timeout
---------------
The default Woopra visitor timeout -- the time after which Woopra
ignores inactive visitors on a website -- is set at 4 minutes. This
means that if a user opens your Web page and then leaves it open in
another browser window, Woopra will report that the visitor has gone
away after 4 minutes of inactivity on that page (no page scrolling,
clicking or other action).
If you would like to increase or decrease the idle timeout setting you
can set :const:`WOOPRA_IDLE_TIMEOUT` to a time in milliseconds. For
example, to set the default timout to 10 minutes::
WOOPRA_IDLE_TIMEOUT = 10 * 60 * 1000
Keep in mind that increasing this number will not only show you more
visitors on your site at a time, but will also skew your average time on
a page reporting. So its important to keep the number reasonable in
order to accurately make predictions.
Internal IP addresses
---------------------
Usually you do not want to track clicks from your development or
internal IP addresses. By default, if the tags detect that the client
comes from any address in the :const:`WOOPRA_INTERNAL_IPS` setting,
the tracking code is commented out. It takes the value of
:const:`ANALYTICAL_INTERNAL_IPS` by default (which in turn is
:const:`INTERNAL_IPS` by default). See :ref:`identifying-visitors` for
important information about detecting the visitor IP address.
Custom data
-----------
As described in the Woopra documentation on `custom visitor data`_,
the data that is tracked by Woopra can be customized. Using template
context variables, you can let the :ttag:`woopra` tag pass custom data
to Woopra automatically. You can set the context variables in your view
when your render a template containing the tracking code::
context = RequestContext({'woopra_cart_value': cart.total_price})
return some_template.render(context)
For some data, it is annoying to do this for every view, so you may want
to set variables in a context processor that you add to the
:data:`TEMPLATE_CONTEXT_PROCESSORS` list in :file:`settings.py`::
from django.utils.hashcompat import md5_constructor as md5
GRAVATAR_URL = 'http://www.gravatar.com/avatar/'
def woopra_custom_data(request):
try:
email = request.user.email
except AttributeError:
return {}
email_hash = md5(email).hexdigest()
avatar_url = "%s%s" % (GRAVATAR_URL, email_hash)
return {'woopra_avatar': avatar_url}
Just remember that if you set the same context variable in the
:class:`~django.template.context.RequestContext` constructor and in a
context processor, the latter clobbers the former.
Standard variables that will be displayed in the Woopra live visitor
data are listed in the table below, but you can define any ``woopra_*``
variable you like and have that detail passed from within the visitor
live stream data when viewing Woopra.
==================== ===================================
Context variable Description
==================== ===================================
``woopra_name`` The visitor's full name.
-------------------- -----------------------------------
``woopra_email`` The visitor's email address.
-------------------- -----------------------------------
``woopra_avatar`` A URL link to a visitor avatar.
==================== ===================================
.. _`custom visitor data`: http://www.woopra.com/docs/tracking/custom-visitor-data/
Identifying authenticated users
-------------------------------
If you have not set the ``woopra_name`` or ``woopra_email`` variables
explicitly, the username and email address of an authenticated user are
passed to Woopra automatically. See :ref:`identifying-visitors`.
----
Thanks go to Woopra for their support with the development of this
application.