Skip to content

Commit

Permalink
Initial Google Gtag Analytics
Browse files Browse the repository at this point in the history
  • Loading branch information
Peter-Potts committed May 29, 2019
1 parent 5487fd6 commit adee3f7
Show file tree
Hide file tree
Showing 4 changed files with 381 additions and 0 deletions.
1 change: 1 addition & 0 deletions analytical/templatetags/analytical.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
'analytical.gauges',
'analytical.google_analytics',
'analytical.google_analytics_js',
'analytical.google_gtag_js',
'analytical.gosquared',
'analytical.hotjar',
'analytical.hubspot',
Expand Down
129 changes: 129 additions & 0 deletions analytical/templatetags/google_gtag_js.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
"""
Google Analytics template tags and filters, using the new gtag.js library.
gtag.js documentation found at: https://developers.google.com/analytics/devguides/collection/gtagjs/
API reference at: https://developers.google.com/gtagjs/reference/api
"""

from __future__ import absolute_import

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

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

GA_MEASUREMENT_ID_RE = re.compile(r'[a-zA-Z\d_\-]+')

SETUP_CODE = """
<script async src="https://www.googletagmanager.com/gtag/js?id={property_id}"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){{dataLayer.push(arguments);}}
gtag('js', new Date());
{set_commands}
gtag('config', '{property_id}', {config_parameters_json});
</script>
"""

CUSTOM_SET_KV_CODE = "gtag('set', '{key}', {value_json});"
CUSTOM_SET_DATA_CODE = "gtag('set', {value_json});"

# You are allowed to config more than one GA_MEASUREMENT_ID on a page.
# This could be used, but for now is not.
CUSTOM_CONFIG_CODE = "gtag('config', '{property_id}', {config_parameters_json});"

register = Library()


@register.tag
def google_gtag_js(parser, token):
"""
Google Analytics Global Site Tag tracking template tag.
Renders Javascript code to track page visits. You must supply
your website property ID (as a string) in the
``GOOGLE_GTAG_JS_PROPERTY_ID`` setting.
"""
bits = token.split_contents()
if len(bits) > 1:
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
return GoogleGTagJsNode()


class GoogleGTagJsNode(Node):
def __init__(self):
self.property_id = get_required_setting(
'GOOGLE_GTAG_JS_PROPERTY_ID', GA_MEASUREMENT_ID_RE,
"must be a string like a slug")

def render(self, context):
config_parameters = self._get_config_parameters(context)
config_parameters_json = self._to_json(config_parameters)

set_commands = self._get_set_commands(context)

html = SETUP_CODE.format(
property_id=self.property_id,
config_parameters_json=config_parameters_json,
set_commands=" ".join(set_commands),
)
if is_internal_ip(context, 'GOOGLE_ANALYTICS'):
html = disable_html(html, 'Google Analytics')
return html

def _get_config_parameters(self, context):
config_data = getattr(
settings, 'GOOGLE_GTAG_JS_DEFAULT_CONFIG', {},
)

config_data.update(context.get('google_gtag_js_config_data', {}))

return config_data

def _to_json(self, data, default="{}"):
try:
return json.dumps(data)
except ValueError:
return default
except TypeError:
return default

def _get_set_commands(self, context):
commands = []

if 'google_gtag_js_set_data' in context:
try:
commands.append(CUSTOM_SET_DATA_CODE.format(
value_json=json.dumps(context['google_gtag_js_set_data']),
))
except ValueError:
pass

values = (
context.get('google_gtag_js_set%s' % i) for i in range(1, 6)
)
params = [(i, v) for i, v in enumerate(values, 1) if v is not None]

for _, var in params:
key_name = var[0]
value = var[1]
try:
value_json = json.dumps(value)
except ValueError:
value_json = json.dumps(str(value))
commands.append(CUSTOM_SET_KV_CODE.format(
key=key_name,
value_json=value_json,
))
return commands


def contribute_to_analytical(add_node):
GoogleGTagJsNode() # ensure properly configured
add_node('head_top', GoogleGTagJsNode)
107 changes: 107 additions & 0 deletions analytical/tests/test_tag_google_gtag_js.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
"""
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_gtag_js import GoogleGTagJsNode
from analytical.tests.utils import TagTestCase
from analytical.utils import AnalyticalException


@override_settings(GOOGLE_GTAG_JS_PROPERTY_ID='UA-123456-7')
class GoogleGTagJsTagTestCase(TagTestCase):
"""
Tests for the ``google_gtag_js`` template tag.
"""

def test_tag(self):
r = self.render_tag('google_gtag_js', 'google_gtag_js')
self.assertTrue("""<script async src="https://www.googletagmanager.com/gtag/js?id=UA-123456-7"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());""" in r, r)
self.assertTrue("gtag('config', 'UA-123456-7'" in r, r)

def test_node(self):
r = GoogleGTagJsNode().render(Context())
self.assertTrue("""<script async src="https://www.googletagmanager.com/gtag/js?id=UA-123456-7"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());""" in r, r)
self.assertTrue("gtag('config', 'UA-123456-7'" in r, r)

@override_settings(GOOGLE_GTAG_JS_PROPERTY_ID=None)
def test_no_property_id(self):
self.assertRaises(AnalyticalException, GoogleGTagJsNode)

def test_custom_set_vars(self):
context = Context({
'google_gtag_js_set1': ('test1', 'foo'),
'google_gtag_js_set2': ('test2', 'bar'),
'google_gtag_js_set4': ('test4', 1),
'google_gtag_js_set5': ('test5', 2.2),
})
r = GoogleGTagJsNode().render(context)
self.assertTrue("""gtag('set', 'test1', "foo");""" in r, r)
self.assertTrue("""gtag('set', 'test2', "bar");""" in r, r)
self.assertTrue("""gtag('set', 'test4', 1);""" in r, r)
self.assertTrue("""gtag('set', 'test5', 2.2);""" in r, r)

def test_custom_set_data(self):
context = Context({
'google_gtag_js_set_data': {'test1': 'foo'},
})
r = GoogleGTagJsNode().render(context)
self.assertTrue("""gtag('set', {"test1": "foo"});""" in r, r)

def test_custom_set_data_not_ignored(self):
context = Context({
'google_gtag_js_set_data': {'test1': 'foo'},
'google_gtag_js_set2': ('test2', 'bar'),
})
r = GoogleGTagJsNode().render(context)
self.assertTrue("""gtag('set', {"test1": "foo"});""" in r, r)
self.assertTrue("""gtag('set', 'test2', "bar");""" in r, r)

def test_custom_config_context_dic(self):
context = Context({
'google_gtag_js_config_data': {'test1': True},
'google_gtag_js_set1': ('shouldnt_affect', 'config'),
})
r = GoogleGTagJsNode().render(context)
self.assertTrue("""gtag('config', 'UA-123456-7', {"test1": true});""" in r, r)

@override_settings(
GOOGLE_GTAG_JS_DEFAULT_CONFIG={'test1': True},
)
def test_custom_config_defaults_dic(self):
context = Context({
'google_gtag_js_config_data': {},
})
r = GoogleGTagJsNode().render(context)
self.assertTrue("""gtag('config', 'UA-123456-7', {"test1": true});""" in r, r)

@override_settings(
GOOGLE_GTAG_JS_DEFAULT_CONFIG={'test1': True},
)
def test_custom_config_context_overrides_defaults(self):
context = Context({
'google_gtag_js_config_data': {'test1': False},
})
r = GoogleGTagJsNode().render(context)
self.assertTrue("""gtag('config', 'UA-123456-7', {"test1": false});""" 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 = GoogleGTagJsNode().render(context)
self.assertTrue(r.startswith(
'<!-- Google Analytics disabled on internal IP address'), r)
self.assertTrue(r.endswith('-->'), r)
144 changes: 144 additions & 0 deletions docs/services/google_gtag_js.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
======================================
Google Analytics (gtag.js) -- 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.
This uses the gtags.js version of google analytics.

.. _`Google Analytics`: https://developers.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_gtag_js` template tag library and
insert the :ttag:`google_gtag_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 top of the HTML head::

{% load google_gtag_js %}
<html>
<head>
{% google_gtag_js %}
...

</head>
...


.. _google-analytics-configuration:

Configuration
=============

Before you can use the Google Analytics integration, you must first set
your website property ID. Then you can set any analytics config variables
you wish 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_gtag_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_GTAG_JS_PROPERTY_ID` in the
project :file:`settings.py` file::

GOOGLE_GTAG_JS_PROPERTY_ID = 'UA-XXXXXX-X'

If you do not set a property ID, the tracking code will not be rendered.


Google Analytics Tracking Config
---------------------

:ttag:`google_gtag_js` works by letting you use gtag.js 's
'set' and 'config' `javascript commands
<https://developers.google.com/gtagjs/reference/api>`_
.
The 'set' gtag commands are inserted before the 'config' commands.

You are given the option to 'set' gtag values for a request
via context variable ``google_gtag_js_set_data``. If used, this should be
a json serializable object, to be used like ``gtag('set', <value>)``.

Additionally, you can use ``google_gtag_js_set1`` though
``google_gtag_js_set5``, which should each be key value pairs, to be
used like ``gtag('set', <key>, <value>)``.
Key should be a string, and value a json serializable object (which includes strings).


At the present, :ttag:`google_gtag_js` only supports
configuring one 'GA_MEASUREMENT_ID' property.
The options for this config can be set in the following ways:

1. Via setting: :const:`GOOGLE_GTAG_JS_DEFAULT_CONFIG` - which should
be a json serializable dictionary of all default config
options. This value will be used as the default config on all pages.
2. Via the context variable ``google_gtag_js_config_data``, again a
json serializable dictionary.
The resultant config options will be made from the default config
updated with the values of this 'per request' config.

Note re config options:
Provided you find the right key name, you should be able to configure
the gtag tracking however you need it.
You can use :file:`settings.py` options such as::

GOOGLE_GTAG_JS_DEFAULT_CONFIG = {
'anonymize_ip': True,
'send_page_view': False,
'custom_map': {
'dimension<Index>': 'dimension_name',
},
}



You may also like to create a context processor for setting the gtag
'set' and 'config' options per request, that you add to the
:data:`TEMPLATE_CONTEXT_PROCESSORS` list in :file:`settings.py`, eg::

def process_google_gtag_options(request):
google_gtag_options = {}
google_gtag_options['google_gtag_js_set1'] = ('dimension1', request.some_data)
google_gtag_options['google_gtag_js_config_data'] = {
'currency': 'USD',
'country': 'US',
'custom_map': {'metric5': 'avg_page_load_time'},
}
return google_gtag_options



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.


0 comments on commit adee3f7

Please sign in to comment.