Adds intercom.io support, fixes inconsent fails from json ordering.

This commit is contained in:
skoczen 2014-04-25 10:46:30 -07:00
parent 6763d3e06b
commit d76a0981b8
15 changed files with 263 additions and 18 deletions

View file

@ -10,6 +10,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
@ -22,5 +23,6 @@ The work on Crazy Egg was made possible by `Bateau Knowledge`_.
.. _`Tinnet Coronam`: https://github.com/tinnet
.. _`Philippe O. Wagner`: mailto:admin@arteria.ch
.. _`Max Arnold`: https://github.com/max-arnold
.. _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,3 +1,7 @@
Version 0.17.0
--------------
* Add support for Intercom.io (Steven Skoczen)
Version 0.16.0
--------------
* Add support for GA Display Advertising features (Max Arnold)

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

@ -0,0 +1,63 @@
"""
intercom.io template tags and filters.
"""
from __future__ import absolute_import
import time
import re
from django.template import Library, Node, TemplateSyntaxError
from analytical.utils import disable_html, get_required_setting, get_user_from_context
APP_ID_RE = re.compile(r'[\da-f]+$')
TRACKING_CODE = """
<script id="IntercomSettingsScriptTag">
window.intercomSettings = {'app_id': '%(app_id)s', 'full_name': '%(full_name)s', 'email': '%(email)s', 'created_at': %(created_at)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 render(self, context):
html = ""
user = get_user_from_context(context)
if user is not None and user.is_authenticated():
html = TRACKING_CODE % {
'app_id': self.app_id,
'full_name': "%s %s" % (user.first_name, user.last_name),
'email': user.email,
'created_at': int(time.mktime(user.date_joined.timetuple())),
}
else:
# Intercom is disabled for non-logged in users.
html = disable_html(html, 'Intercom')
return html
def contribute_to_analytical(add_node):
IntercomNode()
add_node('head_bottom', IntercomNode)

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

@ -54,8 +54,12 @@ class ClickyTagTestCase(TagTestCase):
def test_custom(self):
r = ClickyNode().render(Context({'clicky_var1': 'val1',
'clicky_var2': 'val2'}))
self.assertTrue(re.search('var clicky_custom = {.*'
'"var1": "val1", "var2": "val2".*};', r), r)
self.assertTrue(
(re.search('var clicky_custom = {.*'
'"var1": "val1", "var2": "val2".*};', r) or
re.search('var clicky_custom = {.*'
'"var2": "val2", "var1": "val1".*};', r)), r
)
@override_settings(ANALYTICAL_INTERNAL_IPS=['1.1.1.1'])
def test_render_internal_ip(self):

View file

@ -0,0 +1,66 @@
"""
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):
self.assertEqual("""<!-- Intercom disabled on internal IP address
-->""",
self.render_tag('intercom', 'intercom'))
def test_node(self):
now = datetime.datetime(2014, 4, 9, 15, 15, 0)
self.assertEqual(
"""
<script id="IntercomSettingsScriptTag">
window.intercomSettings = {'app_id': '1234567890abcdef0123456789', 'full_name': 'Firstname Lastname', 'email': 'test@example.com', 'created_at': 1397074500};
</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>
""",
IntercomNode().render(Context({
'user': User(
username='test',
first_name='Firstname',
last_name='Lastname',
email="test@example.com",
date_joined=now)
}))
)
@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', "
"'full_name': 'Firstname Lastname', "
"'email': 'test@example.com', 'created_at': 1397074500};" 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

@ -55,14 +55,19 @@ class KissMetricsTagTestCase(TagTestCase):
def test_event(self):
r = KissMetricsNode().render(Context({'kiss_metrics_event':
('test_event', {'prop1': 'val1', 'prop2': 'val2'})}))
self.assertTrue("_kmq.push(['record', 'test_event', "
'{"prop1": "val1", "prop2": "val2"}]);' in r, r)
self.assertTrue(
("_kmq.push(['record', 'test_event', "
'{"prop1": "val1", "prop2": "val2"}]);' in r or\
"_kmq.push(['record', 'test_event', "
'{"prop2": "val2", "prop1": "val1"}]);' in r), r)
def test_property(self):
r = KissMetricsNode().render(Context({'kiss_metrics_properties':
{'prop1': 'val1', 'prop2': 'val2'}}))
self.assertTrue("_kmq.push(['set', "
'{"prop1": "val1", "prop2": "val2"}]);' in r, r)
self.assertTrue(
('_kmq.push([\'set\', {"prop1": "val1", "prop2": "val2"}]);' in r or
'_kmq.push([\'set\', {"prop2": "val2", "prop1": "val1"}]);' in r),
r)
def test_alias(self):
r = KissMetricsNode().render(Context({'kiss_metrics_alias':

View file

@ -54,8 +54,9 @@ class MixpanelTagTestCase(TagTestCase):
def test_event(self):
r = MixpanelNode().render(Context({'mixpanel_event':
('test_event', {'prop1': 'val1', 'prop2': 'val2'})}))
self.assertTrue("mixpanel.track('test_event', "
'{"prop1": "val1", "prop2": "val2"});' in r, r)
self.assertTrue(
('mixpanel.track(\'test_event\', {"prop1": "val1", "prop2": "val2"});' in r or
'mixpanel.track(\'test_event\', {"prop2": "val2", "prop1": "val1"});' in r), r)
@override_settings(ANALYTICAL_INTERNAL_IPS=['1.1.1.1'])
def test_render_internal_ip(self):

View file

@ -89,5 +89,6 @@ class UserVoiceTagTestCase(TagTestCase):
}
}
r = UserVoiceNode().render(Context(vars))
self.assertTrue('"custom_fields": {"field2": "val2", "field1": "val1"}'
in r, r)
self.assertTrue(
('"custom_fields": {"field2": "val2", "field1": "val1"}' in r or
'"custom_fields": {"field1": "val1", "field2": "val2"}' in r ), r)

View file

@ -37,22 +37,25 @@ class WoopraTagTestCase(TagTestCase):
@override_settings(WOOPRA_IDLE_TIMEOUT=1234)
def test_idle_timeout(self):
r = WoopraNode().render(Context({}))
self.assertTrue('var woo_settings = {"domain": "example.com", '
'"idle_timeout": "1234"};' in r, r)
self.assertTrue(
('var woo_settings = {"domain": "example.com", "idle_timeout": "1234"};' in r or
'var woo_settings = {"idle_timeout": "1234", "domain": "example.com"};' 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)
self.assertTrue(
('var woo_visitor = {"var1": "val1", "var2": "val2"};' in r or
'var woo_visitor = {"var2": "val2", "var1": "val1"};' 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 = {"name": "Firstname Lastname", "email": "test@example.com"};' in r or
'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'

View file

@ -0,0 +1,88 @@
=============================
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.
.. _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.