Skip to content

Commit

Permalink
Create Matomo module; leave Piwik to be deprecated
Browse files Browse the repository at this point in the history
With the rebranding of Piwik to Matomo, this commit:
* copies the piwik module to matomo and rebrands
* notes that the piwik module is deprecated
* updates the javascript to the current Matomo version

Implements #132
  • Loading branch information
sckarlin authored and bittner committed Apr 10, 2019
1 parent bfe92c7 commit e5f8c19
Show file tree
Hide file tree
Showing 7 changed files with 447 additions and 2 deletions.
1 change: 1 addition & 0 deletions analytical/templatetags/analytical.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
'analytical.intercom',
'analytical.kiss_insights',
'analytical.kiss_metrics',
'analytical.matomo',
'analytical.mixpanel',
'analytical.olark',
'analytical.optimizely',
Expand Down
118 changes: 118 additions & 0 deletions analytical/templatetags/matomo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
"""
Matomo template tags and filters.
"""

from __future__ import absolute_import

from collections import namedtuple
from itertools import chain
import re

from django.conf import settings
from django.template import Library, Node, TemplateSyntaxError

from analytical.utils import (is_internal_ip, disable_html,
get_required_setting, get_identity)


# domain name (characters separated by a dot), optional port, optional URI path, no slash
DOMAINPATH_RE = re.compile(r'^(([^./?#@:]+\.)*[^./?#@:]+)+(:[0-9]+)?(/[^/?#@:]+)*$')

# numeric ID
SITEID_RE = re.compile(r'^\d+$')

TRACKING_CODE = """
<script type="text/javascript">
var _paq = window._paq || [];
%(variables)s
%(commands)s
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
(function() {
var u="//%(url)s/";
_paq.push(['setTrackerUrl', u+'matomo.php']);
_paq.push(['setSiteId', %(siteid)s]);
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
g.type='text/javascript'; g.async=true; g.defer=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);
})();
</script>
<noscript><p><img src="//%(url)s/piwik.php?idsite=%(siteid)s" style="border:0;" alt="" /></p></noscript>
""" # noqa

VARIABLE_CODE = '_paq.push(["setCustomVariable", %(index)s, "%(name)s", "%(value)s", "%(scope)s"]);' # noqa
IDENTITY_CODE = '_paq.push(["setUserId", "%(userid)s"]);'
DISABLE_COOKIES_CODE = '_paq.push([\'disableCookies\']);'

DEFAULT_SCOPE = 'page'

MatomoVar = namedtuple('MatomoVar', ('index', 'name', 'value', 'scope'))


register = Library()


@register.tag
def matomo(parser, token):
"""
Matomo tracking template tag.
Renders Javascript code to track page visits. You must supply
your Matomo domain (plus optional URI path), and tracked site ID
in the ``MATOMO_DOMAIN_PATH`` and the ``MATOMO_SITE_ID`` setting.
Custom variables can be passed in the ``matomo_vars`` context
variable. It is an iterable of custom variables as tuples like:
``(index, name, value[, scope])`` where scope may be ``'page'``
(default) or ``'visit'``. Index should be an integer and the
other parameters should be strings.
"""
bits = token.split_contents()
if len(bits) > 1:
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
return MatomoNode()


class MatomoNode(Node):
def __init__(self):
self.domain_path = \
get_required_setting('MATOMO_DOMAIN_PATH', DOMAINPATH_RE,
"must be a domain name, optionally followed "
"by an URI path, no trailing slash (e.g. "
"matomo.example.com or my.matomo.server/path)")
self.site_id = \
get_required_setting('MATOMO_SITE_ID', SITEID_RE,
"must be a (string containing a) number")

def render(self, context):
custom_variables = context.get('matomo_vars', ())

complete_variables = (var if len(var) >= 4 else var + (DEFAULT_SCOPE,)
for var in custom_variables)

variables_code = (VARIABLE_CODE % MatomoVar(*var)._asdict()
for var in complete_variables)

commands = []
if getattr(settings, 'MATOMO_DISABLE_COOKIES', False):
commands.append(DISABLE_COOKIES_CODE)

userid = get_identity(context, 'matomo')
if userid is not None:
variables_code = chain(variables_code, (
IDENTITY_CODE % {'userid': userid},
))

html = TRACKING_CODE % {
'url': self.domain_path,
'siteid': self.site_id,
'variables': '\n '.join(variables_code),
'commands': '\n '.join(commands)
}
if is_internal_ip(context, 'MATOMO'):
html = disable_html(html, 'Matomo')
return html


def contribute_to_analytical(add_node):
MatomoNode() # ensure properly configured
add_node('body_bottom', MatomoNode)
2 changes: 2 additions & 0 deletions analytical/templatetags/piwik.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
from analytical.utils import (is_internal_ip, disable_html,
get_required_setting, get_identity)

import warnings
warnings.warn('The Piwik module will be deprecated', DeprecationWarning)

# domain name (characters separated by a dot), optional port, optional URI path, no slash
DOMAINPATH_RE = re.compile(r'^(([^./?#@:]+\.)*[^./?#@:]+)+(:[0-9]+)?(/[^/?#@:]+)*$')
Expand Down
152 changes: 152 additions & 0 deletions analytical/tests/test_tag_matomo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
"""
Tests for the Matomo template tags and filters.
"""

from django.contrib.auth.models import User
from django.http import HttpRequest
from django.template import Context
from django.test.utils import override_settings

from analytical.templatetags.matomo import MatomoNode
from analytical.tests.utils import TagTestCase
from analytical.utils import AnalyticalException


@override_settings(MATOMO_DOMAIN_PATH='example.com', MATOMO_SITE_ID='345')
class MatomoTagTestCase(TagTestCase):
"""
Tests for the ``matomo`` template tag.
"""

def test_tag(self):
r = self.render_tag('matomo', 'matomo')
self.assertTrue('"//example.com/"' in r, r)
self.assertTrue("_paq.push(['setSiteId', 345]);" in r, r)
self.assertTrue('img src="//example.com/matomo.php?idsite=345"'
in r, r)

def test_node(self):
r = MatomoNode().render(Context({}))
self.assertTrue('"//example.com/";' in r, r)
self.assertTrue("_paq.push(['setSiteId', 345]);" in r, r)
self.assertTrue('img src="//example.com/matomo.php?idsite=345"'
in r, r)

@override_settings(MATOMO_DOMAIN_PATH='example.com/matomo',
MATOMO_SITE_ID='345')
def test_domain_path_valid(self):
r = self.render_tag('matomo', 'matomo')
self.assertTrue('"//example.com/matomo/"' in r, r)

@override_settings(MATOMO_DOMAIN_PATH='example.com:1234',
MATOMO_SITE_ID='345')
def test_domain_port_valid(self):
r = self.render_tag('matomo', 'matomo')
self.assertTrue('"//example.com:1234/";' in r, r)

@override_settings(MATOMO_DOMAIN_PATH='example.com:1234/matomo',
MATOMO_SITE_ID='345')
def test_domain_port_path_valid(self):
r = self.render_tag('matomo', 'matomo')
self.assertTrue('"//example.com:1234/matomo/"' in r, r)

@override_settings(MATOMO_DOMAIN_PATH=None)
def test_no_domain(self):
self.assertRaises(AnalyticalException, MatomoNode)

@override_settings(MATOMO_SITE_ID=None)
def test_no_siteid(self):
self.assertRaises(AnalyticalException, MatomoNode)

@override_settings(MATOMO_SITE_ID='x')
def test_siteid_not_a_number(self):
self.assertRaises(AnalyticalException, MatomoNode)

@override_settings(MATOMO_DOMAIN_PATH='http://www.example.com')
def test_domain_protocol_invalid(self):
self.assertRaises(AnalyticalException, MatomoNode)

@override_settings(MATOMO_DOMAIN_PATH='example.com/')
def test_domain_slash_invalid(self):
self.assertRaises(AnalyticalException, MatomoNode)

@override_settings(MATOMO_DOMAIN_PATH='example.com:123:456')
def test_domain_multi_port(self):
self.assertRaises(AnalyticalException, MatomoNode)

@override_settings(MATOMO_DOMAIN_PATH='example.com:')
def test_domain_incomplete_port(self):
self.assertRaises(AnalyticalException, MatomoNode)

@override_settings(MATOMO_DOMAIN_PATH='example.com:/matomo')
def test_domain_uri_incomplete_port(self):
self.assertRaises(AnalyticalException, MatomoNode)

@override_settings(MATOMO_DOMAIN_PATH='example.com:12df')
def test_domain_port_invalid(self):
self.assertRaises(AnalyticalException, MatomoNode)

@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 = MatomoNode().render(context)
self.assertTrue(r.startswith(
'<!-- Matomo disabled on internal IP address'), r)
self.assertTrue(r.endswith('-->'), r)

def test_uservars(self):
context = Context({'matomo_vars': [(1, 'foo', 'foo_val'),
(2, 'bar', 'bar_val', 'page'),
(3, 'spam', 'spam_val', 'visit')]})
r = MatomoNode().render(context)
msg = 'Incorrect Matomo custom variable rendering. Expected:\n%s\nIn:\n%s'
for var_code in ['_paq.push(["setCustomVariable", 1, "foo", "foo_val", "page"]);',
'_paq.push(["setCustomVariable", 2, "bar", "bar_val", "page"]);',
'_paq.push(["setCustomVariable", 3, "spam", "spam_val", "visit"]);']:
self.assertIn(var_code, r, msg % (var_code, r))

@override_settings(ANALYTICAL_AUTO_IDENTIFY=True)
def test_default_usertrack(self):
context = Context({
'user': User(username='BDFL', first_name='Guido', last_name='van Rossum')
})
r = MatomoNode().render(context)
msg = 'Incorrect Matomo user tracking rendering.\nNot found:\n%s\nIn:\n%s'
var_code = '_paq.push(["setUserId", "BDFL"]);'
self.assertIn(var_code, r, msg % (var_code, r))

def test_matomo_usertrack(self):
context = Context({
'matomo_identity': 'BDFL'
})
r = MatomoNode().render(context)
msg = 'Incorrect Matomo user tracking rendering.\nNot found:\n%s\nIn:\n%s'
var_code = '_paq.push(["setUserId", "BDFL"]);'
self.assertIn(var_code, r, msg % (var_code, r))

def test_analytical_usertrack(self):
context = Context({
'analytical_identity': 'BDFL'
})
r = MatomoNode().render(context)
msg = 'Incorrect Matomo user tracking rendering.\nNot found:\n%s\nIn:\n%s'
var_code = '_paq.push(["setUserId", "BDFL"]);'
self.assertIn(var_code, r, msg % (var_code, r))

@override_settings(ANALYTICAL_AUTO_IDENTIFY=True)
def test_disable_usertrack(self):
context = Context({
'user': User(username='BDFL', first_name='Guido', last_name='van Rossum'),
'matomo_identity': None
})
r = MatomoNode().render(context)
msg = 'Incorrect Matomo user tracking rendering.\nFound:\n%s\nIn:\n%s'
var_code = '_paq.push(["setUserId", "BDFL"]);'
self.assertNotIn(var_code, r, msg % (var_code, r))

@override_settings(MATOMO_DISABLE_COOKIES=True)
def test_disable_cookies(self):
r = MatomoNode().render(Context({}))
self.assertTrue("_paq.push(['disableCookies']);" in r, r)
7 changes: 6 additions & 1 deletion docs/install.rst
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,11 @@ settings required to enable each service are listed here:

KISS_METRICS_API_KEY = '0123456789abcdef0123456789abcdef01234567'

* :doc:`Matomo (formerly Piwik) <services/matomo>`::

MATOMO_DOMAIN_PATH = 'your.matomo.server/optional/path'
MATOMO_SITE_ID = '123'

* :doc:`Mixpanel <services/mixpanel>`::

MIXPANEL_API_TOKEN = '0123456789abcdef0123456789abcdef'
Expand All @@ -171,7 +176,7 @@ settings required to enable each service are listed here:

PERFORMABLE_API_KEY = '123abc'

* :doc:`Matomo (formerly Piwik) <services/piwik>`::
* :doc:`Piwik (deprecated, see Matomo) <services/piwik>`::

PIWIK_DOMAIN_PATH = 'your.piwik.server/optional/path'
PIWIK_SITE_ID = '123'
Expand Down
Loading

0 comments on commit e5f8c19

Please sign in to comment.