diff --git a/README.rst b/README.rst index 3b5fa6e..c73a268 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 +* `Heap`_ analytics and events tracking * `Hotjar`_ analytics and user feedback * `HubSpot`_ inbound marketing * `Intercom`_ live chat and support @@ -83,6 +84,7 @@ Currently Supported Services .. _`Gaug.es`: http://get.gaug.es/ .. _`Google Analytics`: http://www.google.com/analytics/ .. _`GoSquared`: http://www.gosquared.com/ +.. _`Heap`: https://heapanalytics.com/ .. _`Hotjar`: https://www.hotjar.com/ .. _`HubSpot`: http://www.hubspot.com/ .. _`Intercom`: http://www.intercom.io/ diff --git a/analytical/templatetags/analytical.py b/analytical/templatetags/analytical.py index f9ca446..a7c364b 100644 --- a/analytical/templatetags/analytical.py +++ b/analytical/templatetags/analytical.py @@ -23,6 +23,7 @@ TAG_MODULES = [ 'analytical.google_analytics_js', 'analytical.google_analytics_gtag', 'analytical.gosquared', + 'analytical.heap', 'analytical.hotjar', 'analytical.hubspot', 'analytical.intercom', diff --git a/analytical/templatetags/heap.py b/analytical/templatetags/heap.py new file mode 100644 index 0000000..7ce7339 --- /dev/null +++ b/analytical/templatetags/heap.py @@ -0,0 +1,57 @@ +""" +Heap template tags and filters. +""" + +import re + +from django.template import Library, Node, TemplateSyntaxError + +from analytical.utils import disable_html, get_required_setting, is_internal_ip + +HEAP_TRACKER_ID_RE = re.compile(r'^\d+$') +TRACKING_CODE = """ + + +""" # noqa + +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 heap(parser, token): + """ + Heap tracker template tag. + + Renders Javascript code to track page visits. You must supply + your heap tracker ID (as a string) in the ``HEAP_TRACKER_ID`` + setting. + """ + _validate_no_args(token) + return HeapNode() + + +class HeapNode(Node): + def __init__(self): + self.tracker_id = get_required_setting('HEAP_TRACKER_ID', + HEAP_TRACKER_ID_RE, + "must be an numeric string") + + def render(self, context): + html = TRACKING_CODE % {'tracker_id': self.tracker_id} + if is_internal_ip(context, 'HEAP'): + html = disable_html(html, 'Heap') + return html + + +def contribute_to_analytical(add_node): + HeapNode() # ensure properly configured + add_node('head_bottom', HeapNode) diff --git a/docs/conf.py b/docs/conf.py index 7d86315..1894cd9 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -10,7 +10,6 @@ sys.path.append(os.path.dirname(os.path.abspath('.'))) import analytical # noqa - # -- General configuration -------------------------------------------------- project = 'django-analytical' diff --git a/docs/services/heap.rst b/docs/services/heap.rst new file mode 100644 index 0000000..a07ca8a --- /dev/null +++ b/docs/services/heap.rst @@ -0,0 +1,59 @@ +===================================== +Heap -- analytics and events tracking +===================================== + +`Heap`_ automatically captures all user interactions on your site, from the moment of installation forward. + +.. _`Heap`: https://heap.io/ + + +.. heap-installation: + +Installation +============ + +To start using the Heap 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. + +.. _heap-configuration: + +Configuration +============= + +Before you can use the Heap integration, you must first get your +Heap Tracker ID. If you don't have a Heap account yet, +`sign up`_ to get your Tracker ID. + +.. _`sign up`: https://heap.io/ + + +.. _heap-tracker-id: + +Setting the Tracker ID +---------------------- + +Heap gives you a unique ID. You can find this ID on the Projects page +of your Heap account. Set :const:`HEAP_TRACKER_ID` in the project +:file:`settings.py` file:: + + HEAP_TRACKER_ID = 'XXXXXXXX' + +If you do not set an Tracker ID, the tracking code will not be +rendered. + +The tracking code will be added just before the closing head tag. + + +.. _heap-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. diff --git a/tests/unit/test_tag_heap.py b/tests/unit/test_tag_heap.py new file mode 100644 index 0000000..37ff046 --- /dev/null +++ b/tests/unit/test_tag_heap.py @@ -0,0 +1,50 @@ +""" +Tests for the Heap template tags and filters. +""" + +import pytest +from django.http import HttpRequest +from django.template import Context, Template, TemplateSyntaxError +from django.test.utils import override_settings +from utils import TagTestCase + +from analytical.templatetags.heap import HeapNode +from analytical.utils import AnalyticalException + + +@override_settings(HEAP_TRACKER_ID='123456789') +class HeapTagTestCase(TagTestCase): + """ + Tests for the ``heap`` template tag. + """ + + def test_tag(self): + r = self.render_tag('heap', 'heap') + assert "123456789" in r + + def test_node(self): + r = HeapNode().render(Context({})) + assert "123456789" in r + + def test_tags_take_no_args(self): + with pytest.raises(TemplateSyntaxError, match="'heap' takes no arguments"): + Template('{% load heap %}{% heap "arg" %}').render(Context({})) + + @override_settings(HEAP_TRACKER_ID=None) + def test_no_site_id(self): + with pytest.raises(AnalyticalException): + HeapNode() + + @override_settings(HEAP_TRACKER_ID='abcdefg') + def test_wrong_site_id(self): + with pytest.raises(AnalyticalException): + HeapNode() + + @override_settings(ANALYTICAL_INTERNAL_IPS=['1.1.1.1']) + def test_render_internal_ip(self): + req = HttpRequest() + req.META['REMOTE_ADDR'] = '1.1.1.1' + context = Context({'request': req}) + r = HeapNode().render(context) + assert r.startswith('')