Basic support of Google analytics.js, #108

This commit is contained in:
Marc Bourqui 2018-09-24 14:38:33 +02:00
parent 128c535550
commit f8c4f6bf47
4 changed files with 310 additions and 0 deletions

View file

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

View file

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

View file

@ -0,0 +1,151 @@
"""
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','//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_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_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.extend(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_EXPIRES', False)
if cookie_expires is not False:
value = int(decimal.Decimal(sessionCookieTimeout))
if value < 0:
raise AnalyticalException("'GOOGLE_ANALYTICS_COOKIE_EXPIRES' 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]
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,156 @@
"""
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_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("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("ga('create', 'UA-123456-7', 'auto', {});" in r, r)
self.assertTrue("ga('send', 'pageview');" in r, r)
@override_settings(GOOGLE_ANALYTICS_PROPERTY_ID=None)
def test_no_property_id(self):
self.assertRaises(AnalyticalException, GoogleAnalyticsJsNode)
@override_settings(GOOGLE_ANALYTICS_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', {'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', 'baz'),
'google_analytics_var5': ('test5', 'qux'),
})
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', 'baz');" in r, r)
self.assertTrue("ga('set', 'test5', 'qux');" 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_EXPIRES=0)
def test_set_session_cookie_timeout_min(self):
r = GoogleAnalyticsJsNode().render(Context())
self.assertTrue("ga('create', 'UA-123456-7', 'auto', {'cookieExpires'; 0});" in r, r)
@override_settings(GOOGLE_ANALYTICS_COOKIE_EXPIRES='10000')
def test_set_session_cookie_timeout_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_EXPIRES=-1)
def test_exception_when_set_session_cookie_timeout_too_small(self):
context = Context()
self.assertRaises(AnalyticalException, GoogleAnalyticsJsNode().render, context)
@override_settings(GOOGLE_ANALYTICS_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)