Merge branch 'master' of github.com:greenkahuna/django-analytical into greenkahuna-master

This commit is contained in:
Joost Cassee 2014-04-29 01:05:11 +02:00
commit cef2956edb
21 changed files with 357 additions and 18 deletions

View file

@ -11,6 +11,7 @@ The 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`_.
The work on Intercom was made possible by `GreenKahuna`_.
.. _`Joost Cassee`: mailto:joost@cassee.net
.. _`Eric Davis`: https://github.com/edavis
@ -24,5 +25,6 @@ The work on Crazy Egg was made possible by `Bateau Knowledge`_.
.. _`Philippe O. Wagner`: mailto:admin@arteria.ch
.. _`Max Arnold`: https://github.com/max-arnold
.. _`Martín Gaitán`: https://github.com/mgaitan
.. _Analytical: https://github.com/jkrall/analytical
.. _`Analytical`: https://github.com/jkrall/analytical
.. _`Bateau Knowledge`: http://www.bateauknowledge.nl/
.. _`GreenKahuna`: http://www.greenkahuna.com/

View file

@ -1,6 +1,7 @@
Version 0.17.0
--------------
* Update UserVoice support (Martín Gaitán)
* Add support for Intercom.io (Steven Skoczen)
Version 0.16.0
--------------

View file

@ -27,6 +27,7 @@ Currently supported services:
* `Google Analytics`_ traffic analysis
* `GoSquared`_ traffic monitoring
* `HubSpot`_ inbound marketing
* `Intercom`_ live chat and support
* `KISSinsights`_ feedback surveys
* `KISSmetrics`_ funnel analysis
* `Mixpanel`_ event tracking
@ -59,6 +60,7 @@ an issue to discuss your plans.
.. _`Google Analytics`: http://www.google.com/analytics/
.. _`GoSquared`: http://www.gosquared.com/
.. _`HubSpot`: http://www.hubspot.com/
.. _`Intercom`: http://www.intercom.io/
.. _`KISSinsights`: http://www.kissinsights.com/
.. _`KISSmetrics`: http://www.kissmetrics.com/
.. _`Mixpanel`: http://www.mixpanel.com/

View file

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

View file

@ -23,6 +23,7 @@ TAG_MODULES = [
'analytical.google_analytics',
'analytical.gosquared',
'analytical.hubspot',
'analytical.intercom',
'analytical.kiss_insights',
'analytical.kiss_metrics',
'analytical.mixpanel',

View file

@ -86,7 +86,7 @@ class ChartbeatBottomNode(Node):
domain = _get_domain(context)
if domain is not None:
config['domain'] = domain
html = SETUP_CODE % {'config': json.dumps(config)}
html = SETUP_CODE % {'config': json.dumps(config, sort_keys=True)}
if is_internal_ip(context, 'CHARTBEAT'):
html = disable_html(html, 'Chartbeat')
return html

View file

@ -66,7 +66,7 @@ class ClickyNode(Node):
custom.setdefault('session', {})['username'] = identity
html = TRACKING_CODE % {'site_id': self.site_id,
'custom': json.dumps(custom)}
'custom': json.dumps(custom, sort_keys=True)}
if is_internal_ip(context, 'CLICKY'):
html = disable_html(html, 'Clicky')
return html

View file

@ -0,0 +1,88 @@
"""
intercom.io template tags and filters.
"""
from __future__ import absolute_import
import json
import time
import re
from django.template import Library, Node, TemplateSyntaxError
from analytical.utils import disable_html, get_required_setting, is_internal_ip,\
get_user_from_context, get_identity
APP_ID_RE = re.compile(r'[\da-f]+$')
TRACKING_CODE = """
<script id="IntercomSettingsScriptTag">
window.intercomSettings = %(settings_json)s;
</script>
<script>(function(){var w=window;var ic=w.Intercom;if(typeof ic==="function"){ic('reattach_activator');ic('update',intercomSettings);}else{var d=document;var i=function(){i.c(arguments)};i.q=[];i.c=function(args){i.q.push(args)};w.Intercom=i;function l(){var s=d.createElement('script');s.type='text/javascript';s.async=true;s.src='https://static.intercomcdn.com/intercom.v1.js';var x=d.getElementsByTagName('script')[0];x.parentNode.insertBefore(s,x);}if(w.attachEvent){w.attachEvent('onload',l);}else{w.addEventListener('load',l,false);}}})()</script>
"""
register = Library()
@register.tag
def intercom(parser, token):
"""
Intercom.io template tag.
Renders Javascript code to intercom.io testing. You must supply
your APP ID account number in the ``INTERCOM_APP_ID``
setting.
"""
bits = token.split_contents()
if len(bits) > 1:
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
return IntercomNode()
class IntercomNode(Node):
def __init__(self):
self.app_id = get_required_setting(
'INTERCOM_APP_ID', APP_ID_RE,
"must be a string looking like 'XXXXXXX'")
def _identify(self, user):
name = user.get_full_name()
if not name:
name = user.username
return name
def _get_custom_attrs(self, context):
vars = {}
for dict_ in context:
for var, val in dict_.items():
if var.startswith('intercom_'):
vars[var[9:]] = val
user = get_user_from_context(context)
if user is not None and user.is_authenticated():
if 'full_name' not in vars:
vars['full_name'] = get_identity(context, 'intercom', self._identify, user)
if 'email' not in vars and user.email:
vars['email'] = user.email
vars['created_at'] = int(time.mktime(user.date_joined.timetuple()))
else:
vars['created_at'] = None
return vars
def render(self, context):
html = ""
user = get_user_from_context(context)
vars = self._get_custom_attrs(context)
vars["app_id"] = self.app_id
html = TRACKING_CODE % {"settings_json": json.dumps(vars, sort_keys=True)}
if is_internal_ip(context, 'INTERCOM') or not user or not user.is_authenticated():
# Intercom is disabled for non-logged in users.
html = disable_html(html, 'Intercom')
return html
def contribute_to_analytical(add_node):
IntercomNode()
add_node('body_bottom', IntercomNode)

View file

@ -78,13 +78,13 @@ class KissMetricsNode(Node):
try:
name, properties = context[EVENT_CONTEXT_KEY]
commands.append(EVENT_CODE % {'name': name,
'properties': json.dumps(properties)})
'properties': json.dumps(properties, sort_keys=True)})
except KeyError:
pass
try:
properties = context[PROPERTY_CONTEXT_KEY]
commands.append(PROPERTY_CODE % {
'properties': json.dumps(properties)})
'properties': json.dumps(properties, sort_keys=True)})
except KeyError:
pass
html = TRACKING_CODE % {'api_key': self.api_key,

View file

@ -56,7 +56,7 @@ class MixpanelNode(Node):
try:
name, properties = context[EVENT_CONTEXT_KEY]
commands.append(EVENT_CODE % {'name': name,
'properties': json.dumps(properties)})
'properties': json.dumps(properties, sort_keys=True)})
except KeyError:
pass
html = TRACKING_CODE % {'token': self.token,

View file

@ -64,7 +64,7 @@ class OlarkNode(Node):
extra_code.append(NICKNAME_CODE % identity)
try:
extra_code.append(STATUS_CODE %
json.dumps(context[STATUS_CONTEXT_KEY]))
json.dumps(context[STATUS_CONTEXT_KEY], sort_keys=True))
except KeyError:
pass
extra_code.extend(self._get_configuration(context))

View file

@ -65,7 +65,7 @@ class ReinvigorateNode(Node):
email = get_identity(context, 'reinvigorate', lambda u: u.email)
if email is not None:
re_vars['context'] = email
tags = " ".join("var re_%s_tag = %s;" % (tag, json.dumps(value))
tags = " ".join("var re_%s_tag = %s;" % (tag, json.dumps(value, sort_keys=True))
for tag, value in re_vars.items())
html = TRACKING_CODE % {'tracking_id': self.tracking_id,

View file

@ -66,7 +66,7 @@ class UserVoiceNode(Node):
getattr(settings, 'USERVOICE_ADD_TRIGGER', True))
html = TRACKING_CODE % {'widget_key': widget_key,
'options': json.dumps(options),
'options': json.dumps(options, sort_keys=True),
'trigger': TRIGGER if trigger else ''}
return html

View file

@ -57,8 +57,8 @@ class WoopraNode(Node):
visitor = self._get_visitor(context)
html = TRACKING_CODE % {
'settings': json.dumps(settings),
'visitor': json.dumps(visitor),
'settings': json.dumps(settings, sort_keys=True),
'visitor': json.dumps(visitor, sort_keys=True),
}
if is_internal_ip(context, 'WOOPRA'):
html = disable_html(html, 'Woopra')

View file

@ -11,6 +11,7 @@ from analytical.tests.test_tag_gauges import *
from analytical.tests.test_tag_google_analytics import *
from analytical.tests.test_tag_gosquared import *
from analytical.tests.test_tag_hubspot import *
from analytical.tests.test_tag_intercom import *
from analytical.tests.test_tag_kiss_insights import *
from analytical.tests.test_tag_kiss_metrics import *
from analytical.tests.test_tag_mixpanel import *

View file

@ -0,0 +1,94 @@
"""
Tests for the intercom template tags and filters.
"""
import datetime
from django.contrib.auth.models import User, AnonymousUser
from django.http import HttpRequest
from django.template import Context
from analytical.templatetags.intercom import IntercomNode
from analytical.tests.utils import TagTestCase, override_settings, SETTING_DELETED
from analytical.utils import AnalyticalException
@override_settings(INTERCOM_APP_ID='1234567890abcdef0123456789')
class IntercomTagTestCase(TagTestCase):
"""
Tests for the ``intercom`` template tag.
"""
def test_tag(self):
rendered_tag = self.render_tag('intercom', 'intercom')
self.assertTrue(rendered_tag.startswith('<!-- Intercom disabled on internal IP address'))
def test_node(self):
now = datetime.datetime(2014, 4, 9, 15, 15, 0)
rendered_tag = IntercomNode().render(Context({
'user': User(
username='test',
first_name='Firstname',
last_name='Lastname',
email="test@example.com",
date_joined=now)
}))
# Because the json isn't predictably ordered, we can't just test the whole thing verbatim.
self.assertEquals("""
<script id="IntercomSettingsScriptTag">
window.intercomSettings = {"app_id": "1234567890abcdef0123456789", "created_at": 1397074500, "email": "test@example.com", "full_name": "Firstname Lastname"};
</script>
<script>(function(){var w=window;var ic=w.Intercom;if(typeof ic==="function"){ic('reattach_activator');ic('update',intercomSettings);}else{var d=document;var i=function(){i.c(arguments)};i.q=[];i.c=function(args){i.q.push(args)};w.Intercom=i;function l(){var s=d.createElement('script');s.type='text/javascript';s.async=true;s.src='https://static.intercomcdn.com/intercom.v1.js';var x=d.getElementsByTagName('script')[0];x.parentNode.insertBefore(s,x);}if(w.attachEvent){w.attachEvent('onload',l);}else{w.addEventListener('load',l,false);}}})()</script>
""", rendered_tag)
@override_settings(INTERCOM_APP_ID=SETTING_DELETED)
def test_no_account_number(self):
self.assertRaises(AnalyticalException, IntercomNode)
@override_settings(INTERCOM_APP_ID='123abQ')
def test_wrong_account_number(self):
self.assertRaises(AnalyticalException, IntercomNode)
def test_identify_name_email_and_created_at(self):
now = datetime.datetime(2014, 4, 9, 15, 15, 0)
r = IntercomNode().render(Context({'user': User(username='test',
first_name='Firstname', last_name='Lastname',
email="test@example.com", date_joined=now)}))
self.assertTrue(
"""window.intercomSettings = {"app_id": "1234567890abcdef0123456789", "created_at": 1397074500, "email": "test@example.com", "full_name": "Firstname Lastname"};"""\
in r
)
def test_custom(self):
r = IntercomNode().render(Context({
'intercom_var1': 'val1',
'intercom_var2': 'val2'
}))
self.assertTrue('var1": "val1", "var2": "val2"' in r)
def test_identify_name_and_email(self):
r = IntercomNode().render(Context({
'user': User(username='test',
first_name='Firstname',
last_name='Lastname',
email="test@example.com")
}))
self.assertTrue('"email": "test@example.com", "full_name": "Firstname Lastname"' in r)
def test_identify_username_no_email(self):
r = IntercomNode().render(Context({'user': User(username='test')}))
self.assertTrue('"full_name": "test"' in r, r)
def test_no_identify_when_explicit_name(self):
r = IntercomNode().render(Context({'intercom_full_name': 'explicit',
'user': User(username='implicit')}))
self.assertTrue('"full_name": "explicit"' in r, r)
def test_no_identify_when_explicit_email(self):
r = IntercomNode().render(Context({'intercom_email': 'explicit',
'user': User(username='implicit')}))
self.assertTrue('"email": "explicit"' in r, r)
def test_disable_for_anonymous_users(self):
r = IntercomNode().render(Context({'user': AnonymousUser()}))
self.assertTrue(r.startswith('<!-- Intercom disabled on internal IP address'), r)

View file

@ -61,7 +61,7 @@ class KissMetricsTagTestCase(TagTestCase):
def test_property(self):
r = KissMetricsNode().render(Context({'kiss_metrics_properties':
{'prop1': 'val1', 'prop2': 'val2'}}))
self.assertTrue("_kmq.push(['set', "
self.assertTrue("_kmq.push([\'set\', "
'{"prop1": "val1", "prop2": "val2"}]);' in r, r)
def test_alias(self):

View file

@ -79,5 +79,4 @@ class UserVoiceTagTestCase(TagTestCase):
@override_settings(USERVOICE_ADD_TRIGGER=False)
def test_auto_trigger_custom_win(self):
r = UserVoiceNode().render(Context({'uservoice_add_trigger': True}))
self.assertTrue("UserVoice.push(['addTrigger', {}]);" in r, r)
self.assertTrue("UserVoice.push(['addTrigger', {}]);" in r, r)

View file

@ -44,15 +44,15 @@ class WoopraTagTestCase(TagTestCase):
r = WoopraNode().render(Context({'woopra_var1': 'val1',
'woopra_var2': 'val2'}))
self.assertTrue('var woo_visitor = {"var1": "val1", "var2": "val2"};'
in r, r)
in r, r)
@override_settings(ANALYTICAL_AUTO_IDENTIFY=True)
def test_identify_name_and_email(self):
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)
self.assertTrue('var woo_visitor = {"email": "test@example.com", '
'"name": "Firstname Lastname"};' in r, r)
@override_settings(ANALYTICAL_AUTO_IDENTIFY=True)
def test_identify_username_no_email(self):

View file

@ -128,6 +128,10 @@ settings required to enable each service are listed here:
HUBSPOT_PORTAL_ID = '1234'
HUBSPOT_DOMAIN = 'somedomain.web101.hubspot.com'
* :doc:`Intercom <services/intercom>`::
INTERCOM_APP_ID = '0123456789abcdef0123456789abcdef01234567'
* :doc:`KISSinsights <services/kiss_insights>`::
KISS_INSIGHTS_ACCOUNT_NUMBER = '12345'

147
docs/services/intercom.rst Normal file
View file

@ -0,0 +1,147 @@
=============================
Intercom.io -- Real-time tracking
=============================
Intercom.io_ is an easy way to implement real-chat and individual
support for a website
.. _Intercom.io: http://www.intercom.io/
.. intercom-installation:
Installation
============
To start using the Intercom.io 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 Intercom.io 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:`intercom-configuration`.
The Intercom.io Javascript code is inserted into templates using a
template tag. Load the :mod:`intercom` template tag library and
insert the :ttag:`intercom` 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 body::
{% load intercom %}
<html>
<head></head>
<body>
<!-- Your page -->
{% intercom %}
</body>
</html>
...
.. _intercom-configuration:
Configuration
=============
Before you can use the Intercom.io integration, you must first set your
app id.
.. _intercom-site-id:
Setting the app id
--------------------------
Intercom.io gives you a unique app id, and the :ttag:`intercom`
tag will include it in the rendered Javascript code. You can find your
app id by clicking the *Tracking Code* link when logged into
the on the intercom.io website. A page will display containing
HTML code looking like this::
<script id="IntercomSettingsScriptTag">
window.intercomSettings = { name: "Jill Doe", email: "jill@example.com", created_at: 1234567890, app_id: "XXXXXXXXXXXXXXXXXXXXXXX" };
</script>
<script>(function(){var w=window;var ic=w.Intercom;if(typeof ic==="function"){ic('reattach_activator');ic('update',intercomSettings);}else{var d=document;var i=function(){i.c(arguments)};i.q=[];i.c=function(args){i.q.push(args)};w.Intercom=i;function l(){var s=d.createElement('script');s.type='text/javascript';s.async=true;s.src='https://static.intercomcdn.com/intercom.v1.js';var x=d.getElementsByTagName('script')[0];x.parentNode.insertBefore(s,x);}if(w.attachEvent){w.attachEvent('onload',l);}else{w.addEventListener('load',l,false);}}})()</script>
The code ``XXXXXXXXXXXXXXXXXXXXXXX`` is your app id. Set
:const:`INTERCOM_APP_ID` in the project :file:`settings.py`
file::
INTERCOM_APP_ID = 'XXXXXXXXXXXXXXXXXXXXXXX'
If you do not set an app id, the Javascript code will not be
rendered.
Custom data
-----------
As described in the Intercom documentation on `custom visitor data`_,
the data that is tracked by Intercom can be customized. Using template
context variables, you can let the :ttag:`intercom` tag pass custom data
to Intercom automatically. You can set the context variables in your view
when your render a template containing the tracking code::
context = RequestContext({'intercom_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 intercom_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 {'intercom_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 Intercom live visitor
data are listed in the table below, but you can define any ``intercom_*``
variable you like and have that detail passed from within the visitor
live stream data when viewing Intercom.
==================== ===========================================
Context variable Description
==================== ===========================================
``intercom_name`` The visitor's full name.
-------------------- -------------------------------------------
``intercom_email`` The visitor's email address.
-------------------- -------------------------------------------
``created_at`` The date the visitor created an account
==================== ===========================================
.. _`custom visitor data`: http://docs.intercom.io/custom-data/adding-custom-data
Identifying authenticated users
-------------------------------
If you have not set the ``intercom_name`` or ``intercom_email`` variables
explicitly, the username and email address of an authenticated user are
passed to Intercom automatically. See :ref:`identifying-visitors`.
.. _intercom-internal-ips:
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:`ANALYTICAL_INTERNAL_IPS` setting
(which is :const:`INTERNAL_IPS` by default,) the tracking code is
commented out. See :ref:`identifying-visitors` for important information
about detecting the visitor IP address.