diff --git a/.bandit b/.bandit
new file mode 120000
index 0000000..bf39a01
--- /dev/null
+++ b/.bandit
@@ -0,0 +1 @@
+tox.ini
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index f3546f5..6e8fe88 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,6 +6,7 @@
/build
/dist
+/docs/_build
/MANIFEST
/docs/_templates/layout.html
diff --git a/.travis.yml b/.travis.yml
index 69a13a3..6731930 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,39 +1,57 @@
-language: python
-cache: pip
+dist: xenial
+sudo: true
+language: python
python:
- - 2.7
- - 3.4
- - 3.5
- - 3.6
+- 2.7
+- 3.4
+- 3.5
+- 3.6
+- 3.7
+
env:
- - DJANGO=1.7
- - DJANGO=1.8
- - DJANGO=1.9
- - DJANGO=1.10
- - DJANGO=1.11
- - DJANGO=2.0
- - DJANGO=2.1
+- DJANGO=1.11
+- DJANGO=2.1
+- DJANGO=2.2
+
matrix:
+ allow_failures:
+ - env: TOXENV=bandit
exclude:
- # Python/Django combinations that aren't officially supported
- - { python: 3.5, env: DJANGO=1.7 }
- - { python: 3.6, env: DJANGO=1.7 }
- - { python: 3.6, env: DJANGO=1.8 }
- - { python: 3.6, env: DJANGO=1.9 }
- - { python: 3.6, env: DJANGO=1.10 }
- - { python: 2.7, env: DJANGO=2.0 }
- - { python: 2.7, env: DJANGO=2.1 }
- - { python: 3.4, env: DJANGO=2.1 }
- include:
- - { python: 3.6, env: TOXENV=flake8 }
- - { python: 3.6, env: TOXENV=readme }
- # Work around Travis Python 3.7 issue: https://github.com/travis-ci/travis-ci/issues/9815
- - { python: 3.7, env: DJANGO=1.11, dist: xenial, sudo: true }
- - { python: 3.7, env: DJANGO=2.0, dist: xenial, sudo: true }
- - { python: 3.7, env: DJANGO=2.1, dist: xenial, sudo: true }
+ # Python/Django combinations that aren't officially supported
+ - { env: DJANGO=1.11, python: 3.7 }
+ - { env: DJANGO=2.1, python: 2.7 }
+ - { env: DJANGO=2.1, python: 3.4 }
+ - { env: DJANGO=2.2, python: 2.7 }
+ - { env: DJANGO=2.2, python: 3.4 }
install:
- - pip install tox-travis
+- pip install tox-travis
script:
- - tox
+- tox
+
+stages:
+- lint
+- test
+- deploy
+
+jobs:
+ include:
+ - { stage: lint, env: TOXENV=flake8, python: 3.7 }
+ - { stage: lint, env: TOXENV=bandit, python: 3.7 }
+ - { stage: lint, env: TOXENV=readme, python: 3.7 }
+ - stage: deploy
+ env:
+ python: 3.7
+ install: skip
+ script: skip
+ deploy:
+ provider: pypi
+ server: https://jazzband.co/projects/django-analytical/upload
+ distributions: sdist bdist_wheel
+ user: jazzband
+ password:
+ secure: JCr5hRjAeXuiISodCJf8HWd4BTJMpl2eiHI8NciPaSM9WwOOeUXxmlcP8+lWlXxgM4BYUC/O7Q90fkwj5x06n+z4oyJSEVerTvCDcpeZ68KMMG1tR1jTbHcxfEKoEvcs2J0fThJ9dIMtfbtUbIpzusJHkZPjsIy8HAJDw8knnJs=
+ on:
+ tags: true
+ repo: jazzband/django-analytical
diff --git a/AUTHORS.rst b/AUTHORS.rst
index 7cc73a7..a32ba85 100644
--- a/AUTHORS.rst
+++ b/AUTHORS.rst
@@ -5,8 +5,9 @@ from `Eric Davis`_, `Paul Oswald`_, `Uros Trebec`_, `Steven Skoczen`_,
`Philippe O. Wagner`_, `Max Arnold`_ , `Martín Gaitán`_, `Craig Bruce`_,
`Peter Bittner`_, `Scott Adams`_, `Eric Amador`_, `Alexandre Pocquet`_,
`Brad Pitcher`_, `Hugo Osvaldo Barrera`_, `Nikolay Korotkiy`_,
- `Steve Schwarz`_, `Aleck Landgraf`_, `Marc Bourqui`_,
- `Diederik van der Boor`_, `Matthäus G. Chajdas`_ and others.
+`Steve Schwarz`_, `Aleck Landgraf`_, `Marc Bourqui`_,
+`Diederik van der Boor`_, `Matthäus G. Chajdas`_, `Scott Karlin`_
+and others.
Included Javascript code snippets for integration of the analytics
services were written by the respective service providers.
@@ -46,3 +47,4 @@ The work on Intercom was made possible by `GreenKahuna`_.
.. _`Analytical`: https://github.com/jkrall/analytical
.. _`Bateau Knowledge`: http://www.bateauknowledge.nl/
.. _`GreenKahuna`: http://www.greenkahuna.com/
+.. _`Scott Karlin`: https://github.com/sckarlin
diff --git a/LICENSE.txt b/LICENSE.txt
index 5a77e87..596cd51 100644
--- a/LICENSE.txt
+++ b/LICENSE.txt
@@ -1,4 +1,4 @@
-Copyright (C) 2011-2016 Joost Cassee and others
+Copyright (C) 2011-2019 Joost Cassee and contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/README.rst b/README.rst
index 9b41962..0fa7527 100644
--- a/README.rst
+++ b/README.rst
@@ -1,7 +1,7 @@
django-analytical |latest-version|
==================================
-|travis-ci| |coveralls| |health| |python-support| |license| |gitter| |jazzband|
+|build-status| |coverage| |python-support| |license| |gitter| |jazzband|
The django-analytical application integrates analytics services into a
Django_ project.
@@ -25,23 +25,20 @@ an asynchronous version of the Javascript code if possible.
.. |latest-version| image:: https://img.shields.io/pypi/v/django-analytical.svg
:alt: Latest version on PyPI
- :target: https://pypi.python.org/pypi/django-analytical
-.. |travis-ci| image:: https://img.shields.io/travis/jazzband/django-analytical/master.svg
+ :target: https://pypi.org/project/django-analytical/
+.. |build-status| image:: https://img.shields.io/travis/jazzband/django-analytical/master.svg
:alt: Build status
:target: https://travis-ci.org/jazzband/django-analytical
-.. |coveralls| image:: https://coveralls.io/repos/jazzband/django-analytical/badge.svg
+.. |coverage| image:: https://img.shields.io/coveralls/github/jazzband/django-analytical/master.svg
:alt: Test coverage
:target: https://coveralls.io/r/jazzband/django-analytical
-.. |health| image:: https://landscape.io/github/jazzband/django-analytical/master/landscape.svg?style=flat
- :target: https://landscape.io/github/jazzband/django-analytical/master
- :alt: Code health
.. |python-support| image:: https://img.shields.io/pypi/pyversions/django-analytical.svg
- :target: https://pypi.python.org/pypi/django-analytical
+ :target: https://pypi.org/project/django-analytical/
:alt: Python versions
.. |license| image:: https://img.shields.io/pypi/l/django-analytical.svg
:alt: Software license
:target: https://github.com/jazzband/django-analytical/blob/master/LICENSE.txt
-.. |gitter| image:: https://badges.gitter.im/Join%20Chat.svg
+.. |gitter| image:: https://img.shields.io/gitter/room/jazzband/django-analytical.svg
:alt: Gitter chat room
:target: https://gitter.im/jazzband/django-analytical
.. |jazzband| image:: https://jazzband.co/static/img/badge.svg
diff --git a/analytical/__init__.py b/analytical/__init__.py
index d5354f6..9a9d9b1 100644
--- a/analytical/__init__.py
+++ b/analytical/__init__.py
@@ -5,5 +5,5 @@ Analytics service integration for Django projects
__author__ = "Joost Cassee"
__email__ = "joost@cassee.net"
__version__ = "2.5.0"
-__copyright__ = "Copyright (C) 2011-2017 Joost Cassee and others"
-__license__ = "MIT License"
+__copyright__ = "Copyright (C) 2011-2019 Joost Cassee and contributors"
+__license__ = "MIT"
diff --git a/analytical/templatetags/analytical.py b/analytical/templatetags/analytical.py
index 6e74bd7..f74684e 100644
--- a/analytical/templatetags/analytical.py
+++ b/analytical/templatetags/analytical.py
@@ -30,6 +30,7 @@ TAG_MODULES = [
'analytical.intercom',
'analytical.kiss_insights',
'analytical.kiss_metrics',
+ 'analytical.matomo',
'analytical.mixpanel',
'analytical.olark',
'analytical.optimizely',
diff --git a/analytical/templatetags/google_analytics_js.py b/analytical/templatetags/google_analytics_js.py
index 377d1f8..d23467e 100644
--- a/analytical/templatetags/google_analytics_js.py
+++ b/analytical/templatetags/google_analytics_js.py
@@ -30,14 +30,12 @@ m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
}})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
ga('create', '{property_id}', 'auto', {create_fields});
-{display_features}
-ga('send', 'pageview');
-{commands}
+{commands}ga('send', 'pageview');
"""
-REQUIRE_DISPLAY_FEATURES = "ga('require', 'displayfeatures');"
-CUSTOM_VAR_CODE = "ga('set', '{name}', {value});"
-ANONYMIZE_IP_CODE = "ga('set', 'anonymizeIp', true);"
+REQUIRE_DISPLAY_FEATURES = "ga('require', 'displayfeatures');\n"
+CUSTOM_VAR_CODE = "ga('set', '{name}', {value});\n"
+ANONYMIZE_IP_CODE = "ga('set', 'anonymizeIp', true);\n"
register = Library()
@@ -70,11 +68,13 @@ class GoogleAnalyticsJsNode(Node):
commands = self._get_custom_var_commands(context)
commands.extend(self._get_other_commands(context))
display_features = getattr(settings, 'GOOGLE_ANALYTICS_DISPLAY_ADVERTISING', False)
+ if display_features:
+ commands.insert(0, REQUIRE_DISPLAY_FEATURES)
+
html = SETUP_CODE.format(
property_id=self.property_id,
create_fields=json.dumps(create_fields),
- display_features=REQUIRE_DISPLAY_FEATURES if display_features else '',
- commands=" ".join(commands),
+ commands="".join(commands),
)
if is_internal_ip(context, 'GOOGLE_ANALYTICS'):
html = disable_html(html, 'Google Analytics')
diff --git a/analytical/templatetags/intercom.py b/analytical/templatetags/intercom.py
index 888a0ab..a2d34a3 100644
--- a/analytical/templatetags/intercom.py
+++ b/analytical/templatetags/intercom.py
@@ -8,8 +8,8 @@ import hashlib
import hmac
import json
import sys
-import time
import re
+import time
from django.conf import settings
from django.template import Library, Node, TemplateSyntaxError
@@ -29,7 +29,7 @@ TRACKING_CODE = """
register = Library()
-def _timestamp(when): # type: (datetime) -> float
+def _timestamp(when):
"""
Python 2 compatibility for `datetime.timestamp()`.
"""
@@ -37,7 +37,7 @@ def _timestamp(when): # type: (datetime) -> float
when.timestamp())
-def _hashable_bytes(data): # type: (AnyStr) -> bytes
+def _hashable_bytes(data):
"""
Coerce strings to hashable bytes.
"""
@@ -49,7 +49,7 @@ def _hashable_bytes(data): # type: (AnyStr) -> bytes
raise TypeError(data)
-def intercom_user_hash(data): # type: (AnyStr) -> Optional[str]
+def intercom_user_hash(data):
"""
Return a SHA-256 HMAC `user_hash` as expected by Intercom, if configured.
@@ -117,9 +117,9 @@ class IntercomNode(Node):
# (If both user_id and email are present, the user_id field takes precedence.)
# See:
# https://www.intercom.com/help/configure-intercom-for-your-product-or-site/staying-secure/enable-identity-verification-on-your-web-product
- user_hash_data = params.get('user_id', params.get('email')) # type: Optional[str]
+ user_hash_data = params.get('user_id', params.get('email'))
if user_hash_data:
- user_hash = intercom_user_hash(str(user_hash_data)) # type: Optional[str]
+ user_hash = intercom_user_hash(str(user_hash_data))
if user_hash is not None:
params.setdefault('user_hash', user_hash)
diff --git a/analytical/templatetags/matomo.py b/analytical/templatetags/matomo.py
new file mode 100644
index 0000000..98bdf8a
--- /dev/null
+++ b/analytical/templatetags/matomo.py
@@ -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 = """
+
+
+""" # 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)
diff --git a/analytical/templatetags/piwik.py b/analytical/templatetags/piwik.py
index 77269cc..91fe648 100644
--- a/analytical/templatetags/piwik.py
+++ b/analytical/templatetags/piwik.py
@@ -14,6 +14,8 @@ from django.template import Library, Node, TemplateSyntaxError
from analytical.utils import (is_internal_ip, disable_html,
get_required_setting, get_identity)
+import warnings
+warnings.warn('The Piwik module is deprecated; use the Matomo module.', DeprecationWarning)
# domain name (characters separated by a dot), optional port, optional URI path, no slash
DOMAINPATH_RE = re.compile(r'^(([^./?#@:]+\.)*[^./?#@:]+)+(:[0-9]+)?(/[^/?#@:]+)*$')
diff --git a/analytical/tests/test_tag_matomo.py b/analytical/tests/test_tag_matomo.py
new file mode 100644
index 0000000..d3d6785
--- /dev/null
+++ b/analytical/tests/test_tag_matomo.py
@@ -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/piwik.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/piwik.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(
+ ''), 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)
diff --git a/docs/_ext/local.py b/docs/_ext/local.py
index b3280ac..3dae85a 100644
--- a/docs/_ext/local.py
+++ b/docs/_ext/local.py
@@ -19,8 +19,3 @@ def setup(app):
rolename="lookup",
indextemplate="pair: %s; field lookup type",
)
- app.add_description_unit(
- directivename="decorator",
- rolename="dec",
- indextemplate="pair: %s; function decorator",
- )
diff --git a/docs/install.rst b/docs/install.rst
index b641065..1f97f07 100644
--- a/docs/install.rst
+++ b/docs/install.rst
@@ -155,6 +155,11 @@ settings required to enable each service are listed here:
KISS_METRICS_API_KEY = '0123456789abcdef0123456789abcdef01234567'
+* :doc:`Matomo (formerly Piwik) `::
+
+ MATOMO_DOMAIN_PATH = 'your.matomo.server/optional/path'
+ MATOMO_SITE_ID = '123'
+
* :doc:`Mixpanel `::
MIXPANEL_API_TOKEN = '0123456789abcdef0123456789abcdef'
@@ -171,7 +176,7 @@ settings required to enable each service are listed here:
PERFORMABLE_API_KEY = '123abc'
-* :doc:`Matomo (formerly Piwik) `::
+* :doc:`Piwik (deprecated, see Matomo) `::
PIWIK_DOMAIN_PATH = 'your.piwik.server/optional/path'
PIWIK_SITE_ID = '123'
diff --git a/docs/services/matomo.rst b/docs/services/matomo.rst
new file mode 100644
index 0000000..0aa4731
--- /dev/null
+++ b/docs/services/matomo.rst
@@ -0,0 +1,160 @@
+==================================
+Matomo (formerly Piwik) -- open source web analytics
+==================================
+
+Matomo_ is an open analytics platform currently used by individuals,
+companies and governments all over the world.
+
+.. _Matomo: http://matomo.org/
+
+
+Installation
+============
+
+To start using the Matomo 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 Matomo 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:`matomo-configuration`.
+
+The Matomo tracking code is inserted into templates using a template
+tag. Load the :mod:`matomo` template tag library and insert the
+:ttag:`matomo` 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 as recommended by the
+`Matomo best practice for Integration Plugins`_::
+
+ {% load matomo %}
+ ...
+ {% matomo %}
+