diff --git a/README.rst b/README.rst index 33fd8f0..ae5910d 100644 --- a/README.rst +++ b/README.rst @@ -57,6 +57,7 @@ Currently Supported Services * `Gaug.es`_ real time web analytics * `Google Analytics`_ traffic analysis * `GoSquared`_ traffic monitoring +* `Hotjar`_ analytics and user feedback * `HubSpot`_ inbound marketing * `Intercom`_ live chat and support * `KISSinsights`_ feedback surveys @@ -81,6 +82,7 @@ Currently Supported Services .. _`Gaug.es`: http://get.gaug.es/ .. _`Google Analytics`: http://www.google.com/analytics/ .. _`GoSquared`: http://www.gosquared.com/ +.. _`Hotjar`: https://www.hotjar.com/ .. _`HubSpot`: http://www.hubspot.com/ .. _`Intercom`: http://www.intercom.io/ .. _`KISSinsights`: http://www.kissinsights.com/ diff --git a/analytical/templatetags/analytical.py b/analytical/templatetags/analytical.py index 7851268..8ab2cdb 100644 --- a/analytical/templatetags/analytical.py +++ b/analytical/templatetags/analytical.py @@ -24,6 +24,7 @@ TAG_MODULES = [ 'analytical.gauges', 'analytical.google_analytics', 'analytical.gosquared', + 'analytical.hotjar', 'analytical.hubspot', 'analytical.intercom', 'analytical.kiss_insights', diff --git a/analytical/templatetags/hotjar.py b/analytical/templatetags/hotjar.py new file mode 100644 index 0000000..b85a126 --- /dev/null +++ b/analytical/templatetags/hotjar.py @@ -0,0 +1,65 @@ +""" +Hotjar template tags and filters. +""" +from __future__ import absolute_import + +import re + +from django.template import Library, Node, TemplateSyntaxError + +from analytical.utils import get_required_setting, is_internal_ip, disable_html + + +HOTJAR_TRACKING_CODE = """\ + +""" + + +register = Library() + + +def _validate_no_args(token): + bits = token.split_contents() + if len(bits) > 1: + raise TemplateSyntaxError("'%s' takes no arguments" % bits[0]) + + +@register.tag +def hotjar(parser, token): + """ + Hotjar template tag. + """ + _validate_no_args(token) + return HotjarNode() + + +class HotjarNode(Node): + + def __init__(self): + self.site_id = get_required_setting( + 'HOTJAR_SITE_ID', + re.compile(r'^\d+$'), + "must be (a string containing) a number", + ) + + def render(self, context): + html = HOTJAR_TRACKING_CODE % {'HOTJAR_SITE_ID': self.site_id} + if is_internal_ip(context, 'HOTJAR'): + return disable_html(html, 'Hotjar') + else: + return html + + +def contribute_to_analytical(add_node): + # ensure properly configured + HotjarNode() + add_node('head_bottom', HotjarNode) diff --git a/analytical/tests/test_tag_hotjar.py b/analytical/tests/test_tag_hotjar.py new file mode 100644 index 0000000..c7e656d --- /dev/null +++ b/analytical/tests/test_tag_hotjar.py @@ -0,0 +1,84 @@ +""" +Tests for the Hotjar template tags. +""" +from django.http import HttpRequest +from django.template import Context, Template, TemplateSyntaxError +from django.test import override_settings + +from analytical.templatetags.analytical import _load_template_nodes +from analytical.templatetags.hotjar import HotjarNode +from analytical.tests.utils import TagTestCase +from analytical.utils import AnalyticalException + + +expected_html = """\ + +""" + + +@override_settings(HOTJAR_SITE_ID='123456789') +class HotjarTagTestCase(TagTestCase): + + maxDiff = None + + def test_tag(self): + html = self.render_tag('hotjar', 'hotjar') + self.assertEqual(expected_html, html) + + def test_node(self): + html = HotjarNode().render(Context({})) + self.assertEqual(expected_html, html) + + def test_tags_take_no_args(self): + self.assertRaisesRegexp( + TemplateSyntaxError, + r"^'hotjar' takes no arguments$", + lambda: (Template('{% load hotjar %}{% hotjar "arg" %}') + .render(Context({}))), + ) + + @override_settings(HOTJAR_SITE_ID=None) + def test_no_id(self): + expected_pattern = r'^HOTJAR_SITE_ID setting is not set$' + self.assertRaisesRegexp(AnalyticalException, expected_pattern, HotjarNode) + + @override_settings(HOTJAR_SITE_ID='invalid') + def test_invalid_id(self): + expected_pattern = ( + r"^HOTJAR_SITE_ID setting: must be \(a string containing\) a number: 'invalid'$") + self.assertRaisesRegexp(AnalyticalException, expected_pattern, HotjarNode) + + @override_settings(ANALYTICAL_INTERNAL_IPS=['1.1.1.1']) + def test_render_internal_ip(self): + request = HttpRequest() + request.META['REMOTE_ADDR'] = '1.1.1.1' + context = Context({'request': request}) + + actual_html = HotjarNode().render(context) + disabled_html = '\n'.join([ + '', + ]) + self.assertEqual(disabled_html, actual_html) + + def test_contribute_to_analytical(self): + """ + `hotjar.contribute_to_analytical` registers the head and body nodes. + """ + template_nodes = _load_template_nodes() + self.assertEqual({ + 'head_top': [], + 'head_bottom': [HotjarNode], + 'body_top': [], + 'body_bottom': [], + }, template_nodes) diff --git a/docs/services/hotjar.rst b/docs/services/hotjar.rst new file mode 100644 index 0000000..54e3403 --- /dev/null +++ b/docs/services/hotjar.rst @@ -0,0 +1,73 @@ +===================================== +Hotjar -- analytics and user feedback +===================================== + +`Hotjar`_ is a website analytics and user feedback tool. + +.. _`Hotjar`: https://www.hotjar.com/ + + +.. hotjar-installation: + +Installation +============ + +To start using the Hotjar 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 Hotjar 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:`hotjar-configuration`. + +The Hotjar code is inserted into templates using template tags. +Because every page that you want to track must have the tag, +it is useful to add it to your base template. +At the top of the template, load the :mod:`hotjar` template tag library. +Then insert the :ttag:`hotjar` tag at the bottom of the head section:: + + {% load hotjar %} + + + ... + {% hotjar %} + + ... + + + +.. _hotjar-configuration: + +Configuration +============= + +Before you can use the Hotjar integration, you must first set your Site ID. + + +.. _hotjar-id: + +Setting the Hotjar Site ID +-------------------------- + +You can find the Hotjar Site ID in the "Sites & Organizations" section of your Hotjar account. +Set :const:`HOTJAR_SITE_ID` in the project :file:`settings.py` file:: + + HOTJAR_SITE_ID = 'XXXXXXXXX' + +If you do not set a Hotjar Site ID, the code will not be rendered. + + +.. _hotjar-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:`HOTJAR_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.