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)