mirror of
https://github.com/jazzband/django-analytical.git
synced 2026-03-16 22:20:25 +00:00
Merge remote-tracking branch 'upstream/master'
This commit is contained in:
commit
4b7bf0d228
19 changed files with 561 additions and 93 deletions
1
.bandit
Symbolic link
1
.bandit
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
tox.ini
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
/build
|
||||
/dist
|
||||
/docs/_build
|
||||
/MANIFEST
|
||||
|
||||
/docs/_templates/layout.html
|
||||
|
|
|
|||
80
.travis.yml
80
.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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
15
README.rst
15
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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ TAG_MODULES = [
|
|||
'analytical.intercom',
|
||||
'analytical.kiss_insights',
|
||||
'analytical.kiss_metrics',
|
||||
'analytical.matomo',
|
||||
'analytical.mixpanel',
|
||||
'analytical.olark',
|
||||
'analytical.optimizely',
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
</script>
|
||||
"""
|
||||
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')
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
118
analytical/templatetags/matomo.py
Normal file
118
analytical/templatetags/matomo.py
Normal file
|
|
@ -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)
|
||||
|
|
@ -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]+)?(/[^/?#@:]+)*$')
|
||||
|
|
|
|||
152
analytical/tests/test_tag_matomo.py
Normal file
152
analytical/tests/test_tag_matomo.py
Normal file
|
|
@ -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(
|
||||
'<!-- 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)
|
||||
|
|
@ -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",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
@ -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'
|
||||
|
|
|
|||
160
docs/services/matomo.rst
Normal file
160
docs/services/matomo.rst
Normal file
|
|
@ -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 %}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
.. _`Matomo best practice for Integration Plugins`: http://matomo.org/integrate/how-to/
|
||||
|
||||
|
||||
|
||||
.. _matomo-configuration:
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
Before you can use the Matomo integration, you must first define
|
||||
domain name and optional URI path to your Matomo server, as well as
|
||||
the Matomo ID of the website you're tracking with your Matomo server,
|
||||
in your project settings.
|
||||
|
||||
|
||||
Setting the domain
|
||||
------------------
|
||||
|
||||
Your Django project needs to know where your Matomo server is located.
|
||||
Typically, you'll have Matomo installed on a subdomain of its own
|
||||
(e.g. ``matomo.example.com``), otherwise it runs in a subdirectory of
|
||||
a website of yours (e.g. ``www.example.com/matomo``). Set
|
||||
:const:`MATOMO_DOMAIN_PATH` in the project :file:`settings.py` file
|
||||
accordingly::
|
||||
|
||||
MATOMO_DOMAIN_PATH = 'matomo.example.com'
|
||||
|
||||
If you do not set a domain the tracking code will not be rendered.
|
||||
|
||||
|
||||
Setting the site ID
|
||||
-------------------
|
||||
|
||||
Your Matomo server can track several websites. Each website has its
|
||||
site ID (this is the ``idSite`` parameter in the query string of your
|
||||
browser's address bar when you visit the Matomo Dashboard). Set
|
||||
:const:`MATOMO_SITE_ID` in the project :file:`settings.py` file to
|
||||
the value corresponding to the website you're tracking::
|
||||
|
||||
MATOMO_SITE_ID = '4'
|
||||
|
||||
If you do not set the site ID the tracking code will not be rendered.
|
||||
|
||||
|
||||
.. _matomo-uservars:
|
||||
|
||||
User variables
|
||||
--------------
|
||||
|
||||
Matomo supports sending `custom variables`_ along with default statistics. If
|
||||
you want to set a custom variable, use the context variable ``matomo_vars`` when
|
||||
you render your template. It should be an iterable of custom variables
|
||||
represented by 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. ::
|
||||
|
||||
context = Context({
|
||||
'matomo_vars': [(1, 'foo', 'Sir Lancelot of Camelot'),
|
||||
(2, 'bar', 'To seek the Holy Grail', 'page'),
|
||||
(3, 'spam', 'Blue', 'visit')]
|
||||
})
|
||||
return some_template.render(context)
|
||||
|
||||
Matomo default settings allow up to 5 custom variables for both scope. Variable
|
||||
mapping betweeen index and name must stay constant, or the latest name
|
||||
override the previous one.
|
||||
|
||||
If you use the same user variables in different views and its value can
|
||||
be computed from the HTTP request, you can also set them in a context
|
||||
processor that you add to the :data:`TEMPLATE_CONTEXT_PROCESSORS` list
|
||||
in :file:`settings.py`.
|
||||
|
||||
.. _`custom variables`: http://developer.matomo.org/guides/tracking-javascript-guide#custom-variables
|
||||
|
||||
|
||||
.. _matomo-user-tracking:
|
||||
|
||||
User tracking
|
||||
-------------
|
||||
|
||||
If you use the standard Django authentication system, you can allow Matomo to
|
||||
`track individual users`_ by setting the :data:`ANALYTICAL_AUTO_IDENTIFY`
|
||||
setting to :const:`True`. This is enabled by default. Matomo will identify
|
||||
users based on their ``username``.
|
||||
|
||||
If you disable this settings, or want to customize what user id to use, you can
|
||||
set the context variable ``analytical_identity`` (for global configuration) or
|
||||
``matomo_identity`` (for Matomo specific configuration). Setting one to
|
||||
:const:`None` will disable the user tracking feature::
|
||||
|
||||
# Matomo will identify this user as 'BDFL' if ANALYTICAL_AUTO_IDENTIFY is True or unset
|
||||
request.user = User(username='BDFL', first_name='Guido', last_name='van Rossum')
|
||||
|
||||
# Matomo will identify this user as 'Guido van Rossum'
|
||||
request.user = User(username='BDFL', first_name='Guido', last_name='van Rossum')
|
||||
context = Context({
|
||||
'matomo_identity': request.user.get_full_name()
|
||||
})
|
||||
|
||||
# Matomo will not identify this user (but will still collect statistics)
|
||||
request.user = User(username='BDFL', first_name='Guido', last_name='van Rossum')
|
||||
context = Context({
|
||||
'matomo_identity': None
|
||||
})
|
||||
|
||||
.. _`track individual users`: http://developer.matomo.org/guides/tracking-javascript-guide#user-id
|
||||
|
||||
Disabling cookies
|
||||
-----------------
|
||||
|
||||
If you want to `disable cookies`_, set :data:`MATOMO_DISABLE_COOKIES` to
|
||||
:const:`True`. This is disabled by default.
|
||||
|
||||
.. _`disable cookies`: https://matomo.org/faq/general/faq_157/
|
||||
|
||||
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` (which
|
||||
takes the value of :const:`INTERNAL_IPS` by default) the tracking code
|
||||
is commented out. See :ref:`identifying-visitors` for important
|
||||
information about detecting the visitor IP address.
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
==================================
|
||||
Matomo (formerly Piwik) -- open source web analytics
|
||||
Piwik (deprecated) -- open source web analytics
|
||||
==================================
|
||||
|
||||
Piwik_ is an open analytics platform currently used by individuals,
|
||||
|
|
@ -9,6 +9,14 @@ will always be yours, because you run your own analytics server.
|
|||
.. _Piwik: http://www.piwik.org/
|
||||
|
||||
|
||||
Deprecated
|
||||
==========
|
||||
|
||||
Note that Piwik is now known as Matomo. New projects should use the
|
||||
Matomo integration. The Piwik integration in django-analytical is
|
||||
deprecated and eventually will be removed.
|
||||
|
||||
|
||||
Installation
|
||||
============
|
||||
|
||||
|
|
|
|||
10
setup.py
10
setup.py
|
|
@ -61,6 +61,7 @@ setup(
|
|||
license=package.__license__,
|
||||
description=package.__doc__.strip(),
|
||||
long_description=read_file('README.rst'),
|
||||
long_description_content_type='text/x-rst',
|
||||
author=package.__author__,
|
||||
author_email=package.__email__,
|
||||
packages=[
|
||||
|
|
@ -74,13 +75,9 @@ setup(
|
|||
'Development Status :: 5 - Production/Stable',
|
||||
'Environment :: Web Environment',
|
||||
'Framework :: Django',
|
||||
'Framework :: Django :: 1.7',
|
||||
'Framework :: Django :: 1.8',
|
||||
'Framework :: Django :: 1.9',
|
||||
'Framework :: Django :: 1.10',
|
||||
'Framework :: Django :: 1.11',
|
||||
'Framework :: Django :: 2.0',
|
||||
'Framework :: Django :: 2.1',
|
||||
'Framework :: Django :: 2.2',
|
||||
'Intended Audience :: Developers',
|
||||
'License :: OSI Approved :: MIT License',
|
||||
'Operating System :: OS Independent',
|
||||
|
|
@ -99,7 +96,4 @@ setup(
|
|||
url='https://github.com/jazzband/django-analytical',
|
||||
download_url='https://github.com/jazzband/django-analytical/archive/master.zip',
|
||||
cmdclass=cmdclass,
|
||||
install_requires=[
|
||||
'Django>=1.7.0',
|
||||
],
|
||||
)
|
||||
|
|
|
|||
52
tox.ini
52
tox.ini
|
|
@ -1,15 +1,13 @@
|
|||
[tox]
|
||||
envlist =
|
||||
# Python/Django combinations that are officially supported
|
||||
py{27,34}-django17
|
||||
py{27,34,35}-django18
|
||||
py{27,34,35}-django19
|
||||
py{27,34,35}-django110
|
||||
py{27,34,35,36,37}-django111
|
||||
py{34,35,36,37}-django20
|
||||
py{35,36,37}-django21
|
||||
py{27,34,35,36}-django111
|
||||
py{35,36,37}-django{21,22}
|
||||
flake8
|
||||
bandit
|
||||
readme
|
||||
docs
|
||||
clean
|
||||
|
||||
[testenv]
|
||||
commands =
|
||||
|
|
@ -18,33 +16,49 @@ commands =
|
|||
deps =
|
||||
coverage
|
||||
coveralls
|
||||
django17: Django>=1.7,<1.8
|
||||
django18: Django>=1.8,<1.9
|
||||
django19: Django>=1.9,<1.10
|
||||
django110: Django>=1.10,<1.11
|
||||
django111: Django>=1.11,<2.0
|
||||
django20: Django>=2.0,<2.1
|
||||
django21: Django>=2.1,<2.2
|
||||
django22: Django>=2.2,<3.0
|
||||
passenv = TRAVIS TRAVIS_JOB_ID TRAVIS_BRANCH
|
||||
whitelist_externals = sh
|
||||
|
||||
[testenv:bandit]
|
||||
deps = bandit
|
||||
commands = bandit -r --ini tox.ini
|
||||
|
||||
[testenv:clean]
|
||||
deps = pyclean
|
||||
commands =
|
||||
py3clean -v {toxinidir}
|
||||
rm -rf .tox/ django_analytical.egg-info/ build/ dist/ docs/_build/
|
||||
whitelist_externals =
|
||||
rm
|
||||
|
||||
[testenv:docs]
|
||||
deps = sphinx
|
||||
commands = sphinx-build -b html -d docs/_build/doctrees docs docs/_build/html
|
||||
whitelist_externals = make
|
||||
|
||||
[testenv:flake8]
|
||||
deps = flake8
|
||||
commands = flake8
|
||||
|
||||
[testenv:readme]
|
||||
deps = readme_renderer
|
||||
commands = python setup.py check --restructuredtext --strict
|
||||
deps = twine
|
||||
commands =
|
||||
{envpython} setup.py -q sdist bdist_wheel
|
||||
twine check dist/*
|
||||
|
||||
[travis:env]
|
||||
DJANGO =
|
||||
1.7: django17
|
||||
1.8: django18
|
||||
1.9: django19
|
||||
1.10: django110
|
||||
1.11: django111
|
||||
2.0: django20
|
||||
2.1: django21
|
||||
2.2: django22
|
||||
|
||||
[bandit]
|
||||
exclude = .cache,.git,.tox,build,dist,docs,tests
|
||||
targets = .
|
||||
|
||||
[flake8]
|
||||
exclude = .cache,.git,.tox,build,dist
|
||||
max-line-length = 100
|
||||
|
|
|
|||
Loading…
Reference in a new issue