Merge pull request #135 from EDSI-Tech/issue-108

Basic support of Google analytics.js
This commit is contained in:
Joost Cassee 2018-11-15 20:19:09 +01:00 committed by GitHub
commit 32c45f2f0c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 557 additions and 3 deletions

View file

@ -23,6 +23,7 @@ TAG_MODULES = [
'analytical.facebook_pixel',
'analytical.gauges',
'analytical.google_analytics',
'analytical.google_analytics_js',
'analytical.gosquared',
'analytical.hotjar',
'analytical.hubspot',

View file

@ -1,5 +1,7 @@
"""
Google Analytics template tags and filters.
DEPRECATED
"""
from __future__ import absolute_import

View file

@ -0,0 +1,155 @@
"""
Google Analytics template tags and filters, using the new analytics.js library.
"""
from __future__ import absolute_import
import decimal
import re
from django.conf import settings
from django.template import Library, Node, TemplateSyntaxError
from analytical.utils import (
AnalyticalException,
disable_html,
get_domain,
get_required_setting,
is_internal_ip,
)
TRACK_SINGLE_DOMAIN = 1
TRACK_MULTIPLE_SUBDOMAINS = 2
TRACK_MULTIPLE_DOMAINS = 3
PROPERTY_ID_RE = re.compile(r'^UA-\d+-\d+$')
SETUP_CODE = """
<script>
(function(i,s,o,g,r,a,m){{i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){{
(i[r].q=i[r].q||[]).push(arguments)}},i[r].l=1*new Date();a=s.createElement(o),
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}
</script>
"""
REQUIRE_DISPLAY_FEATURES = "ga('require', 'displayfeatures');"
CUSTOM_VAR_CODE = "ga('set', '{name}', {value});"
ANONYMIZE_IP_CODE = "ga('set', 'anonymizeIp', true);"
register = Library()
@register.tag
def google_analytics_js(parser, token):
"""
Google Analytics tracking template tag.
Renders Javascript code to track page visits. You must supply
your website property ID (as a string) in the
``GOOGLE_ANALYTICS_JS_PROPERTY_ID`` setting.
"""
bits = token.split_contents()
if len(bits) > 1:
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
return GoogleAnalyticsJsNode()
class GoogleAnalyticsJsNode(Node):
def __init__(self):
self.property_id = get_required_setting(
'GOOGLE_ANALYTICS_JS_PROPERTY_ID', PROPERTY_ID_RE,
"must be a string looking like 'UA-XXXXXX-Y'")
def render(self, context):
import json
create_fields = self._get_domain_fields(context)
create_fields.update(self._get_other_create_fields(context))
commands = self._get_custom_var_commands(context)
commands.extend(self._get_other_commands(context))
display_features = getattr(settings, 'GOOGLE_ANALYTICS_DISPLAY_ADVERTISING', False)
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),
)
if is_internal_ip(context, 'GOOGLE_ANALYTICS'):
html = disable_html(html, 'Google Analytics')
return html
def _get_domain_fields(self, context):
domain_fields = {}
tracking_type = getattr(settings, 'GOOGLE_ANALYTICS_TRACKING_STYLE', TRACK_SINGLE_DOMAIN)
if tracking_type == TRACK_SINGLE_DOMAIN:
pass
else:
domain = get_domain(context, 'google_analytics')
if domain is None:
raise AnalyticalException(
"tracking multiple domains with Google Analytics requires a domain name")
domain_fields['legacyCookieDomain'] = domain
if tracking_type == TRACK_MULTIPLE_DOMAINS:
domain_fields['allowLinker'] = True
return domain_fields
def _get_other_create_fields(self, context):
other_fields = {}
site_speed_sample_rate = getattr(settings, 'GOOGLE_ANALYTICS_SITE_SPEED_SAMPLE_RATE', False)
if site_speed_sample_rate is not False:
value = int(decimal.Decimal(site_speed_sample_rate))
if not 0 <= value <= 100:
raise AnalyticalException(
"'GOOGLE_ANALYTICS_SITE_SPEED_SAMPLE_RATE' must be >= 0 and <= 100")
other_fields['siteSpeedSampleRate'] = value
sample_rate = getattr(settings, 'GOOGLE_ANALYTICS_SAMPLE_RATE', False)
if sample_rate is not False:
value = int(decimal.Decimal(sample_rate))
if not 0 <= value <= 100:
raise AnalyticalException("'GOOGLE_ANALYTICS_SAMPLE_RATE' must be >= 0 and <= 100")
other_fields['sampleRate'] = value
cookie_expires = getattr(settings, 'GOOGLE_ANALYTICS_COOKIE_EXPIRATION', False)
if cookie_expires is not False:
value = int(decimal.Decimal(cookie_expires))
if value < 0:
raise AnalyticalException("'GOOGLE_ANALYTICS_COOKIE_EXPIRATION' must be >= 0")
other_fields['cookieExpires'] = value
return other_fields
def _get_custom_var_commands(self, context):
values = (
context.get('google_analytics_var%s' % i) for i in range(1, 6)
)
params = [(i, v) for i, v in enumerate(values, 1) if v is not None]
commands = []
for _, var in params:
name = var[0]
value = var[1]
try:
float(value)
except ValueError:
value = "'{}'".format(value)
commands.append(CUSTOM_VAR_CODE.format(
name=name,
value=value,
))
return commands
def _get_other_commands(self, context):
commands = []
if getattr(settings, 'GOOGLE_ANALYTICS_ANONYMIZE_IP', False):
commands.append(ANONYMIZE_IP_CODE)
return commands
def contribute_to_analytical(add_node):
GoogleAnalyticsJsNode() # ensure properly configured
add_node('head_bottom', GoogleAnalyticsJsNode)

View file

@ -0,0 +1,169 @@
"""
Tests for the Google Analytics template tags and filters, using the new analytics.js library.
"""
from django.http import HttpRequest
from django.template import Context
from django.test.utils import override_settings
from analytical.templatetags.google_analytics_js import GoogleAnalyticsJsNode, \
TRACK_SINGLE_DOMAIN, TRACK_MULTIPLE_DOMAINS, TRACK_MULTIPLE_SUBDOMAINS
from analytical.tests.utils import TestCase, TagTestCase
from analytical.utils import AnalyticalException
@override_settings(GOOGLE_ANALYTICS_JS_PROPERTY_ID='UA-123456-7',
GOOGLE_ANALYTICS_TRACKING_STYLE=TRACK_SINGLE_DOMAIN)
class GoogleAnalyticsTagTestCase(TagTestCase):
"""
Tests for the ``google_analytics_js`` template tag.
"""
def test_tag(self):
r = self.render_tag('google_analytics_js', 'google_analytics_js')
self.assertTrue("""(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
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');""" in r, r)
self.assertTrue("ga('create', 'UA-123456-7', 'auto', {});" in r, r)
self.assertTrue("ga('send', 'pageview');" in r, r)
def test_node(self):
r = GoogleAnalyticsJsNode().render(Context())
self.assertTrue("""(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
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');""" in r, r)
self.assertTrue("ga('create', 'UA-123456-7', 'auto', {});" in r, r)
self.assertTrue("ga('send', 'pageview');" in r, r)
@override_settings(GOOGLE_ANALYTICS_JS_PROPERTY_ID=None)
def test_no_property_id(self):
self.assertRaises(AnalyticalException, GoogleAnalyticsJsNode)
@override_settings(GOOGLE_ANALYTICS_JS_PROPERTY_ID='wrong')
def test_wrong_property_id(self):
self.assertRaises(AnalyticalException, GoogleAnalyticsJsNode)
@override_settings(GOOGLE_ANALYTICS_TRACKING_STYLE=TRACK_MULTIPLE_SUBDOMAINS,
GOOGLE_ANALYTICS_DOMAIN='example.com')
def test_track_multiple_subdomains(self):
r = GoogleAnalyticsJsNode().render(Context())
self.assertTrue(
"""ga('create', 'UA-123456-7', 'auto', {"legacyCookieDomain": "example.com"}""" in r, r)
@override_settings(GOOGLE_ANALYTICS_TRACKING_STYLE=TRACK_MULTIPLE_DOMAINS,
GOOGLE_ANALYTICS_DOMAIN='example.com')
def test_track_multiple_domains(self):
r = GoogleAnalyticsJsNode().render(Context())
self.assertTrue("ga('create', 'UA-123456-7', 'auto', {" in r, r)
self.assertTrue('"legacyCookieDomain": "example.com"' in r, r)
self.assertTrue('"allowLinker\": true' in r, r)
def test_custom_vars(self):
context = Context({
'google_analytics_var1': ('test1', 'foo'),
'google_analytics_var2': ('test2', 'bar'),
'google_analytics_var4': ('test4', 1),
'google_analytics_var5': ('test5', 2.2),
})
r = GoogleAnalyticsJsNode().render(context)
self.assertTrue("ga('set', 'test1', 'foo');" in r, r)
self.assertTrue("ga('set', 'test2', 'bar');" in r, r)
self.assertTrue("ga('set', 'test4', 1);" in r, r)
self.assertTrue("ga('set', 'test5', 2.2);" in r, r)
def test_display_advertising(self):
with override_settings(GOOGLE_ANALYTICS_DISPLAY_ADVERTISING=True):
r = GoogleAnalyticsJsNode().render(Context())
self.assertTrue("""ga('create', 'UA-123456-7', 'auto', {});
ga('require', 'displayfeatures');
ga('send', 'pageview');""" in r, r)
@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 = GoogleAnalyticsJsNode().render(context)
self.assertTrue(r.startswith(
'<!-- Google Analytics disabled on internal IP address'), r)
self.assertTrue(r.endswith('-->'), r)
@override_settings(GOOGLE_ANALYTICS_ANONYMIZE_IP=True)
def test_anonymize_ip(self):
r = GoogleAnalyticsJsNode().render(Context())
self.assertTrue("ga('set', 'anonymizeIp', true);" in r, r)
@override_settings(GOOGLE_ANALYTICS_ANONYMIZE_IP=False)
def test_anonymize_ip_not_present(self):
r = GoogleAnalyticsJsNode().render(Context())
self.assertFalse("ga('set', 'anonymizeIp', true);" in r, r)
@override_settings(GOOGLE_ANALYTICS_SAMPLE_RATE=0.0)
def test_set_sample_rate_min(self):
r = GoogleAnalyticsJsNode().render(Context())
self.assertTrue("""ga('create', 'UA-123456-7', 'auto', {"sampleRate": 0});""" in r, r)
@override_settings(GOOGLE_ANALYTICS_SAMPLE_RATE='100.00')
def test_set_sample_rate_max(self):
r = GoogleAnalyticsJsNode().render(Context())
self.assertTrue("""ga('create', 'UA-123456-7', 'auto', {"sampleRate": 100});""" in r, r)
@override_settings(GOOGLE_ANALYTICS_SAMPLE_RATE=-1)
def test_exception_whenset_sample_rate_too_small(self):
context = Context()
self.assertRaises(AnalyticalException, GoogleAnalyticsJsNode().render, context)
@override_settings(GOOGLE_ANALYTICS_SAMPLE_RATE=101)
def test_exception_when_set_sample_rate_too_large(self):
context = Context()
self.assertRaises(AnalyticalException, GoogleAnalyticsJsNode().render, context)
@override_settings(GOOGLE_ANALYTICS_SITE_SPEED_SAMPLE_RATE=0.0)
def test_set_site_speed_sample_rate_min(self):
r = GoogleAnalyticsJsNode().render(Context())
self.assertTrue(
"""ga('create', 'UA-123456-7', 'auto', {"siteSpeedSampleRate": 0});""" in r, r)
@override_settings(GOOGLE_ANALYTICS_SITE_SPEED_SAMPLE_RATE='100.00')
def test_set_site_speed_sample_rate_max(self):
r = GoogleAnalyticsJsNode().render(Context())
self.assertTrue(
"""ga('create', 'UA-123456-7', 'auto', {"siteSpeedSampleRate": 100});""" in r, r)
@override_settings(GOOGLE_ANALYTICS_SITE_SPEED_SAMPLE_RATE=-1)
def test_exception_whenset_site_speed_sample_rate_too_small(self):
context = Context()
self.assertRaises(AnalyticalException, GoogleAnalyticsJsNode().render, context)
@override_settings(GOOGLE_ANALYTICS_SITE_SPEED_SAMPLE_RATE=101)
def test_exception_when_set_site_speed_sample_rate_too_large(self):
context = Context()
self.assertRaises(AnalyticalException, GoogleAnalyticsJsNode().render, context)
@override_settings(GOOGLE_ANALYTICS_COOKIE_EXPIRATION=0)
def test_set_cookie_expiration_min(self):
r = GoogleAnalyticsJsNode().render(Context())
self.assertTrue("""ga('create', 'UA-123456-7', 'auto', {"cookieExpires": 0});""" in r, r)
@override_settings(GOOGLE_ANALYTICS_COOKIE_EXPIRATION='10000')
def test_set_cookie_expiration_as_string(self):
r = GoogleAnalyticsJsNode().render(Context())
self.assertTrue(
"""ga('create', 'UA-123456-7', 'auto', {"cookieExpires": 10000});""" in r, r)
@override_settings(GOOGLE_ANALYTICS_COOKIE_EXPIRATION=-1)
def test_exception_when_set_cookie_expiration_too_small(self):
context = Context()
self.assertRaises(AnalyticalException, GoogleAnalyticsJsNode().render, context)
@override_settings(GOOGLE_ANALYTICS_JS_PROPERTY_ID='UA-123456-7',
GOOGLE_ANALYTICS_TRACKING_STYLE=TRACK_MULTIPLE_DOMAINS,
GOOGLE_ANALYTICS_DOMAIN=None,
ANALYTICAL_DOMAIN=None)
class NoDomainTestCase(TestCase):
def test_exception_without_domain(self):
context = Context()
self.assertRaises(AnalyticalException, GoogleAnalyticsJsNode().render, context)

View file

@ -1,5 +1,5 @@
======================================
Google Analytics -- traffic analysis
Google Analytics (legacy) -- traffic analysis
======================================
`Google Analytics`_ is the well-known web analytics service from
@ -15,7 +15,7 @@ features.
Installation
============
To start using the Google Analytics integration, you must have installed
To start using the Google Analytics (legacy) 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.
@ -72,7 +72,7 @@ Tracking multiple domains
The default code is suitable for tracking a single domain. If you track
multiple domains, set the :const:`GOOGLE_ANALYTICS_TRACKING_STYLE`
setting to one of the :const:`analytical.templatetags.google_analytics.SCOPE_*`
setting to one of the :const:`analytical.templatetags.google_analytics.TRACK_*`
constants:
============================= ===== =============================================

View file

@ -0,0 +1,227 @@
======================================
Google Analytics -- traffic analysis
======================================
`Google Analytics`_ is the well-known web analytics service from
Google. The product is aimed more at marketers than webmasters or
technologists, supporting integration with AdWords and other e-commence
features.
.. _`Google Analytics`: http://www.google.com/analytics/
.. google-analytics-installation:
Installation
============
To start using the Google Analytics 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 Google Analytics 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:`google-analytics-configuration`.
The Google Analytics tracking code is inserted into templates using a
template tag. Load the :mod:`google_analytics_js` template tag library and
insert the :ttag:`google_analytics_js` 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 head::
{% load google_analytics_js %}
<html>
<head>
...
{% google_analytics_js %}
</head>
...
.. _google-analytics-configuration:
Configuration
=============
Before you can use the Google Analytics integration, you must first set
your website property ID. If you track multiple domains with the same
code, you also need to set-up the domain. Finally, you can add custom
segments for Google Analytics to track.
.. _google-analytics-property-id:
Setting the property ID
-----------------------
Every website you track with Google Analytics gets its own property ID,
and the :ttag:`google_analytics_js` tag will include it in the rendered
Javascript code. You can find the web property ID on the overview page
of your account. Set :const:`GOOGLE_ANALYTICS_JS_PROPERTY_ID` in the
project :file:`settings.py` file::
GOOGLE_ANALYTICS_JS_PROPERTY_ID = 'UA-XXXXXX-X'
If you do not set a property ID, the tracking code will not be rendered.
Tracking multiple domains
-------------------------
The default code is suitable for tracking a single domain. If you track
multiple domains, set the :const:`GOOGLE_ANALYTICS_TRACKING_STYLE`
setting to one of the :const:`analytical.templatetags.google_analytics_js.TRACK_*`
constants:
============================= ===== =============================================
Constant Value Description
============================= ===== =============================================
``TRACK_SINGLE_DOMAIN`` 1 Track one domain.
``TRACK_MULTIPLE_SUBDOMAINS`` 2 Track multiple subdomains of the same top
domain (e.g. `fr.example.com` and
`nl.example.com`).
``TRACK_MULTIPLE_DOMAINS`` 3 Track multiple top domains (e.g. `example.fr`
and `example.nl`).
============================= ===== =============================================
As noted, the default tracking style is
:const:`~analytical.templatetags.google_analytics_js.TRACK_SINGLE_DOMAIN`.
When you track multiple (sub)domains, django-analytical needs to know
what domain name to pass to Google Analytics. If you use the contrib
sites app, the domain is automatically picked up from the current
:const:`~django.contrib.sites.models.Site` instance. Otherwise, you may
either pass the domain to the template tag through the context variable
:const:`google_analytics_domain` (fallback: :const:`analytical_domain`)
or set it in the project :file:`settings.py` file using
:const:`GOOGLE_ANALYTICS_DOMAIN` (fallback: :const:`ANALYTICAL_DOMAIN`).
Display Advertising
-------------------
Display Advertising allows you to view Demographics and Interests reports,
add Remarketing Lists and support DoubleClick Campain Manager integration.
You can enable `Display Advertising features`_ by setting the
:const:`GOOGLE_ANALYTICS_DISPLAY_ADVERTISING` configuration setting::
GOOGLE_ANALYTICS_DISPLAY_ADVERTISING = True
By default, display advertising features are disabled.
.. _`Display Advertising features`: https://support.google.com/analytics/answer/3450482
.. _google-analytics-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:`GOOGLE_ANALYTICS_INTERNAL_IPS`
setting, the tracking code is commented out. It takes the value of
:const:`ANALYTICAL_INTERNAL_IPS` by default (which in turn is
:const:`INTERNAL_IPS` by default). See :ref:`identifying-visitors` for
important information about detecting the visitor IP address.
.. _google-analytics-custom-variables:
Custom variables
----------------
As described in the Google Analytics `custom variables`_ documentation
page, you can define custom segments. Using template context variables
``google_analytics_var1`` through ``google_analytics_var5``, you can let
the :ttag:`google_analytics_js` tag pass custom variables to Google
Analytics automatically. You can set the context variables in your view
when your render a template containing the tracking code::
context = RequestContext({'google_analytics_var1': ('gender', 'female'),
'google_analytics_var2': ('visit', 1)})
return some_template.render(context)
The value of the context variable is a tuple *(name, value)*.
You may want to set custom variables in a context processor that you add
to the :data:`TEMPLATE_CONTEXT_PROCESSORS` list in :file:`settings.py`::
def google_analytics_segment_language(request):
try:
return {'google_analytics_var3': request.LANGUAGE_CODE}
except AttributeError:
return {}
Just remember that if you set the same context variable in the
:class:`~django.template.context.RequestContext` constructor and in a
context processor, the latter clobbers the former.
.. _`custom variables`: https://developers.google.com/analytics/devguides/collection/upgrade/reference/gajs-analyticsjs#custom-vars
.. _google-analytics-anonimyze-ips:
Anonymize IPs
-------------
You can enable the `IP anonymization`_ feature by setting the
:const:`GOOGLE_ANALYTICS_ANONYMIZE_IP` configuration setting::
GOOGLE_ANALYTICS_ANONYMIZE_IP = True
This may be mandatory for deployments in countries that have a firm policies
concerning data privacy (e.g. Germany).
By default, IPs are not anonymized.
.. _`IP anonymization`: https://support.google.com/analytics/bin/answer.py?hl=en&answer=2763052
.. _google-analytics-sample-rate:
Sample Rate
-----------
You can configure the `Sample Rate`_ feature by setting the
:const:`GOOGLE_ANALYTICS_SAMPLE_RATE` configuration setting::
GOOGLE_ANALYTICS_SAMPLE_RATE = 10
The value is a percentage and can be between 0 and 100 and can be a string or
integer value.
.. _`Sample Rate`: https://developers.google.com/analytics/devguides/collection/analyticsjs/field-reference#sampleRate
.. _google-analytics-site-speed-sample-rate:
Site Speed Sample Rate
----------------------
You can configure the `Site Speed Sample Rate`_ feature by setting the
:const:`GOOGLE_ANALYTICS_SITE_SPEED_SAMPLE_RATE` configuration setting::
GOOGLE_ANALYTICS_SITE_SPEED_SAMPLE_RATE = 10
The value is a percentage and can be between 0 and 100 and can be a string or
integer value.
.. _`Site Speed Sample Rate`: https://developers.google.com/analytics/devguides/collection/analyticsjs/field-reference#siteSpeedSampleRate
.. _google-analytics-cookie-expiration:
Cookie Expiration
----------------------
You can configure the `Cookie Expiration`_ feature by setting the
:const:`GOOGLE_ANALYTICS_COOKIE_EXPIRATION` configuration setting::
GOOGLE_ANALYTICS_COOKIE_EXPIRATION = 3600000
The value is the cookie expiration in seconds or 0 to delete the cookie when the browser is closed.
.. _`Cookie Expiration`: https://developers.google.com/analytics/devguides/collection/gajs/methods/gaJSApiBasicConfiguration#_setsessioncookietimeout