From 3db3bf4b0e99a9a1012528e05050605b3da66147 Mon Sep 17 00:00:00 2001 From: Joost Cassee Date: Fri, 28 Jan 2011 18:01:09 +0100 Subject: [PATCH] Simplify analytics tag loading The general-purpose tag loading code was much too generic and complex, so this commit hard-codes the supported services. Also adds a tutorial to the documentation. This commit is not tested. --- analytical/__init__.py | 2 +- analytical/templatetags/analytical.py | 116 +++++++------------ analytical/templatetags/chartbeat.py | 14 +-- analytical/templatetags/clicky.py | 9 +- analytical/templatetags/crazy_egg.py | 9 +- analytical/templatetags/google_analytics.py | 9 +- analytical/templatetags/hubspot.py | 9 +- analytical/templatetags/kiss_insights.py | 7 +- analytical/templatetags/kiss_metrics.py | 9 +- analytical/templatetags/mixpanel.py | 9 +- analytical/templatetags/optimizely.py | 9 +- docs/history.rst | 15 ++- docs/index.rst | 46 +++++--- docs/install.rst | 30 +++-- docs/{services/index.rst => services.rst} | 5 +- docs/services/crazy_egg.rst | 11 +- docs/settings.rst | 20 +--- docs/tutorial.rst | 122 ++++++++++++++++++++ 18 files changed, 269 insertions(+), 182 deletions(-) rename docs/{services/index.rst => services.rst} (72%) create mode 100644 docs/tutorial.rst diff --git a/analytical/__init__.py b/analytical/__init__.py index 444bc47..29462f7 100644 --- a/analytical/__init__.py +++ b/analytical/__init__.py @@ -10,7 +10,7 @@ Django_ project. See the ``docs`` directory for more information. __author__ = "Joost Cassee" __email__ = "joost@cassee.net" -__version__ = "0.1.0" +__version__ = "0.2.0alpha" __copyright__ = "Copyright (C) 2011 Joost Cassee" __license__ = "MIT License" diff --git a/analytical/templatetags/analytical.py b/analytical/templatetags/analytical.py index 7a0e99b..c5a917f 100644 --- a/analytical/templatetags/analytical.py +++ b/analytical/templatetags/analytical.py @@ -1,5 +1,5 @@ """ -Analytical template tags. +Analytical template tags and filters. """ import logging @@ -10,34 +10,46 @@ from django.core.exceptions import ImproperlyConfigured from django.template import Node, TemplateSyntaxError from django.utils.importlib import import_module - -DEFAULT_SERVICES = [ - 'analytical.templatetags.chartbeat.service', - 'analytical.templatetags.clicky.service', - 'analytical.templatetags.crazy_egg.service', - 'analytical.templatetags.google_analytics.service', - 'analytical.templatetags.hubspot.service', - 'analytical.templatetags.kiss_insights.service', - 'analytical.templatetags.kiss_metrics.service', - 'analytical.templatetags.mixpanel.service', - 'analytical.templatetags.optimizely.service', -] -LOCATIONS = ['head_top', 'head_bottom', 'body_top', 'body_bottom'] +from analytical.templatetags import chartbeat, clicky, crazy_egg, \ + google_analytics, hubspot, kiss_insights, kiss_metrics, mixpanel, \ + optimizely -_log = logging.getLogger(__name__) +TAG_NODES = { + 'head_top': [ + chartbeat.ChartbeatTopNode, # Chartbeat should come first + kiss_metrics.KissMetricsNode, + optimizely.OptimizelyNode, + ], + 'head_bottom': [ + google_analytics.GoogleAnalyticsNode, + mixpanel.MixpanelNode, + ], + 'body_top': [ + kiss_insights.KissInsightsNode, + ], + 'body_bottom': [ + clicky.ClickyNode, + crazy_egg.CrazyEggNode, + hubspot.HubSpotNode, + chartbeat.ChartbeatBottomNode, # Chartbeat should come last + ], +} + + +logger = logging.getLogger(__name__) register = template.Library() def _location_tag(location): - def tag(parser, token): + def analytical_tag(parser, token): bits = token.split_contents() if len(bits) > 1: raise TemplateSyntaxError("'%s' tag takes no arguments" % bits[0]) return AnalyticalNode(location) - return tag + return analytical_tag -for loc in LOCATIONS: +for loc in TAG_NODES.keys(): register.tag('analytical_%s' % loc, _location_tag(loc)) @@ -50,64 +62,16 @@ class AnalyticalNode(Node): def _load_template_nodes(): - try: - service_paths = settings.ANALYTICAL_SERVICES - autoload = False - except AttributeError: - service_paths = DEFAULT_SERVICES - autoload = True - services = _get_services(service_paths) - location_nodes = dict((loc, []) for loc in LOCATIONS) - for location in LOCATIONS: - node_tuples = [] - for service in services: - node_tuple = service.get(location) - if node_tuple is not None: - if not isinstance(node_tuple, tuple): - node_tuple = (node_tuple, None) - node_tuples[location].append(node_tuple) - location_nodes[location] = _get_nodes(node_tuples, autoload) + location_nodes = {} + for location, node_classes in TAG_NODES.items(): + location_nodes[location] = [] + for node_class in node_classes: + try: + node = node_class() + except ImproperlyConfigured, e: + logger.debug("not loading analytical service '%s': %s", + node_class.name, e) + location_nodes.append(node) return location_nodes -def _get_nodes(node_tuples, autoload): - nodes = [] - node_sort_key = lambda n: {'first': -1, None: 0, 'last': 1}[n[1]] - for node_tuple in sorted(node_tuples, key=node_sort_key): - node_cls = node_tuple[0] - try: - nodes.append(node_cls()) - except ImproperlyConfigured, e: - if autoload: - _log.debug("not loading analytical service '%s': %s", - node_cls.__module__, e) - continue - else: - raise - return nodes - -def _get_services(paths, autoload): - services = [] - for path in paths: - mod_name, attr_name = path.rsplit('.', 1) - try: - mod = import_module(mod_name) - except ImportError, e: - if autoload: - _log.exception(e) - continue - else: - raise - try: - service = getattr(mod, attr_name) - except AttributeError, e: - if autoload: - _log.debug("not loading analytical service '%s': " - "module '%s' does not provide attribute '%s'", - path, mod_name, attr_name) - continue - else: - raise - services.append(service) - return services - template_nodes = _load_template_nodes() diff --git a/analytical/templatetags/chartbeat.py b/analytical/templatetags/chartbeat.py index f28df71..c1b00bd 100644 --- a/analytical/templatetags/chartbeat.py +++ b/analytical/templatetags/chartbeat.py @@ -56,9 +56,11 @@ def chartbeat_top(parser, token): return ChartbeatTopNode() class ChartbeatTopNode(Node): + name = 'Chartbeat top code' + def render(self, context): if is_internal_ip(context): - return disable_html(INIT_CODE, 'Chartbeat') + return disable_html(INIT_CODE, self.name) return INIT_CODE @@ -77,6 +79,8 @@ def chartbeat_bottom(parser, token): return ChartbeatBottomNode() class ChartbeatBottomNode(Node): + name = 'Chartbeat bottom code' + def __init__(self): self.user_id = self.get_required_setting( 'CHARTBEAT_USER_ID', USER_ID_RE, @@ -94,11 +98,5 @@ class ChartbeatBottomNode(Node): config['domain'] = domain html = SETUP_CODE % {'config': simplejson.dumps(config)} if is_internal_ip(context): - html = disable_html(html, 'Chartbeat') + html = disable_html(html, self.name) return html - - -service = { - 'head_top': (ChartbeatTopNode, 'first'), - 'body_bottom': (ChartbeatBottomNode, 'last'), -} diff --git a/analytical/templatetags/clicky.py b/analytical/templatetags/clicky.py index cd0b969..a4e86b2 100644 --- a/analytical/templatetags/clicky.py +++ b/analytical/templatetags/clicky.py @@ -49,6 +49,8 @@ def clicky(parser, token): return ClickyNode() class ClickyNode(Node): + name = 'Clicky' + def __init__(self): self.site_id = get_required_setting('CLICKY_SITE_ID', SITE_ID_RE, "must be a string containing an eight-digit number") @@ -66,10 +68,5 @@ class ClickyNode(Node): html = TRACKING_CODE % {'site_id': self.site_id, 'custom': simplejson.dumps(custom)} if is_internal_ip(context): - html = disable_html(html, 'Clicky') + html = disable_html(html, self.name) return html - - -service = { - 'body_bottom': ClickyNode, -} diff --git a/analytical/templatetags/crazy_egg.py b/analytical/templatetags/crazy_egg.py index 269fa86..5e19a61 100644 --- a/analytical/templatetags/crazy_egg.py +++ b/analytical/templatetags/crazy_egg.py @@ -34,6 +34,8 @@ def crazy_egg(parser, token): return CrazyEggNode() class CrazyEggNode(Node): + name = 'Crazy Egg' + def __init__(self): self.account_nr = self.get_required_setting('CRAZY_EGG_ACCOUNT_NUMBER', ACCOUNT_NUMBER_RE, @@ -50,10 +52,5 @@ class CrazyEggNode(Node): html = '%s\n' \ % (html, js) if is_internal_ip(context): - html = disable_html(html, 'Crazy Egg') + html = disable_html(html, self.name) return html - - -service = { - 'body_bottom': CrazyEggNode, -} diff --git a/analytical/templatetags/google_analytics.py b/analytical/templatetags/google_analytics.py index c062ce7..f2490d8 100644 --- a/analytical/templatetags/google_analytics.py +++ b/analytical/templatetags/google_analytics.py @@ -52,6 +52,8 @@ def google_analytics(parser, token): return GoogleAnalyticsNode() class GoogleAnalyticsNode(Node): + name = 'Google Analytics' + def __init__(self): self.property_id = self.get_required_setting( 'GOOGLE_ANALYTICS_PROPERTY_ID', PROPERTY_ID_RE, @@ -62,7 +64,7 @@ class GoogleAnalyticsNode(Node): html = SETUP_CODE % {'property_id': self.property_id, 'commands': " ".join(commands)} if is_internal_ip(context): - html = disable_html(html, 'Google Analytics') + html = disable_html(html, self.name) return html def _get_custom_var_commands(self, context): @@ -79,8 +81,3 @@ class GoogleAnalyticsNode(Node): scope = SCOPE_PAGE commands.append(CUSTOM_VAR_CODE % locals()) return commands - - -service = { - 'head_bottom': GoogleAnalyticsNode, -} diff --git a/analytical/templatetags/hubspot.py b/analytical/templatetags/hubspot.py index 8c1b0d9..5284943 100644 --- a/analytical/templatetags/hubspot.py +++ b/analytical/templatetags/hubspot.py @@ -41,6 +41,8 @@ def hubspot(parser, token): return HubSpotNode() class HubSpotNode(Node): + name = 'HubSpot' + def __init__(self): self.site_id = get_required_setting('HUPSPOT_PORTAL_ID', PORTAL_ID_RE, "must be a (string containing a) number") @@ -51,10 +53,5 @@ class HubSpotNode(Node): html = TRACKING_CODE % {'portal_id': self.portal_id, 'domain': self.domain} if is_internal_ip(context): - html = disable_html(html, 'HubSpot') + html = disable_html(html, self.name) return html - - -service = { - 'body_bottom': HubSpotNode, -} diff --git a/analytical/templatetags/kiss_insights.py b/analytical/templatetags/kiss_insights.py index eafe1ac..7f68d9f 100644 --- a/analytical/templatetags/kiss_insights.py +++ b/analytical/templatetags/kiss_insights.py @@ -39,6 +39,8 @@ def kiss_insights(parser, token): return KissInsightsNode() class KissInsightsNode(Node): + name = 'KISSinsights' + def __init__(self): self.account_number = self.get_required_setting( 'KISS_INSIGHTS_ACCOUNT_NUMBER', ACCOUNT_NUMBER_RE, @@ -59,8 +61,3 @@ class KissInsightsNode(Node): html = SETUP_CODE % {'account_number': self.account_number, 'site_code': self.site_code, 'commands': " ".join(commands)} return html - - -service = { - 'body_top': KissInsightsNode, -} diff --git a/analytical/templatetags/kiss_metrics.py b/analytical/templatetags/kiss_metrics.py index e3a1c50..61ce903 100644 --- a/analytical/templatetags/kiss_metrics.py +++ b/analytical/templatetags/kiss_metrics.py @@ -53,6 +53,8 @@ def kiss_metrics(parser, token): return KissMetricsNode() class KissMetricsNode(Node): + name = 'KISSmetrics' + def __init__(self): self.api_key = self.get_required_setting('KISS_METRICS_API_KEY', API_KEY_RE, @@ -72,10 +74,5 @@ class KissMetricsNode(Node): html = TRACKING_CODE % {'api_key': self.api_key, 'commands': " ".join(commands)} if is_internal_ip(context): - html = disable_html(html, 'Mixpanel') + html = disable_html(html, self.name) return html - - -service = { - 'head_top': KissMetricsNode, -} diff --git a/analytical/templatetags/mixpanel.py b/analytical/templatetags/mixpanel.py index 89adc80..36daa5c 100644 --- a/analytical/templatetags/mixpanel.py +++ b/analytical/templatetags/mixpanel.py @@ -46,6 +46,8 @@ def mixpanel(parser, token): return MixpanelNode() class MixpanelNode(Node): + name = 'Mixpanel' + def __init__(self): self.token = self.get_required_setting('MIXPANEL_TOKEN', MIXPANEL_TOKEN_RE, @@ -65,10 +67,5 @@ class MixpanelNode(Node): html = TRACKING_CODE % {'token': self.token, 'commands': " ".join(commands)} if is_internal_ip(context): - html = disable_html(html, 'Mixpanel') + html = disable_html(html, self.name) return html - - -service = { - 'head_bottom': MixpanelNode, -} diff --git a/analytical/templatetags/optimizely.py b/analytical/templatetags/optimizely.py index 92f9566..ea91f81 100644 --- a/analytical/templatetags/optimizely.py +++ b/analytical/templatetags/optimizely.py @@ -33,6 +33,8 @@ def optimizely(parser, token): return OptimizelyNode() class OptimizelyNode(Node): + name = 'Optimizely' + def __init__(self): self.account_number = self.get_required_setting( 'OPTIMIZELY_ACCOUNT_NUMBER', ACCOUNT_NUMBER_RE, @@ -41,10 +43,5 @@ class OptimizelyNode(Node): def render(self, context): html = SETUP_CODE % {'account_number': self.account_number} if is_internal_ip(context): - html = disable_html(html, 'Optimizely') + html = disable_html(html, self.name) return html - - -service = { - 'head_top': OptimizelyNode, -} diff --git a/docs/history.rst b/docs/history.rst index 2797554..82f444e 100644 --- a/docs/history.rst +++ b/docs/history.rst @@ -1,17 +1,24 @@ +=================== History and credits =================== Changelog ---------- +========= + +0.2.0 +----- +* Added support for the HubSpot service. +* Added template tags for individual services. 0.1.0 - First project release. +----- +* First project release. Credits -------- +======= django-analytical was written by `Joost Cassee`_. The project source -code is hosted generously hosted by GitHub_. +code is generously hosted by GitHub_. This application was inspired by and uses ideas from Analytical_, Joshua Krall's all-purpose analytics front-end for Rails. The work on diff --git a/docs/index.rst b/docs/index.rst index 388ca4c..81b30ef 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -16,20 +16,18 @@ Overview If your want to integrating an analytics service into a Django project, you need to add Javascript tracking code to the project templates. -Unfortunately, every services has its own specific installation -instructions. Furthermore, you need to specify your unique identifiers -which would end up in templates. This application hides the details of -the different analytics services behind a generic interface. It is -designed to make the common case easy while allowing advanced users to -customize tracking. +Of course, every service has its own specific installation instructions. +Furthermore, you need to include your unique identifiers, which then end +up in the templates. This application hides the details of the +different analytics services behind a generic interface, and keeps +personal information and configuration out of the templates. Its goal +is to make basic usage set-up very simple, while allowing advanced users +to customize tracking. Each service is set-up as recommended by the +services themselves, using an asynchronous version of the Javascript +code if possible. -The application provides four generic template tags that are added to -the top and bottom of the head and body section of the base template. -Configured services will be enabled automatically by adding Javascript -code at these locations. The installation will follow the -recommendations from the analytics services, using an asynchronous -version of the code if possible. See :doc:`services/index` for detailed -information about each individual analytics service. +To get a feel of how django-analytics works, first check out the +:doc:`tutorial`. Contents @@ -38,7 +36,27 @@ Contents .. toctree:: :maxdepth: 2 + tutorial install - services/index + services settings history + + +Helping out +=========== + +If you want to help out with development of django-analytical, by +posting detailed bug reports, suggesting new features or other analytics +services to support, or doing some development work yourself, please use +the `GitHub project page`_: + +* Use the `issue tracker`_ to discuss bugs and features. +* If you want to do the work yourself, great! Clone the repository, make + changes and send a pull request. Please create a new issue first so + that we can discuss it, and keep people from stepping on each others + toes. +* Of course, you can always send ideas and patches to joost@cassee.net. + +.. _`GitHub project page`: http://github.com/jcassee/django-analytical +.. _`issue tracker`: http://github.com/jcassee/django-analytical/issues diff --git a/docs/install.rst b/docs/install.rst index cd0d178..20bd3dd 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -4,8 +4,8 @@ Installation and configuration Integration of your analytics service is very simple. There are four steps: installing the package, adding it to the list of installed Django -applications, adding the template tags to your base template, and adding -the identifiers for the services you use to the project settings. +applications, adding the template tags to your base template, and +configuring the services you use in the project settings. #. `Installing the Python package`_ #. `Installing the Django application`_ @@ -13,6 +13,8 @@ the identifiers for the services you use to the project settings. #. `Configuring the application`_ +.. _installing-the-package: + Installing the Python package ============================= @@ -31,16 +33,18 @@ get the development code:: .. _PyPI: http://pypi.python.org/pypi/django-analytical/ .. _GitHub: http://github.com/jcassee/django-analytical -Then install by running the setup script:: +Then install the package by running the setup script:: $ cd django-analytical $ python setup.py install +.. _installing-the-application: + Installing the Django application ================================= -After you install django-analytical, add the ``analytical`` Django +After you installed django-analytical, add the ``analytical`` Django application to the list of installed applications in the ``settings.py`` file of your project:: @@ -51,14 +55,16 @@ file of your project:: ] +.. _adding-the-template-tags: + Adding the template tags to the base template ============================================= -Because every analytics service has uses own specific Javascript code -that should be added to the top or bottom of either the head or body -of every HTML page, the django-analytical provides four general-purpose -tags that will render the code needed for the services you are using. -Your base template should look like this:: +Because every analytics service uses own specific Javascript code that +should be added to the top or bottom of either the head or body of the +HTML page, django-analytical provides four general-purpose template tags +that will render the code needed for the services you are using. Your +base template should look like this:: {% load analytical %} @@ -79,6 +85,12 @@ Your base template should look like this:: +Instead of using the general-purpose tags, you can also just use the +tags for the analytics service(s) you are using. See :ref:`services` +for documentation on using individual services. + + +.. _configuration: Configuring the application =========================== diff --git a/docs/services/index.rst b/docs/services.rst similarity index 72% rename from docs/services/index.rst rename to docs/services.rst index 1b2de4f..9df77f9 100644 --- a/docs/services/index.rst +++ b/docs/services.rst @@ -1,3 +1,6 @@ +.. _services: + +======== Services ======== @@ -7,4 +10,4 @@ A number of analytics services is supported. :maxdepth: 1 :glob: - * + services/* diff --git a/docs/services/crazy_egg.rst b/docs/services/crazy_egg.rst index 5ebadca..b8e4c4b 100644 --- a/docs/services/crazy_egg.rst +++ b/docs/services/crazy_egg.rst @@ -99,11 +99,12 @@ 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`:: - def segment_on_ip_proto(request): - addr = request.META.get('HTTP_X_FORWARDED_FOR', - request.META.get('REMOTE_ADDR', '')) - proto = 'ipv6' if ':' in addr else 'ipv4' - return {'crazy_egg_var3': proto} + def track_admin_role(request): + if request.user.is_staff(): + role = 'staff' + else: + role = 'visitor' + return {'crazy_egg_var3': role} Just remember that if you set the same context variable in the :class:`~django.template.context.RequestContext` constructor and in a diff --git a/docs/settings.rst b/docs/settings.rst index febdf96..25e4a96 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -1,3 +1,5 @@ +.. _identifying-visitors: + ======== Settings ======== @@ -6,6 +8,7 @@ Here's a full list of all available settings, in alphabetical order, and their default values. + .. data:: ANALYTICAL_AUTO_IDENTIFY Default: ``True`` @@ -50,20 +53,3 @@ their default values. ``'django.core.context_processors.request'`` to the list of context processors in the ``TEMPLATE_CONTEXT_PROCESSORS`` setting. - - -.. data:: ANALYTICAL_SERVICES - - Default: all included services that have been configured correctly - - A list or tuple of analytics services to use. If this setting is - used and one of the services is not configured correctly, an - :exc:`ImproperlyConfigured` exception is raised when the services - are first loaded. - - Example:: - - ANALYTICAL_SERVICES = [ - 'analytical.services.crazy_egg.CrazyEggService', - 'analytical.services.google_analytics.GoogleAnalyticsService', - ] diff --git a/docs/tutorial.rst b/docs/tutorial.rst new file mode 100644 index 0000000..a98ab5c --- /dev/null +++ b/docs/tutorial.rst @@ -0,0 +1,122 @@ +.. _tutorial: + +======== +Tutorial +======== + +In this tutorial you will learn how to install and configure +django-analytical for basic tracking. Suppose you want to use two +different analytics services on your Django website: + +* :doc:`Clicky ` for detailed traffic analysis +* :doc:`Crazy Egg ` to see where visitors click on your pages + +At the end of this tutorial, the project will track visitors on both +Clicky and Crazy Egg, identify authenticated users and add extra +tracking data to segment mouse clicks on Crazy Egg based on whether +visitors are using IPv4 or IPv6. + + +Installation +============ + +To get started with django-analytical, the package must first be +installed. You can download and install the latest stable package from +the Python Package Index automatically by using ``easy_install``:: + + $ easy_install django-analytical + +For more ways to install django-analytical, see +:ref:`installing-the-package`. + +After you install django-analytical, you need to add it to the list of +installed applications in the ``settings.py`` file of your project:: + + INSTALLED_APPS = [ + ... + 'analytical', + ... + ] + +Now add the general-purpose django-analytical template tags to your base +template:: + + {% load analytical %} + + + + {% analytical_head_top %} + + ... + + {% analytical_head_bottom %} + + + {% analytical_body_top %} + + ... + + {% analytical_body_bottom %} + + + +Finally, you need to configure the Clicky Site ID and the Crazy Egg +account number. Add the following to your project :file:`settings.py` +file:: + + CLICKY_SITE_ID = 'xxxxxxxx' + CRAZY_EGG_ACCOUNT_NUMBER = 'xxxxxxxx' + +The analytics services are now installed. If you run Django with these +changes, both Clicky and Crazy Egg will be tracking your visitors. + + +Identifying authenticated users +=============================== + +Some analytics services, such as Clicky, can identify and track +individual visitors. If django-analytical tags detect that the current +user is authenticated, they will automatically include code to send the +username to services that support this feature. This only works if the +template context has the current user in the ``user`` or +``request.user`` context variable. If you use a +:class:`~django.template.RequestContext` to render templates (which is +recommended anyway) and have the +:class:`django.contrib.auth.context_processors.auth` context processor +in the :data:`TEMPLATE_CONTEXT_PROCESSORS` setting (which is default), +then this identification works without having to make any changes. + +For more detailed information on automatic identification, and how to +disable or override it, see :ref:`identifying-visitors`. + + +Adding custom tracking data +=========================== + +You want to track whether visitors are using IPv4 or IPv6. (Maybe you +are running a website on the IPv6 transition?) This means including +the visitor IP protocol version as custom data with the tracking code. +The easiest way to do this is by using a context processor:: + + def track_ip_proto(request): + addr = request.META.get('HTTP_X_FORWARDED_FOR', '') + if not addr: + addr = request.META.get('REMOTE_ADDR', '') + if ':' in addr: + proto = 'ipv6' + else: + proto = 'ipv4' # assume IPv4 if no information + return {'crazy_egg_var1': proto} + +Use a :class:`~django.template.RequestContext` when rendering templates +and add the ``'track_ip_proto'`` to :data:`TEMPLATE_CONTEXT_PROCESSORS`. +In Crazy Egg, you can now select *User Var1* in the overlay or confetti +views to see whether visitors using IPv4 behave differently from those +using IPv6. + + +---- + +This concludes the tutorial. For information about setting up, +configuring and customizing the different analytics services, see +:ref:`services`.