Compare commits

..

No commits in common. "main" and "v0.1.0" have entirely different histories.
main ... v0.1.0

149 changed files with 1908 additions and 11452 deletions

View file

@ -1,35 +0,0 @@
name: Check
on:
pull_request:
branches:
- main
push:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
env:
- lint
- format
- audit
- package
- docs
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install prerequisites
run: python -m pip install tox
- name: Run ${{ matrix.env }}
run: tox -e ${{ matrix.env }}

View file

@ -1,44 +0,0 @@
name: Release
on:
push:
tags:
- '*'
env:
PIP_DISABLE_PIP_VERSION_CHECK: '1'
PY_COLORS: '1'
jobs:
build:
if: github.repository == 'jazzband/django-analytical'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: pip
cache-dependency-path: |
**/pyproject.toml
- name: Install dependencies
run: |
python -m pip install tox
- name: Build package
run: |
tox -e package
- name: Upload packages to Jazzband
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
uses: pypa/gh-action-pypi-publish@release/v1
with:
user: jazzband
password: ${{ secrets.JAZZBAND_RELEASE_KEY }}
repository_url: https://jazzband.co/projects/django-analytical/upload

View file

@ -1,58 +0,0 @@
name: Test
on:
pull_request:
branches:
- main
push:
branches:
- main
env:
PIP_DISABLE_PIP_VERSION_CHECK: '1'
PY_COLORS: '1'
jobs:
python-django:
runs-on: ubuntu-latest
strategy:
max-parallel: 5
matrix:
python-version:
- '3.10'
- '3.11'
- '3.12'
- '3.13'
django-version:
- '4.2'
- '5.1'
- '5.2'
include:
- { python-version: '3.9', django-version: '4.2' }
exclude:
- { python-version: '3.13', django-version: '4.2' }
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: pip
cache-dependency-path: |
**/pyproject.toml
- name: Install dependencies
run: |
python -m pip install tox tox-gh-actions
- name: Tox tests (Python ${{ matrix.python-version }}, Django ${{ matrix.django-version }})
run: tox
env:
DJANGO: ${{ matrix.django-version }}
- name: Upload coverage
uses: codecov/codecov-action@v5
with:
name: Python ${{ matrix.python-version }}

22
.gitignore vendored
View file

@ -1,23 +1,9 @@
.*.sw?
/*.geany
/.idea
/.tox
/.vscode
*.pyc
*.pyo
/.coverage
/coverage.xml
/tests/*-report.json
/tests/*-report.xml
/.*
!/.gitignore
/build
/dist
/docs/_build
/docs/_templates/layout.html
/MANIFEST
*.egg-info
/requirements.txt
/uv.lock
*.pyc
*.pyo

View file

@ -1 +0,0 @@
repos: []

View file

@ -1,18 +0,0 @@
# Read the Docs configuration file
# https://docs.readthedocs.io/en/stable/config-file/v2.html
version: 2
build:
os: ubuntu-24.04
tools:
python: "3.12"
sphinx:
configuration: docs/conf.py
# We recommend specifying your dependencies to enable reproducible builds:
# https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
# python:
# install:
# - requirements: docs/requirements.txt

View file

@ -1,238 +0,0 @@
(unreleased)
------------
* Fix GA gtag user_id setup and add support for custom dimensions (Erick Massip)
* Change spelling of "JavaScript" across all files in docstrings and docs
(Peter Bittner)
* Ask site visitors for consent when using Matomo (Julian Haluska & Ronard Luna)
Version 3.2.0
-------------
* Remove deprecated Piwik integration. Use Matomo instead! (Peter Bittner)
* Migrate packaging from setup.py to pyproject.toml with Ruff for linting
and formatting (Peter Bittner)
* Remove obsolete type attribute in script tags for JavaScript (Peter Bittner)
* Drop the end-of-life Python 3.8 as required by changed semantics of the
license metadata field (Peter Bittner)
* Remove AUTHORS file to avoid confusion; this is now metadata maintained
in pyproject.toml (Peter Bittner)
* Add more configuration options for Woopra (Peter Bittner)
Version 3.1.0
-------------
* Rename default branch from master to main (Peter Bittner, Jannis Leidel)
* Modernize packaging setup, add pyproject.toml (Peter Bittner)
* Integrate isort, reorganize imports (David Smith)
* Refactor test suite from Python unit tests to Pytest (David Smith)
* Add Heap integration (Garrett Coakley)
* Drop Django 3.1, cover Django 4.0 and Python 3.10 in test suite (David Smith)
Version 3.0.0
-------------
* Add support for Lucky Orange (Peter Bittner)
* Add missing instructions in Installation chapter of the docs (Peter Bittner)
* Migrate test setup to Pytest (David Smith, Peter Bittner, Pi Delport)
* Support Django 3.1 and Python 3.9, drop Django 1.11 and Python 2.7/3.5 (David Smith)
* Migrate from Travis CI to GitHub Actions (Jannis Leidel)
* Update accepted patterns (regex) for Google Analytics GTag (Taha Rushain)
* Scope Piwik warning to use of Piwik (Hugo Barrera)
* Add ``user_id`` to Google Analytics GTag (Sean Wallace)
Version 2.6.0
-------------
* Support Django 3.0 and Python 3.8, drop Django 2.1
* Add support for Google Analytics Tag Manager (Marc Bourqui)
* Add Matomo, the renamed version of Piwik (Scott Karlin)
* Move Joost's project over to the Jazzband
Version 2.5.0
-------------
* Add support for Google analytics.js (Marc Bourqui)
* Add support for Intercom HMAC identity verification (Pi Delport)
* Add support for Hotjar (Pi Delport)
* Make sure _trackPageview happens before other settings in Google Analytics
(Diederik van der Boor)
Version 2.4.0
-------------
* Support Django 2.0 (Matthäus G. Chajdas)
Version 2.3.0
-------------
* Add Facebook Pixel support (Pi Delport)
* Add Python 3.6 and Django 1.10 & 1.11 tests (Pi Delport)
* Drop Python 3.2 support
Version 2.2.2
-------------
* Allow port in Piwik domain path. (Alex Ramsay)
Version 2.2.1
-------------
* Fix a bug with the extra Google Analytics variables also pushing the `_gat.`
flag onto the configuration array.
Version 2.2.0
-------------
* Update Woopra JavaScript snippet (Aleck Landgraf)
Version 2.1.0
-------------
* Support Rating\@mail.ru (Nikolay Korotkiy)
* Support Yandex.Metrica (Nikolay Korotkiy)
* Add support for extra Google Analytics variables (Steve Schwarz)
* Remove support for Reinvigorate (service shut down)
Version 2.0.0
-------------
* Support Django 1.9, drop support for Django < 1.7 (Hugo Osvaldo Barrera)
* Support custom user models with an alternative username field (Brad Pitcher)
Version 1.0.0
-------------
* Add Piwik user variables support (Alexandre Pocquet)
Version 0.22.0
--------------
* Mark package as Python 3 compatible (Martín Gaitán)
* Fix Clickmap tracker id regular expression
* Test with Django 1.8
Version 0.21.0
--------------
* Added compatibility with Python 3 (Eric Amador)
Version 0.20.0
--------------
* Support Django 1.7 (Craig Bruce)
* Update Mixpanel identity code (Martín Gaitán)
* Identify authenticated users in Uservoice (Martín Gaitán)
* Add full name and email to Olark (Scott Adams)
Version 0.19.0
--------------
* Add Piwik integration (Peter Bittner)
Version 0.18.0
--------------
* Update HubSpot code (Craig Bruce)
Version 0.17.1
--------------
* Fix typo in Intercom.io support (Steven Skoczen)
Version 0.17.0
--------------
* Update UserVoice support (Martín Gaitán)
* Add support for Intercom.io (Steven Skoczen)
Version 0.16.0
--------------
* Add support for GA Display Advertising features (Max Arnold)
Version 0.15.0
--------------
* Add IP anonymization setting to GA tracking pixel (Tinnet Coronam)
* Include Django 1.5 in tox.ini (Tinnet Coronam)
* Add Clickmap integration (Philippe O. Wagner)
Version 0.14.0
--------------
* Update mixpanel integration to latest code (Simon Ye)
Version 0.13.0
--------------
* Add support for the KISSmetrics alias feature (Sandra Mau)
* Update testing code for Django 1.4 (Pi Delport)
Version 0.12.0
--------------
* Add support for the UserVoice service.
Version 0.11.3
--------------
* Added support for Gaug.es (Steven Skoczen)
Version 0.11.2
--------------
* Fix Spring Metrics custom variables.
* Update Spring Metrics documentation.
Version 0.11.1
--------------
* Fix Woopra for anonymous users (Steven Skoczen).
Version 0.11.0
--------------
* Added support for the Spring Metrics service.
* Allow sending events and properties to KISSmetrics (Paul Oswald).
* Add support for the Site Speed report in Google Analytics (Uros
Trebec).
Version 0.10.0
--------------
* Added multiple domains support for Google Analytics.
* Fixed bug in deleted settings testing code (Eric Davis).
Version 0.9.2
-------------
* Added support for the SnapEngage service.
* Updated Mixpanel code (Julien Grenier).
Version 0.9.1
-------------
* Fixed compatibility with Python 2.5 (Iván Raskovsky).
Version 0.9.0
-------------
* Updated Clicky tracking code to support multiple site ids.
* Fixed Chartbeat auto-domain bug when the Sites framework is not used
(Eric Davis).
* Improved testing code (Eric Davis).
Version 0.8.1
-------------
* Fixed MANIFEST bug that caused GoSquared support to be missing from
the source distribution.
Version 0.8.0
-------------
* Added support for the GoSquared service.
* Updated Clicky tracking code to use relative URLs.
Version 0.7.0
-------------
* Added support for the Woopra service.
* Added chat window text customization to Olark.
* Renamed ``MIXPANEL_TOKEN`` setting to ``MIXPANEL_API_TOKEN`` for
compatibility with Wes Winham's mixpanel-celery_ package.
* Fixed the ``<script>`` tag for Crazy Egg.
.. _mixpanel-celery: https://github.com/winhamwr/mixpanel-celery
Version 0.6.0
-------------
* Added support for the Reinvigorate service.
* Added support for the Olark service.
Version 0.5.0
-------------
* Split off Geckoboard support into django-geckoboard_.
.. _django-geckoboard: https://pypi.org/project/django-geckoboard
Version 0.4.0
-------------
* Added support for the Geckoboard service.
Version 0.3.0
-------------
* Added support for the Performable service.
Version 0.2.0
-------------
* Added support for the HubSpot service.
* Added template tags for individual services.
Version 0.1.0
-------------
* First project release.

View file

@ -1,46 +0,0 @@
# Code of Conduct
As contributors and maintainers of the Jazzband projects, and in the interest of
fostering an open and welcoming community, we pledge to respect all people who
contribute through reporting issues, posting feature requests, updating documentation,
submitting pull requests or patches, and other activities.
We are committed to making participation in the Jazzband a harassment-free experience
for everyone, regardless of the level of experience, gender, gender identity and
expression, sexual orientation, disability, personal appearance, body size, race,
ethnicity, age, religion, or nationality.
Examples of unacceptable behavior by participants include:
- The use of sexualized language or imagery
- Personal attacks
- Trolling or insulting/derogatory comments
- Public or private harassment
- Publishing other's private information, such as physical or electronic addresses,
without explicit permission
- Other unethical or unprofessional conduct
The Jazzband roadies have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are not
aligned to this Code of Conduct, or to ban temporarily or permanently any contributor
for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
By adopting this Code of Conduct, the roadies commit themselves to fairly and
consistently applying these principles to every aspect of managing the jazzband
projects. Roadies who do not follow or enforce the Code of Conduct may be permanently
removed from the Jazzband roadies.
This code of conduct applies both within project spaces and in public spaces when an
individual is representing the project or its community.
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by
contacting the roadies at `roadies@jazzband.co`. All complaints will be reviewed and
investigated and will result in a response that is deemed necessary and appropriate to
the circumstances. Roadies are obligated to maintain confidentiality with regard to the
reporter of an incident.
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version
1.3.0, available at [https://contributor-covenant.org/version/1/3/0/][version]
[homepage]: https://contributor-covenant.org
[version]: https://contributor-covenant.org/version/1/3/0/

View file

@ -1,5 +0,0 @@
.. image:: https://jazzband.co/static/img/jazzband.svg
:target: https://jazzband.co/
:alt: Jazzband
This is a `Jazzband <https://jazzband.co>`_ project. By contributing you agree to abide by the `Contributor Code of Conduct <https://jazzband.co/about/conduct>`_ and follow the `guidelines <https://jazzband.co/about/guidelines>`_.

View file

@ -1,4 +1,4 @@
Copyright (C) 2011-2019 Joost Cassee and contributors
Copyright (C) 2011 Joost Cassee
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View file

@ -1,3 +1 @@
include LICENSE.txt *.rst
recursive-include docs *.rst *.py
recursive-include tests *.py
include LICENSE.txt

View file

@ -1,141 +1,38 @@
django-analytical |latest-version|
==================================
|build-status| |coverage| |python-support| |license| |jazzband|
The django-analytical application integrates analytics services into a
Django_ project.
.. start docs include
Using an analytics service with a Django project means adding JavaScript
tracking code to the project templates. 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.
Not very nice.
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 the basic
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.
.. end docs include
.. |latest-version| image:: https://img.shields.io/pypi/v/django-analytical.svg
:alt: Latest version on PyPI
:target: https://pypi.org/project/django-analytical/
.. |build-status| image:: https://github.com/jazzband/django-analytical/workflows/Test/badge.svg
:target: https://github.com/jazzband/django-analytical/actions
:alt: GitHub Actions
.. |coverage| image:: https://codecov.io/gh/jazzband/django-analytical/branch/main/graph/badge.svg
:alt: Test coverage
:target: https://codecov.io/gh/jazzband/django-analytical
.. |python-support| image:: https://img.shields.io/pypi/pyversions/django-analytical.svg
: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/main/LICENSE.txt
.. |jazzband| image:: https://jazzband.co/static/img/badge.svg
:alt: Jazzband
:target: https://jazzband.co/projects/django-analytical
.. _`Django`: http://www.djangoproject.com/
Currently Supported Services
----------------------------
* `Chartbeat`_ traffic analysis
* `Clickmap`_ visual click tracking
* `Clicky`_ traffic analysis
* `Crazy Egg`_ visual click tracking
* `Facebook Pixel`_ advertising analytics
* `Gaug.es`_ real time web analytics
* `Google Analytics`_ traffic analysis
* `GoSquared`_ traffic monitoring
* `Heap`_ analytics and events tracking
* `Hotjar`_ analytics and user feedback
* `HubSpot`_ inbound marketing
* `Intercom`_ live chat and support
* `KISSinsights`_ feedback surveys
* `KISSmetrics`_ funnel analysis
* `Lucky Orange`_ analytics and user feedback
* `Mixpanel`_ event tracking
* `Olark`_ visitor chat
* `Optimizely`_ A/B testing
* `Performable`_ web analytics and landing pages
* `Matomo (formerly Piwik)`_ open source web analytics
* `Rating\@Mail.ru`_ web analytics
* `SnapEngage`_ live chat
* `Spring Metrics`_ conversion tracking
* `UserVoice`_ user feedback and helpdesk
* `Woopra`_ web analytics
* `Yandex.Metrica`_ web analytics
.. _`Chartbeat`: http://www.chartbeat.com/
.. _`Clickmap`: http://clickmap.ch/
.. _`Clicky`: http://getclicky.com/
.. _`Crazy Egg`: http://www.crazyegg.com/
.. _`Facebook Pixel`: https://developers.facebook.com/docs/facebook-pixel/
.. _`Gaug.es`: http://get.gaug.es/
.. _`Google Analytics`: http://www.google.com/analytics/
.. _`GoSquared`: http://www.gosquared.com/
.. _`Heap`: https://heapanalytics.com/
.. _`Hotjar`: https://www.hotjar.com/
.. _`HubSpot`: http://www.hubspot.com/
.. _`Intercom`: http://www.intercom.io/
.. _`KISSinsights`: http://www.kissinsights.com/
.. _`KISSmetrics`: http://www.kissmetrics.com/
.. _`Lucky Orange`: http://www.luckyorange.com/
.. _`Mixpanel`: http://www.mixpanel.com/
.. _`Olark`: http://www.olark.com/
.. _`Optimizely`: http://www.optimizely.com/
.. _`Performable`: http://www.performable.com/
.. _`Matomo (formerly Piwik)`: https://matomo.org
.. _`Rating\@Mail.ru`: http://top.mail.ru/
.. _`SnapEngage`: http://www.snapengage.com/
.. _`Spring Metrics`: http://www.springmetrics.com/
.. _`UserVoice`: http://www.uservoice.com/
.. _`Woopra`: http://www.woopra.com/
.. _`Yandex.Metrica`: http://metrica.yandex.com
Documentation and Support
-------------------------
The documentation can be found in the ``docs`` directory or `read
online`_. The source code and issue tracker are generously `hosted by
GitHub`_.
.. _`read online`: https://django-analytical.readthedocs.io/
.. _`hosted by GitHub`: https://github.com/jazzband/django-analytical
.. _`Gitter chat room`: https://gitter.im/jazzband/django-analytical
How To Contribute
-----------------
.. start contribute include
If you want to help out with the development of django-analytical, by
posting detailed bug reports, proposing new features or other analytics
services to support, or suggesting documentation improvements, use the
`issue tracker`_. If you want to get your hands dirty, great! Clone
the repository, make changes and place a `pull request`_. Creating an
issue to discuss your plans is useful.
At the end, don't forget to add yourself to the `list of authors`_ and
update the `changelog`_ with a short description of your contribution.
We want you to stand out from the crowd as an open source superstar! ✦
This is a `Jazzband`_ project. By contributing you agree to abide by the
`Contributor Code of Conduct`_ and follow the `guidelines`_.
.. _`issue tracker`: https://github.com/jazzband/django-analytical/issues
.. _`pull request`: https://github.com/jazzband/django-analytical/pulls
.. _`list of authors`: https://github.com/jazzband/django-analytical/blob/main/pyproject.toml
.. _`changelog`: https://github.com/jazzband/django-analytical/blob/main/CHANGELOG.rst
.. _`Jazzband`: https://jazzband.co
.. _`Contributor Code of Conduct`: https://jazzband.co/about/conduct
.. _`guidelines`: https://jazzband.co/about/guidelines
.. end contribute include
django-analytical
-----------------
The django-analytical application integrates various analytics services
into a Django_ project. Currently supported services:
* `Chartbeat`_ -- traffic analysis
* `Clicky`_ -- traffic analysis
* `Crazy Egg`_ -- visual click tracking
* `Google Analytics`_ traffic analysis
* `KISSinsights`_ -- feedback surveys
* `KISSmetrics`_ -- funnel analysis
* `Mixpanel`_ -- event tracking
* `Optimizely`_ -- A/B testing
The documentation can be found in the ``docs`` directory or `read
online`_. The source code and issue tracker are `hosted on GitHub`_.
Copyright (C) 2011 Joost Cassee <joost@cassee.net>. This software is
licensed under the MIT License (see LICENSE.txt).
This application was inspired by and uses ideas from Analytical_,
Joshua Krall's all-purpose analytics front-end for Rails. The work on
Crazy Egg was made possible by `Bateau Knowledge`_.
.. _Django: http://www.djangoproject.com/
.. _Chartbeat: http://www.chartbeat.com/
.. _Clicky: http://getclicky.com/
.. _`Crazy Egg`: http://www.crazyegg.com/
.. _`Google Analytics`: http://www.google.com/analytics/
.. _KISSinsights: http://www.kissinsights.com/
.. _KISSmetrics: http://www.kissmetrics.com/
.. _Mixpanel: http://www.mixpanel.com/
.. _Optimizely: http://www.optimizely.com/
.. _`read online`: http://packages.python.org/django-analytical/
.. _`hosted on GitHub`: http://www.github.com/jcassee/django-analytical
.. _Analytical: https://github.com/jkrall/analytical
.. _`Bateau Knowledge`: http://www.bateauknowledge.nl/

View file

@ -1,5 +1,22 @@
"""
Analytics service integration for Django projects.
Analytics service integration for Django
========================================
The django-analytical application integrates analytics services into a
Django_ project. See the ``docs`` directory for more information.
.. _Django: http://www.djangoproject.com/
"""
__version__ = '3.2.0'
__author__ = "Joost Cassee"
__email__ = "joost@cassee.net"
__version__ = "0.1.0"
__copyright__ = "Copyright (C) 2011 Joost Cassee"
__license__ = "MIT License"
try:
from collections import namedtuple
except ImportError:
namedtuple = lambda name, fields: lambda *values: values
Property = namedtuple('Property', ['num', 'name', 'value'])

View file

@ -0,0 +1,33 @@
"""
Context processors for django-analytical.
"""
from django.conf import settings
IMPORT_SETTINGS = [
'ANALYTICAL_INTERNAL_IPS',
'ANALYTICAL_SERVICES',
'CHARTBEAT_USER_ID',
'CLICKY_SITE_ID',
'CRAZY_EGG_ACCOUNT_NUMBER',
'GOOGLE_ANALYTICS_PROPERTY_ID',
'KISS_INSIGHTS_ACCOUNT_NUMBER',
'KISS_INSIGHTS_SITE_CODE',
'KISS_METRICS_API_KEY',
'MIXPANEL_TOKEN',
'OPTIMIZELY_ACCOUNT_NUMBER',
]
def settings(request):
"""
Import all django-analytical settings into the template context.
"""
vars = {}
for setting in IMPORT_SETTINGS:
try:
vars[setting] = getattr(settings, setting)
except AttributeError:
pass
return vars

View file

@ -1,5 +0,0 @@
"""
Models for the django-analytical Django application.
This application currently does not use models.
"""

View file

@ -0,0 +1,66 @@
"""
Analytics services.
"""
import logging
from django.conf import settings
from django.utils.importlib import import_module
from django.core.exceptions import ImproperlyConfigured
_log = logging.getLogger(__name__)
DEFAULT_SERVICES = [
'analytical.services.chartbeat.ChartbeatService',
'analytical.services.clicky.ClickyService',
'analytical.services.crazy_egg.CrazyEggService',
'analytical.services.google_analytics.GoogleAnalyticsService',
'analytical.services.kiss_insights.KissInsightsService',
'analytical.services.kiss_metrics.KissMetricsService',
'analytical.services.mixpanel.MixpanelService',
'analytical.services.optimizely.OptimizelyService',
]
enabled_services = None
def get_enabled_services():
global enabled_services
if enabled_services is None:
enabled_services = load_services()
return enabled_services
def load_services():
enabled_services = []
try:
service_paths = settings.ANALYTICAL_SERVICES
autoload = False
except AttributeError:
service_paths = DEFAULT_SERVICES
autoload = True
for path in service_paths:
try:
service = _load_service(path)
enabled_services.append(service)
except ImproperlyConfigured, e:
if autoload:
_log.debug('not loading analytical service "%s": %s',
path, e)
else:
raise
return enabled_services
def _load_service(path):
module, attr = path.rsplit('.', 1)
try:
mod = import_module(module)
except ImportError, e:
raise ImproperlyConfigured(
'error importing analytical service %s: "%s"' % (module, e))
try:
service = getattr(mod, attr)()
except (AttributeError, TypeError):
raise ImproperlyConfigured(
'module "%s" does not define callable service "%s"'
% (module, attr))
return service

View file

@ -0,0 +1,88 @@
"""
Base analytical service.
"""
from django.core.exceptions import ImproperlyConfigured
from django.conf import settings
HTML_COMMENT = "<!-- %(message):%(sep)s%(html)s%(sep)s-->"
JS_COMMENT = "*/ %(message):%(sep)s%(html)s%(sep)s*/"
IDENTITY_CONTEXT_KEY = 'analytical_identity'
class AnalyticalService(object):
"""
Analytics service.
"""
def render(self, location, context):
func_name = "render_%s" % location
func = getattr(self, func_name)
html = func(context)
return html
def render_head_top(self, context):
return ""
def render_head_bottom(self, context):
return ""
def render_body_top(self, context):
return ""
def render_body_bottom(self, context):
return ""
def render_event(self, name, properties):
return ""
def get_required_setting(self, setting, value_re, invalid_msg):
try:
value = getattr(settings, setting)
except AttributeError:
raise ImproperlyConfigured("%s setting: not found" % setting)
value = str(value)
if not value_re.search(value):
raise ImproperlyConfigured("%s setting: %s: '%s'"
% (setting, invalid_msg, value))
return value
def get_identity(self, context):
try:
return context[IDENTITY_CONTEXT_KEY]
except KeyError:
pass
if getattr(settings, 'ANALYTICAL_AUTO_IDENTIFY', True):
try:
try:
user = context['user']
except KeyError:
request = context['request']
user = request.user
if user.is_authenticated():
return user.username
except (KeyError, AttributeError):
pass
return None
def get_events(self, context):
return context.get('analytical_events', {})
def get_properties(self, context):
return context.get('analytical_properties', {})
def _html_comment(self, html, message=""):
return self._comment(HTML_COMMENT, html, message)
def _js_comment(self, html, message=""):
return self._comment(JS_COMMENT, html, message)
def _comment(self, format, html, message):
if not message:
message = "Disabled"
if message.find('\n') > -1:
sep = '\n'
else:
sep = ' '
return format % {'message': message, 'html': html, 'sep': sep}

View file

@ -0,0 +1,57 @@
"""
Chartbeat service.
"""
import re
from django.contrib.sites.models import Site, RequestSite
from django.core.exceptions import ImproperlyConfigured
from django.utils import simplejson
from analytical.services.base import AnalyticalService
USER_ID_RE = re.compile(r'^\d{5}$')
INIT_CODE = """<script type="text/javascript">var _sf_startpt=(new Date()).getTime()</script>"""
SETUP_CODE = """
<script type="text/javascript">
var _sf_async_config=%(config)s;
(function(){
function loadChartbeat() {
window._sf_endpt=(new Date()).getTime();
var e = document.createElement('script');
e.setAttribute('language', 'javascript');
e.setAttribute('type', 'text/javascript');
e.setAttribute('src',
(("https:" == document.location.protocol) ? "https://a248.e.akamai.net/chartbeat.download.akamai.com/102508/" : "http://static.chartbeat.com/") +
"js/chartbeat.js");
document.body.appendChild(e);
}
var oldonload = window.onload;
window.onload = (typeof window.onload != 'function') ?
loadChartbeat : function() { oldonload(); loadChartbeat(); };
})();
</script>
"""
DOMAIN_CONTEXT_KEY = 'chartbeat_domain'
class ChartbeatService(AnalyticalService):
def __init__(self):
self.user_id = self.get_required_setting(
'CHARTBEAT_USER_ID', USER_ID_RE,
"must be a string containing an five-digit number")
def render_head_top(self, context):
return INIT_CODE
def render_body_bottom(self, context):
config = {'uid': self.user_id}
try:
config['domain'] = context[DOMAIN_CONTEXT_KEY]
except KeyError:
try:
config['domain'] = Site.objects.get_current().domain
except (ImproperlyConfigured, Site.DoesNotExist, AttributeError):
pass
return SETUP_CODE % {'config': simplejson.dumps(config)}

View file

@ -0,0 +1,47 @@
"""
Clicky service.
"""
import re
from django.utils import simplejson
from analytical.services.base import AnalyticalService
SITE_ID_RE = re.compile(r'^\d{8}$')
SETUP_CODE = """
<script type="text/javascript">
var clicky = { log: function(){ return; }, goal: function(){ return; }};
var clicky_site_id = %(site_id)s;
var clicky_custom = %(custom)s;
(function() {
var s = document.createElement('script');
s.type = 'text/javascript';
s.async = true;
s.src = ( document.location.protocol == 'https:' ? 'https://static.getclicky.com/js' : 'http://static.getclicky.com/js' );
( document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0] ).appendChild( s );
})();
</script>
<noscript><p><img alt="Clicky" width="1" height="1" src="http://in.getclicky.com/%(site_id)sns.gif" /></p></noscript>
"""
CUSTOM_CONTEXT_KEY = 'clicky_custom'
class ClickyService(AnalyticalService):
def __init__(self):
self.site_id = self.get_required_setting('CLICKY_SITE_ID', SITE_ID_RE,
"must be a string containing an eight-digit number")
def render_body_bottom(self, context):
custom = {
'session': {
'username': self.get_identity(context),
}
}
custom.update(context.get(CUSTOM_CONTEXT_KEY, {}))
return SETUP_CODE % {'site_id': self.site_id,
'custom': simplejson.dumps(custom)}
def _convert_properties(self):
pass

View file

@ -0,0 +1,42 @@
"""
Console debugging service.
"""
from analytical.services.base import AnalyticalService
DEBUG_CODE = """
<script type="text/javascript">
if(typeof(console) !== 'undefined' && console != null) {
%s
}
</script>
"""
LOG_CODE_ANONYMOUS = """
console.log('Analytical: rendering analytical_%(location)s tag');
"""
LOG_CODE_IDENTIFIED = """
console.log('Analytical: rendering analytical_%(location)s tag for user %(identity)s');
"""
class ConsoleService(AnalyticalService):
def render_head_top(self, context):
return self._render_code('head_top', context)
def render_head_bottom(self, context):
return self._render_code('head_bottom', context)
def render_body_top(self, context):
return self._render_code('body_top', context)
def render_body_bottom(self, context):
return self._render_code('body_bottom', context)
def _render_code(self, location, context):
vars = {'location': location, 'identity': self.get_identity(context)}
if vars['identity'] is None:
debug_code = DEBUG_CODE % LOG_CODE_ANONYMOUS
else:
debug_code = DEBUG_CODE % LOG_CODE_IDENTIFIED
return debug_code % vars

View file

@ -0,0 +1,31 @@
"""
Crazy Egg service.
"""
import re
from analytical.services.base import AnalyticalService
ACCOUNT_NUMBER_RE = re.compile(r'^\d{8}$')
SETUP_CODE = """<script type="text/javascript" src="//dnn506yrbagrg.cloudfront.net/pages/scripts/%(account_nr_1)s/%(account_nr_2)s.js"</script>"""
USERVAR_CODE = "CE2.set(%(varnr)d, '%(value)s');"
USERVAR_CONTEXT_VAR = 'crazy_egg_uservars'
class CrazyEggService(AnalyticalService):
def __init__(self):
self.account_nr = self.get_required_setting('CRAZY_EGG_ACCOUNT_NUMBER',
ACCOUNT_NUMBER_RE,
"must be a string containing an eight-digit number")
def render_body_bottom(self, context):
html = SETUP_CODE % {'account_nr_1': self.account_nr[:4],
'account_nr_2': self.account_nr[4:]}
uservars = context.get(USERVAR_CONTEXT_VAR, {})
if uservars:
js = "".join(USERVAR_CODE % {'varnr': varnr, 'value': value}
for (varnr, value) in uservars.items())
html = '%s\n<script type="text/javascript">%s</script>' \
% (html, js)
return html

View file

@ -0,0 +1,58 @@
"""
Google Analytics service.
"""
import re
from analytical.services.base import AnalyticalService
PROPERTY_ID_RE = re.compile(r'^UA-\d+-\d+$')
SETUP_CODE = """
<script type="text/javascript">
var _gaq = _gaq || [];
_gaq.push(['_setAccount', '%(property_id)s']);
%(commands)s
(function() {
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
})();
</script>
"""
TRACK_CODE = "_gaq.push(['_trackPageview']);"
CUSTOM_VARS_CONTEXT_KEY = "google_analytics_custom_vars"
CUSTOM_VAR_CODE = "_gaq.push(['_setCustomVar', %(index)d, '%(name)s', " \
"'%(value)s', %(scope)d]);"
class GoogleAnalyticsService(AnalyticalService):
def __init__(self):
self.property_id = self.get_required_setting(
'GOOGLE_ANALYTICS_PROPERTY_ID', PROPERTY_ID_RE,
"must be a string looking like 'UA-XXXXXX-Y'")
def render_head_bottom(self, context):
commands = self._get_custom_var_commands(context)
commands.append(TRACK_CODE)
return SETUP_CODE % {'property_id': self.property_id,
'commands': " ".join(commands)}
def _get_custom_var_commands(self, context):
commands = []
vardefs = context.get(CUSTOM_VARS_CONTEXT_KEY, [])
for vardef in vardefs:
index = vardef[0]
if not 1 <= index <= 5:
raise ValueError("Google Analytics custom variable index must "
"be between 1 and 5: %s" % index)
name = vardef[1]
value = vardef[2]
if len(vardef) >= 4:
scope = vardef[3]
else:
scope = 2
commands.append(CUSTOM_VAR_CODE % locals())
return commands

View file

@ -0,0 +1,39 @@
"""
KISSinsights service.
"""
import re
from analytical.services.base import AnalyticalService
ACCOUNT_NUMBER_RE = re.compile(r'^\d{5}$')
SITE_CODE_RE = re.compile(r'^[\d\w]{3}$')
SETUP_CODE = """
<script type="text/javascript">var _kiq = _kiq || []; %(commands)s</script>
<script type="text/javascript" src="//s3.amazonaws.com/ki.js/%(account_number)s/%(site_code)s.js" async="true"></script>
"""
IDENTIFY_CODE = "_kiq.push(['identify', '%s']);"
SHOW_SURVEY_CODE = "_kiq.push(['showSurvey', %s]);"
SHOW_SURVEY_CONTEXT_KEY = 'kiss_insights_show_survey'
class KissInsightsService(AnalyticalService):
def __init__(self):
self.account_number = self.get_required_setting(
'KISS_INSIGHTS_ACCOUNT_NUMBER', ACCOUNT_NUMBER_RE,
"must be a string containing an five-digit number")
self.site_code = self.get_required_setting('KISS_INSIGHTS_SITE_CODE',
SITE_CODE_RE, "must be a string containing three characters")
def render_body_top(self, context):
commands = []
identity = self.get_identity(context)
if identity is not None:
commands.append(IDENTIFY_CODE % identity)
try:
commands.append(SHOW_SURVEY_CODE
% context[SHOW_SURVEY_CONTEXT_KEY])
except KeyError:
pass
return SETUP_CODE % {'account_number': self.account_number,
'site_code': self.site_code, 'commands': " ".join(commands)}

View file

@ -0,0 +1,51 @@
"""
KISSmetrics service.
"""
import re
from django.utils import simplejson
from analytical.services.base import AnalyticalService
API_KEY_RE = re.compile(r'^[0-9a-f]{40}$')
SETUP_CODE = """
<script type="text/javascript">
var _kmq = _kmq || [];
%(commands)s
function _kms(u){
setTimeout(function(){
var s = document.createElement('script');
s.type = 'text/javascript';
s.async = true;
s.src = u;
var f = document.getElementsByTagName('script')[0];
f.parentNode.insertBefore(s, f);
}, 1);
}
_kms('//i.kissmetrics.com/i.js');
_kms('//doug1izaerwt3.cloudfront.net/%(api_key)s.1.js');
</script>
"""
IDENTIFY_CODE = "_kmq.push(['identify', '%s']);"
JS_EVENT_CODE = "_kmq.push(['record', '%(name)s', %(properties)s]);"
class KissMetricsService(AnalyticalService):
def __init__(self):
self.api_key = self.get_required_setting('KISS_METRICS_API_KEY',
API_KEY_RE,
"must be a string containing a 40-digit hexadecimal number")
def render_head_top(self, context):
commands = []
identity = self.get_identity(context)
if identity is not None:
commands.append(IDENTIFY_CODE % identity)
return SETUP_CODE % {'api_key': self.api_key,
'commands': commands}
def render_event(self, name, properties):
return JS_EVENT_CODE % {'name': name,
'properties': simplejson.dumps(properties)}

View file

@ -0,0 +1,45 @@
"""
Mixpanel service.
"""
import re
from django.utils import simplejson
from analytical.services.base import AnalyticalService
MIXPANEL_TOKEN_RE = re.compile(r'^[0-9a-f]{32}$')
SETUP_CODE = """
<script type="text/javascript">
var mpq = [];
mpq.push(['init', '%(token)s']);
%(commands)s
(function() {
var mp = document.createElement("script"); mp.type = "text/javascript"; mp.async = true;
mp.src = (document.location.protocol == 'https:' ? 'https:' : 'http:') + "//api.mixpanel.com/site_media/js/api/mixpanel.js";
var s = document.getElementsByTagName("script")[0]; s.parentNode.insertBefore(mp, s);
})();
</script>
"""
IDENTIFY_CODE = "mpq.push(['identify', '%s']);"
EVENT_CODE = "mpq.push(['track', '%(name)s', %(properties)s]);"
class MixpanelService(AnalyticalService):
def __init__(self):
self.token = self.get_required_setting('MIXPANEL_TOKEN',
MIXPANEL_TOKEN_RE,
"must be a string containing a 32-digit hexadecimal number")
def render_head_bottom(self, context):
commands = []
identity = self.get_identity(context)
if identity is not None:
commands.append(IDENTIFY_CODE % identity)
return SETUP_CODE % {'token': self.token,
'commands': " ".join(commands)}
def render_event(self, name, properties):
return EVENT_CODE % {'name': name,
'properties': simplejson.dumps(properties)}

View file

@ -0,0 +1,21 @@
"""
Optimizely service.
"""
import re
from analytical.services.base import AnalyticalService
ACCOUNT_NUMBER_RE = re.compile(r'^\d{7}$')
SETUP_CODE = """<script src="//cdn.optimizely.com/js/%(account_number)s.js"></script>"""
class OptimizelyService(AnalyticalService):
def __init__(self):
self.account_number = self.get_required_setting(
'OPTIMIZELY_ACCOUNT_NUMBER', ACCOUNT_NUMBER_RE,
"must be a string containing an seven-digit number")
def render_head_top(self, context):
return SETUP_CODE % {'account_number': self.account_number}

View file

@ -1,96 +1,99 @@
"""
Analytical template tags and filters.
Analytical template tags.
"""
import logging
from importlib import import_module
from __future__ import absolute_import
from django import template
from django.template import Node, TemplateSyntaxError
from django.conf import settings
from django.template import Node, TemplateSyntaxError, Variable
from analytical.utils import AnalyticalException
from analytical.services import get_enabled_services
HTML_COMMENT_CODE = "<!-- Analytical disabled on internal IP address\n%s\n-->"
JS_COMMENT_CODE = "/* %s */"
SCRIPT_CODE = """<script type="text/javascript">%s</script>"""
TAG_LOCATIONS = ['head_top', 'head_bottom', 'body_top', 'body_bottom']
TAG_POSITIONS = ['first', None, 'last']
TAG_MODULES = [
'analytical.chartbeat',
'analytical.clickmap',
'analytical.clicky',
'analytical.crazy_egg',
'analytical.facebook_pixel',
'analytical.gauges',
'analytical.google_analytics',
'analytical.google_analytics_js',
'analytical.google_analytics_gtag',
'analytical.gosquared',
'analytical.heap',
'analytical.hotjar',
'analytical.hubspot',
'analytical.intercom',
'analytical.kiss_insights',
'analytical.kiss_metrics',
'analytical.luckyorange',
'analytical.matomo',
'analytical.mixpanel',
'analytical.olark',
'analytical.optimizely',
'analytical.performable',
'analytical.rating_mailru',
'analytical.snapengage',
'analytical.spring_metrics',
'analytical.uservoice',
'analytical.woopra',
'analytical.yandex_metrica',
]
logger = logging.getLogger(__name__)
register = template.Library()
def _location_tag(location):
def analytical_tag(parser, token):
def 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 TAG_LOCATIONS:
register.tag('analytical_%s' % loc, _location_tag(loc))
for l in ['head_top', 'head_bottom', 'body_top', 'body_bottom']:
register.tag('analytical_setup_%s' % l, _location_tag(l))
class AnalyticalNode(Node):
def __init__(self, location):
self.nodes = [node_cls() for node_cls in template_nodes[location]]
self.location = location
self.render_func_name = "render_%s" % self.location
self.internal_ips = getattr(settings, 'ANALYTICAL_INTERNAL_IPS',
getattr(settings, 'INTERNAL_IPS', ()))
def render(self, context):
return ''.join([node.render(context) for node in self.nodes])
result = "".join([self._render_service(service, context)
for service in get_enabled_services()])
if not result:
return ""
if self._is_internal_ip(context):
return HTML_COMMENT_CODE % result
return result
def _render_service(self, service, context):
func = getattr(service, self.render_func_name)
return func(context)
def _load_template_nodes():
template_nodes = {loc: {pos: [] for pos in TAG_POSITIONS} for loc in TAG_LOCATIONS}
def add_node_cls(location, node, position=None):
template_nodes[location][position].append(node)
for path in TAG_MODULES:
module = _import_tag_module(path)
def _is_internal_ip(self, context):
try:
module.contribute_to_analytical(add_node_cls)
except AnalyticalException as e:
logger.debug("not loading tags from '%s': %s", path, e)
for location in TAG_LOCATIONS:
template_nodes[location] = sum(
(template_nodes[location][p] for p in TAG_POSITIONS), []
)
return template_nodes
request = context['request']
remote_ip = request.META.get('HTTP_X_FORWARDED_FOR',
request.META.get('REMOTE_ADDR', ''))
return remote_ip in self.internal_ips
except KeyError, AttributeError:
return False
def _import_tag_module(path):
app_name, lib_name = path.rsplit('.', 1)
return import_module('%s.templatetags.%s' % (app_name, lib_name))
def event(parser, token):
bits = token.split_contents()
if len(bits) < 2:
raise TemplateSyntaxError("'%s' tag takes at least one argument"
% bits[0])
properties = _parse_properties(bits[0], bits[2:])
return EventNode(bits[1], properties)
register.tag('event', event)
template_nodes = _load_template_nodes()
class EventNode(Node):
def __init__(self, name, properties):
self.name = name
self.properties = properties
def render(self, context):
props = dict((var, Variable(val).resolve(context))
for var, val in self.properties)
result = "".join([service.render_js_event(props)
for service in get_enabled_services()])
if not result:
return ""
if self._is_internal_ip(context):
return JS_COMMENT_CODE % result
return result
def _parse_properties(tag_name, bits):
properties = []
for bit in bits:
try:
properties.append(bit.split('=', 1))
except IndexError:
raise TemplateSyntaxError("'%s' tag argument must be of the form "
" property=value: '%s'" % (tag_name, bit))
return properties

View file

@ -1,114 +0,0 @@
"""
Chartbeat template tags and filters.
"""
import json
import re
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.template import Library, Node, TemplateSyntaxError
from analytical.utils import disable_html, get_required_setting, is_internal_ip
USER_ID_RE = re.compile(r'^\d+$')
INIT_CODE = """<script>var _sf_startpt=(new Date()).getTime()</script>"""
SETUP_CODE = """
<script>
var _sf_async_config=%(config)s;
(function(){
function loadChartbeat() {
window._sf_endpt=(new Date()).getTime();
var e = document.createElement('script');
e.setAttribute('language', 'javascript');
e.setAttribute('type', 'text/javascript');
e.setAttribute('src',
(("https:" == document.location.protocol) ? "https://a248.e.akamai.net/chartbeat.download.akamai.com/102508/" : "http://static.chartbeat.com/") +
"js/chartbeat.js");
document.body.appendChild(e);
}
var oldonload = window.onload;
window.onload = (typeof window.onload != 'function') ?
loadChartbeat : function() { oldonload(); loadChartbeat(); };
})();
</script>
""" # noqa
DOMAIN_CONTEXT_KEY = 'chartbeat_domain'
register = Library()
@register.tag
def chartbeat_top(parser, token):
"""
Top Chartbeat template tag.
Render the top JavaScript code for Chartbeat.
"""
bits = token.split_contents()
if len(bits) > 1:
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
return ChartbeatTopNode()
class ChartbeatTopNode(Node):
def render(self, context):
if is_internal_ip(context):
return disable_html(INIT_CODE, 'Chartbeat')
return INIT_CODE
@register.tag
def chartbeat_bottom(parser, token):
"""
Bottom Chartbeat template tag.
Render the bottom JavaScript code for Chartbeat. You must supply
your Chartbeat User ID (as a string) in the ``CHARTBEAT_USER_ID``
setting.
"""
bits = token.split_contents()
if len(bits) > 1:
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
return ChartbeatBottomNode()
class ChartbeatBottomNode(Node):
def __init__(self):
self.user_id = get_required_setting(
'CHARTBEAT_USER_ID', USER_ID_RE, 'must be (a string containing) a number'
)
def render(self, context):
config = {'uid': self.user_id}
domain = _get_domain(context)
if domain is not None:
config['domain'] = domain
html = SETUP_CODE % {'config': json.dumps(config, sort_keys=True)}
if is_internal_ip(context, 'CHARTBEAT'):
html = disable_html(html, 'Chartbeat')
return html
def contribute_to_analytical(add_node):
ChartbeatBottomNode() # ensure properly configured
add_node('head_top', ChartbeatTopNode, 'first')
add_node('body_bottom', ChartbeatBottomNode, 'last')
def _get_domain(context):
domain = context.get(DOMAIN_CONTEXT_KEY)
if domain is not None:
return domain
else:
if 'django.contrib.sites' not in settings.INSTALLED_APPS:
return
elif getattr(settings, 'CHARTBEAT_AUTO_DOMAIN', True):
from django.contrib.sites.models import Site
try:
return Site.objects.get_current().domain
except (ImproperlyConfigured, Site.DoesNotExist): # pylint: disable=E1101
return

View file

@ -1,60 +0,0 @@
"""
Clickmap template tags and filters.
"""
import re
from django.template import Library, Node, TemplateSyntaxError
from analytical.utils import disable_html, get_required_setting, is_internal_ip
CLICKMAP_TRACKER_ID_RE = re.compile(r'^\w+$')
TRACKING_CODE = """
<script>
var clickmapConfig = {tracker: '%(tracker_id)s', version:'2'};
window.clickmapAsyncInit = function(){ __clickmap.init(clickmapConfig); };
(function() { var _cmf = document.createElement('script'); _cmf.async = true;
_cmf.src = document.location.protocol + '//www.clickmap.ch/tracker.js?t=';
_cmf.src += clickmapConfig.tracker; _cmf.id += 'clickmap_tracker';
_cmf.src += '&v='+clickmapConfig.version+'&now='+(new Date().getTime());
if (document.getElementById('clickmap_tracker')==null) {
document.getElementsByTagName('head')[0].appendChild(_cmf); }}());
</script>
"""
register = Library()
@register.tag
def clickmap(parser, token):
"""
Clickmap tracker template tag.
Renders JavaScript code to track page visits. You must supply
your clickmap tracker ID (as a string) in the ``CLICKMAP_TRACKER_ID``
setting.
"""
bits = token.split_contents()
if len(bits) > 1:
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
return ClickmapNode()
class ClickmapNode(Node):
def __init__(self):
self.tracker_id = get_required_setting(
'CLICKMAP_TRACKER_ID',
CLICKMAP_TRACKER_ID_RE,
'must be an alphanumeric string',
)
def render(self, context):
html = TRACKING_CODE % {'tracker_id': self.tracker_id}
if is_internal_ip(context, 'CLICKMAP'):
html = disable_html(html, 'Clickmap')
return html
def contribute_to_analytical(add_node):
ClickmapNode() # ensure properly configured
add_node('body_bottom', ClickmapNode)

View file

@ -1,81 +0,0 @@
"""
Clicky template tags and filters.
"""
import json
import re
from django.template import Library, Node, TemplateSyntaxError
from analytical.utils import (
disable_html,
get_identity,
get_required_setting,
is_internal_ip,
)
SITE_ID_RE = re.compile(r'^\d+$')
TRACKING_CODE = """
<script>
var clicky = { log: function(){ return; }, goal: function(){ return; }};
var clicky_site_ids = clicky_site_ids || [];
clicky_site_ids.push(%(site_id)s);
var clicky_custom = %(custom)s;
(function() {
var s = document.createElement('script');
s.type = 'text/javascript';
s.async = true;
s.src = '//static.getclicky.com/js';
( document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0] ).appendChild( s );
})();
</script>
<noscript><p><img alt="Clicky" width="1" height="1" src="//in.getclicky.com/%(site_id)sns.gif" /></p></noscript>
""" # noqa
register = Library()
@register.tag
def clicky(parser, token):
"""
Clicky tracking template tag.
Renders JavaScript code to track page visits. You must supply
your Clicky Site ID (as a string) in the ``CLICKY_SITE_ID``
setting.
"""
bits = token.split_contents()
if len(bits) > 1:
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
return ClickyNode()
class ClickyNode(Node):
def __init__(self):
self.site_id = get_required_setting(
'CLICKY_SITE_ID', SITE_ID_RE, 'must be a (string containing) a number'
)
def render(self, context):
custom = {}
for dict_ in context:
for var, val in dict_.items():
if var.startswith('clicky_'):
custom[var[7:]] = val
if 'username' not in custom.get('session', {}):
identity = get_identity(context, 'clicky')
if identity is not None:
custom.setdefault('session', {})['username'] = identity
html = TRACKING_CODE % {
'site_id': self.site_id,
'custom': json.dumps(custom, sort_keys=True),
}
if is_internal_ip(context, 'CLICKY'):
html = disable_html(html, 'Clicky')
return html
def contribute_to_analytical(add_node):
ClickyNode() # ensure properly configured
add_node('body_bottom', ClickyNode)

View file

@ -1,69 +0,0 @@
"""
Crazy Egg template tags and filters.
"""
import re
from django.template import Library, Node, TemplateSyntaxError
from analytical.utils import disable_html, get_required_setting, is_internal_ip
ACCOUNT_NUMBER_RE = re.compile(r'^\d+$')
SETUP_CODE = '<script src="{placeholder_url}"></script>'.format(
placeholder_url='//dnn506yrbagrg.cloudfront.net/pages/scripts/'
'%(account_nr_1)s/%(account_nr_2)s.js'
)
USERVAR_CODE = "CE2.set(%(varnr)d, '%(value)s');"
register = Library()
@register.tag
def crazy_egg(parser, token):
"""
Crazy Egg tracking template tag.
Renders JavaScript code to track page clicks. You must supply
your Crazy Egg account number (as a string) in the
``CRAZY_EGG_ACCOUNT_NUMBER`` setting.
"""
bits = token.split_contents()
if len(bits) > 1:
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
return CrazyEggNode()
class CrazyEggNode(Node):
def __init__(self):
self.account_nr = get_required_setting(
'CRAZY_EGG_ACCOUNT_NUMBER',
ACCOUNT_NUMBER_RE,
'must be (a string containing) a number',
)
def render(self, context):
html = SETUP_CODE % {
'account_nr_1': self.account_nr[:4],
'account_nr_2': self.account_nr[4:],
}
values = (context.get('crazy_egg_var%d' % i) for i in range(1, 6))
params = [(i, v) for i, v in enumerate(values, 1) if v is not None]
if params:
js = ' '.join(
USERVAR_CODE
% {
'varnr': varnr,
'value': value,
}
for (varnr, value) in params
)
html = '%s\n<script>%s</script>' % (html, js)
if is_internal_ip(context, 'CRAZY_EGG'):
html = disable_html(html, 'Crazy Egg')
return html
def contribute_to_analytical(add_node):
CrazyEggNode() # ensure properly configured
add_node('body_bottom', CrazyEggNode)

View file

@ -1,96 +0,0 @@
"""
Facebook Pixel template tags and filters.
"""
import re
from django.template import Library, Node, TemplateSyntaxError
from analytical.utils import disable_html, get_required_setting, is_internal_ip
FACEBOOK_PIXEL_HEAD_CODE = """\
<script>
!function(f,b,e,v,n,t,s)
{if(f.fbq)return;n=f.fbq=function(){n.callMethod?
n.callMethod.apply(n,arguments):n.queue.push(arguments)};
if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';
n.queue=[];t=b.createElement(e);t.async=!0;
t.src=v;s=b.getElementsByTagName(e)[0];
s.parentNode.insertBefore(t,s)}(window, document,'script',
'https://connect.facebook.net/en_US/fbevents.js');
fbq('init', '%(FACEBOOK_PIXEL_ID)s');
fbq('track', 'PageView');
</script>
"""
FACEBOOK_PIXEL_BODY_CODE = """\
<noscript><img height="1" width="1" style="display:none"
src="https://www.facebook.com/tr?id=%(FACEBOOK_PIXEL_ID)s&ev=PageView&noscript=1"
/></noscript>
"""
register = Library()
def _validate_no_args(token):
bits = token.split_contents()
if len(bits) > 1:
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
@register.tag
def facebook_pixel_head(parser, token):
"""
Facebook Pixel head template tag.
"""
_validate_no_args(token)
return FacebookPixelHeadNode()
@register.tag
def facebook_pixel_body(parser, token):
"""
Facebook Pixel body template tag.
"""
_validate_no_args(token)
return FacebookPixelBodyNode()
class _FacebookPixelNode(Node):
"""
Base class: override and provide code_template.
"""
def __init__(self):
self.pixel_id = get_required_setting(
'FACEBOOK_PIXEL_ID',
re.compile(r'^\d+$'),
'must be (a string containing) a number',
)
def render(self, context):
html = self.code_template % {'FACEBOOK_PIXEL_ID': self.pixel_id}
if is_internal_ip(context, 'FACEBOOK_PIXEL'):
return disable_html(html, 'Facebook Pixel')
else:
return html
@property
def code_template(self):
raise NotImplementedError # pragma: no cover
class FacebookPixelHeadNode(_FacebookPixelNode):
code_template = FACEBOOK_PIXEL_HEAD_CODE
class FacebookPixelBodyNode(_FacebookPixelNode):
code_template = FACEBOOK_PIXEL_BODY_CODE
def contribute_to_analytical(add_node):
# ensure properly configured
FacebookPixelHeadNode()
FacebookPixelBodyNode()
add_node('head_bottom', FacebookPixelHeadNode)
add_node('body_bottom', FacebookPixelBodyNode)

View file

@ -1,61 +0,0 @@
"""
Gaug.es template tags and filters.
"""
import re
from django.template import Library, Node, TemplateSyntaxError
from analytical.utils import disable_html, get_required_setting, is_internal_ip
SITE_ID_RE = re.compile(r'[\da-f]+$')
TRACKING_CODE = """
<script>
var _gauges = _gauges || [];
(function() {
var t = document.createElement('script');
t.type = 'text/javascript';
t.async = true;
t.id = 'gauges-tracker';
t.setAttribute('data-site-id', '%(site_id)s');
t.src = '//secure.gaug.es/track.js';
var s = document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(t, s);
})();
</script>
"""
register = Library()
@register.tag
def gauges(parser, token):
"""
Gaug.es template tag.
Renders JavaScript code to gaug.es testing. You must supply
your Site ID account number in the ``GAUGES_SITE_ID``
setting.
"""
bits = token.split_contents()
if len(bits) > 1:
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
return GaugesNode()
class GaugesNode(Node):
def __init__(self):
self.site_id = get_required_setting(
'GAUGES_SITE_ID', SITE_ID_RE, "must be a string looking like 'XXXXXXX'"
)
def render(self, context):
html = TRACKING_CODE % {'site_id': self.site_id}
if is_internal_ip(context, 'GAUGES'):
html = disable_html(html, 'Gauges')
return html
def contribute_to_analytical(add_node):
GaugesNode()
add_node('head_bottom', GaugesNode)

View file

@ -1,206 +0,0 @@
"""
Google Analytics template tags and filters.
DEPRECATED
"""
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
SCOPE_VISITOR = 1
SCOPE_SESSION = 2
SCOPE_PAGE = 3
PROPERTY_ID_RE = re.compile(r'^UA-\d+-\d+$')
SETUP_CODE = """
<script>
var _gaq = _gaq || [];
_gaq.push(['_setAccount', '%(property_id)s']);
%(commands)s
(function() {
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
ga.src = ('https:' == document.location.protocol ? %(source_scheme)s) + %(source_url)s;
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
})();
</script>
"""
DOMAIN_CODE = "_gaq.push(['_setDomainName', '%s']);"
NO_ALLOW_HASH_CODE = "_gaq.push(['_setAllowHash', false]);"
TRACK_PAGE_VIEW = "_gaq.push(['_trackPageview']);"
ALLOW_LINKER_CODE = "_gaq.push(['_setAllowLinker', true]);"
CUSTOM_VAR_CODE = (
"_gaq.push(['_setCustomVar', %(index)s, '%(name)s', '%(value)s', %(scope)s]);"
)
SITE_SPEED_CODE = "_gaq.push(['_trackPageLoadTime']);"
ANONYMIZE_IP_CODE = "_gaq.push(['_gat._anonymizeIp']);"
SAMPLE_RATE_CODE = "_gaq.push(['_setSampleRate', '%s']);"
SITE_SPEED_SAMPLE_RATE_CODE = "_gaq.push(['_setSiteSpeedSampleRate', '%s']);"
SESSION_COOKIE_TIMEOUT_CODE = "_gaq.push(['_setSessionCookieTimeout', '%s']);"
VISITOR_COOKIE_TIMEOUT_CODE = "_gaq.push(['_setVisitorCookieTimeout', '%s']);"
DEFAULT_SOURCE = ("'https://ssl' : 'http://www'", "'.google-analytics.com/ga.js'")
DISPLAY_ADVERTISING_SOURCE = (
"'https://' : 'http://'",
"'stats.g.doubleclick.net/dc.js'",
)
ZEROPLACES = decimal.Decimal('0')
TWOPLACES = decimal.Decimal('0.01')
register = Library()
@register.tag
def google_analytics(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 GoogleAnalyticsNode()
class GoogleAnalyticsNode(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):
commands = self._get_domain_commands(context)
commands.extend(self._get_custom_var_commands(context))
commands.extend(self._get_other_commands(context))
commands.append(TRACK_PAGE_VIEW)
if getattr(settings, 'GOOGLE_ANALYTICS_DISPLAY_ADVERTISING', False):
source = DISPLAY_ADVERTISING_SOURCE
else:
source = DEFAULT_SOURCE
html = SETUP_CODE % {
'property_id': self.property_id,
'commands': ' '.join(commands),
'source_scheme': source[0],
'source_url': source[1],
}
if is_internal_ip(context, 'GOOGLE_ANALYTICS'):
html = disable_html(html, 'Google Analytics')
return html
def _get_domain_commands(self, context):
commands = []
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'
)
commands.append(DOMAIN_CODE % domain)
commands.append(NO_ALLOW_HASH_CODE)
if tracking_type == TRACK_MULTIPLE_DOMAINS:
commands.append(ALLOW_LINKER_CODE)
return commands
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 index, var in params:
name = var[0]
value = var[1]
try:
scope = var[2]
except IndexError:
scope = SCOPE_PAGE
commands.append(
CUSTOM_VAR_CODE
% {
'index': index,
'name': name,
'value': value,
'scope': scope,
}
)
return commands
def _get_other_commands(self, context):
commands = []
if getattr(settings, 'GOOGLE_ANALYTICS_SITE_SPEED', False):
commands.append(SITE_SPEED_CODE)
if getattr(settings, 'GOOGLE_ANALYTICS_ANONYMIZE_IP', False):
commands.append(ANONYMIZE_IP_CODE)
sampleRate = getattr(settings, 'GOOGLE_ANALYTICS_SAMPLE_RATE', False)
if sampleRate is not False:
value = decimal.Decimal(sampleRate)
if not 0 <= value <= 100:
raise AnalyticalException(
"'GOOGLE_ANALYTICS_SAMPLE_RATE' must be >= 0 and <= 100"
)
commands.append(SAMPLE_RATE_CODE % value.quantize(TWOPLACES))
siteSpeedSampleRate = getattr(
settings, 'GOOGLE_ANALYTICS_SITE_SPEED_SAMPLE_RATE', False
)
if siteSpeedSampleRate is not False:
value = decimal.Decimal(siteSpeedSampleRate)
if not 0 <= value <= 100:
raise AnalyticalException(
"'GOOGLE_ANALYTICS_SITE_SPEED_SAMPLE_RATE' must be >= 0 and <= 100"
)
commands.append(SITE_SPEED_SAMPLE_RATE_CODE % value.quantize(TWOPLACES))
sessionCookieTimeout = getattr(
settings, 'GOOGLE_ANALYTICS_SESSION_COOKIE_TIMEOUT', False
)
if sessionCookieTimeout is not False:
value = decimal.Decimal(sessionCookieTimeout)
if value < 0:
raise AnalyticalException(
"'GOOGLE_ANALYTICS_SESSION_COOKIE_TIMEOUT' must be >= 0"
)
commands.append(SESSION_COOKIE_TIMEOUT_CODE % value.quantize(ZEROPLACES))
visitorCookieTimeout = getattr(
settings, 'GOOGLE_ANALYTICS_VISITOR_COOKIE_TIMEOUT', False
)
if visitorCookieTimeout is not False:
value = decimal.Decimal(visitorCookieTimeout)
if value < 0:
raise AnalyticalException(
"'GOOGLE_ANALYTICS_VISITOR_COOKIE_TIMEOUT' must be >= 0"
)
commands.append(VISITOR_COOKIE_TIMEOUT_CODE % value.quantize(ZEROPLACES))
return commands
def contribute_to_analytical(add_node):
GoogleAnalyticsNode() # ensure properly configured
add_node('head_bottom', GoogleAnalyticsNode)

View file

@ -1,78 +0,0 @@
"""
Google Analytics template tags and filters, using the new gtag.js library.
https://developers.google.com/tag-platform/gtagjs/reference
"""
import json
import re
from django.template import Library, Node, TemplateSyntaxError
from analytical.utils import (
disable_html,
get_identity,
get_required_setting,
is_internal_ip,
)
PROPERTY_ID_RE = re.compile(
r'^UA-\d+-\d+$|^G-[a-zA-Z0-9]+$|^AW-[a-zA-Z0-9]+$|^DC-[a-zA-Z0-9]+$'
)
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());
gtag('config', '{property_id}', {custom_dimensions});
</script>
"""
register = Library()
@register.tag
def google_analytics_gtag(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_GTAG_PROPERTY_ID`` setting.
"""
bits = token.split_contents()
if len(bits) > 1:
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
return GoogleAnalyticsGTagNode()
class GoogleAnalyticsGTagNode(Node):
def __init__(self):
self.property_id = get_required_setting(
'GOOGLE_ANALYTICS_GTAG_PROPERTY_ID',
PROPERTY_ID_RE,
"""must be a string looking like one of these patterns
('UA-XXXXXX-Y' , 'AW-XXXXXXXXXX',
'G-XXXXXXXX', 'DC-XXXXXXXX')""",
)
def render(self, context):
custom_dimensions = context.get('google_analytics_custom_dimensions', {})
identity = get_identity(context, prefix='google_analytics_gtag')
if identity is not None:
custom_dimensions['user_id'] = identity
html = SETUP_CODE.format(
property_id=self.property_id,
custom_dimensions=json.dumps(custom_dimensions),
)
if is_internal_ip(context, 'GOOGLE_ANALYTICS'):
html = disable_html(html, 'Google Analytics')
return html
def contribute_to_analytical(add_node):
GoogleAnalyticsGTagNode() # ensure properly configured
add_node('head_top', GoogleAnalyticsGTagNode)

View file

@ -1,176 +0,0 @@
"""
Google Analytics template tags and filters, using the new analytics.js library.
"""
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','{js_source}','ga');
ga('create', '{property_id}', 'auto', {create_fields});
{commands}ga('send', 'pageview');
</script>
"""
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()
@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
)
if display_features:
commands.insert(0, REQUIRE_DISPLAY_FEATURES)
js_source = getattr(
settings,
'GOOGLE_ANALYTICS_JS_SOURCE',
'https://www.google-analytics.com/analytics.js',
)
html = SETUP_CODE.format(
property_id=self.property_id,
create_fields=json.dumps(create_fields),
commands=''.join(commands),
js_source=js_source,
)
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 = f"'{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

@ -1,82 +0,0 @@
"""
GoSquared template tags and filters.
"""
import re
from django.template import Library, Node, TemplateSyntaxError
from analytical.utils import (
disable_html,
get_identity,
get_required_setting,
is_internal_ip,
)
TOKEN_RE = re.compile(r'^\S+-\S+-\S+$')
TRACKING_CODE = """
<script>
var GoSquared={};
%(config)s
(function(w){
function gs(){
w._gstc_lt=+(new Date); var d=document;
var g = d.createElement("script"); g.type = "text/javascript"; g.async = true; g.src = "//d1l6p2sc9645hc.cloudfront.net/tracker.js";
var s = d.getElementsByTagName("script")[0]; s.parentNode.insertBefore(g, s);
}
w.addEventListener?w.addEventListener("load",gs,false):w.attachEvent("onload",gs);
})(window);
</script>
""" # noqa
TOKEN_CODE = 'GoSquared.acct = "%s";'
IDENTIFY_CODE = 'GoSquared.UserName = "%s";'
register = Library()
@register.tag
def gosquared(parser, token):
"""
GoSquared tracking template tag.
Renders JavaScript code to track page visits. You must supply
your GoSquared site token in the ``GOSQUARED_SITE_TOKEN`` setting.
"""
bits = token.split_contents()
if len(bits) > 1:
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
return GoSquaredNode()
class GoSquaredNode(Node):
def __init__(self):
self.site_token = get_required_setting(
'GOSQUARED_SITE_TOKEN',
TOKEN_RE,
'must be a string looking like XXX-XXXXXX-X',
)
def render(self, context):
configs = [TOKEN_CODE % self.site_token]
identity = get_identity(context, 'gosquared', self._identify)
if identity:
configs.append(IDENTIFY_CODE % identity)
html = TRACKING_CODE % {
'token': self.site_token,
'config': ' '.join(configs),
}
if is_internal_ip(context, 'GOSQUARED'):
html = disable_html(html, 'GoSquared')
return html
def _identify(self, user):
name = user.get_full_name()
if not name:
name = user.username
return name
def contribute_to_analytical(add_node):
GoSquaredNode() # ensure properly configured
add_node('body_bottom', GoSquaredNode)

View file

@ -1,57 +0,0 @@
"""
Heap template tags and filters.
"""
import re
from django.template import Library, Node, TemplateSyntaxError
from analytical.utils import disable_html, get_required_setting, is_internal_ip
HEAP_TRACKER_ID_RE = re.compile(r'^\d+$')
TRACKING_CODE = """
<script>
window.heap=window.heap||[],heap.load=function(e,t){window.heap.appid=e,window.heap.config=t=t||{};var r=document.createElement("script");r.type="text/javascript",r.async=!0,r.src="https://cdn.heapanalytics.com/js/heap-"+e+".js";var a=document.getElementsByTagName("script")[0];a.parentNode.insertBefore(r,a);for(var n=function(e){return function(){heap.push([e].concat(Array.prototype.slice.call(arguments,0)))}},p=["addEventProperties","addUserProperties","clearEventProperties","identify","resetIdentity","removeEventProperty","setEventProperties","track","unsetEventProperty"],o=0;o<p.length;o++)heap[p[o]]=n(p[o])};
heap.load("%(tracker_id)s");
</script>
""" # noqa
register = Library()
def _validate_no_args(token):
bits = token.split_contents()
if len(bits) > 1:
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
@register.tag
def heap(parser, token):
"""
Heap tracker template tag.
Renders JavaScript code to track page visits. You must supply
your heap tracker ID (as a string) in the ``HEAP_TRACKER_ID``
setting.
"""
_validate_no_args(token)
return HeapNode()
class HeapNode(Node):
def __init__(self):
self.tracker_id = get_required_setting(
'HEAP_TRACKER_ID', HEAP_TRACKER_ID_RE, 'must be an numeric string'
)
def render(self, context):
html = TRACKING_CODE % {'tracker_id': self.tracker_id}
if is_internal_ip(context, 'HEAP'):
html = disable_html(html, 'Heap')
return html
def contribute_to_analytical(add_node):
HeapNode() # ensure properly configured
add_node('head_bottom', HeapNode)

View file

@ -1,62 +0,0 @@
"""
Hotjar template tags and filters.
"""
import re
from django.template import Library, Node, TemplateSyntaxError
from analytical.utils import disable_html, get_required_setting, is_internal_ip
HOTJAR_TRACKING_CODE = """\
<script>
(function(h,o,t,j,a,r){
h.hj=h.hj||function(){(h.hj.q=h.hj.q||[]).push(arguments)};
h._hjSettings={hjid:%(HOTJAR_SITE_ID)s,hjsv:6};
a=o.getElementsByTagName('head')[0];
r=o.createElement('script');r.async=1;
r.src=t+h._hjSettings.hjid+j+h._hjSettings.hjsv;
a.appendChild(r);
})(window,document,'https://static.hotjar.com/c/hotjar-','.js?sv=');
</script>
"""
register = Library()
def _validate_no_args(token):
bits = token.split_contents()
if len(bits) > 1:
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
@register.tag
def hotjar(parser, token):
"""
Hotjar template tag.
"""
_validate_no_args(token)
return HotjarNode()
class HotjarNode(Node):
def __init__(self):
self.site_id = get_required_setting(
'HOTJAR_SITE_ID',
re.compile(r'^\d+$'),
'must be (a string containing) a number',
)
def render(self, context):
html = HOTJAR_TRACKING_CODE % {'HOTJAR_SITE_ID': self.site_id}
if is_internal_ip(context, 'HOTJAR'):
return disable_html(html, 'Hotjar')
else:
return html
def contribute_to_analytical(add_node):
# ensure properly configured
HotjarNode()
add_node('head_bottom', HotjarNode)

View file

@ -1,57 +0,0 @@
"""
HubSpot template tags and filters.
"""
import re
from django.template import Library, Node, TemplateSyntaxError
from analytical.utils import disable_html, get_required_setting, is_internal_ip
PORTAL_ID_RE = re.compile(r'^\d+$')
TRACKING_CODE = """
<!-- Start of Async HubSpot Analytics Code -->
<script>
(function(d,s,i,r) {
if (d.getElementById(i)){return;}
var n=d.createElement(s),e=d.getElementsByTagName(s)[0];
n.id=i;n.src='//js.hs-analytics.net/analytics/'+(Math.ceil(new Date()/r)*r)+'/%(portal_id)s.js';
e.parentNode.insertBefore(n, e);
})(document,"script","hs-analytics",300000);
</script>
<!-- End of Async HubSpot Analytics Code -->
""" # noqa
register = Library()
@register.tag
def hubspot(parser, token):
"""
HubSpot tracking template tag.
Renders JavaScript code to track page visits. You must supply
your portal ID (as a string) in the ``HUBSPOT_PORTAL_ID`` setting.
"""
bits = token.split_contents()
if len(bits) > 1:
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
return HubSpotNode()
class HubSpotNode(Node):
def __init__(self):
self.portal_id = get_required_setting(
'HUBSPOT_PORTAL_ID', PORTAL_ID_RE, 'must be a (string containing a) number'
)
def render(self, context):
html = TRACKING_CODE % {'portal_id': self.portal_id}
if is_internal_ip(context, 'HUBSPOT'):
html = disable_html(html, 'HubSpot')
return html
def contribute_to_analytical(add_node):
HubSpotNode() # ensure properly configured
add_node('body_bottom', HubSpotNode)

View file

@ -1,132 +0,0 @@
"""
intercom.io template tags and filters.
"""
import hashlib
import hmac
import json
import re
from django.conf import settings
from django.template import Library, Node, TemplateSyntaxError
from analytical.utils import (
disable_html,
get_identity,
get_required_setting,
get_user_from_context,
get_user_is_authenticated,
is_internal_ip,
)
APP_ID_RE = re.compile(r'[\da-z]+$')
TRACKING_CODE = """
<script id="IntercomSettingsScriptTag">
window.intercomSettings = %(settings_json)s;
</script>
<script>(function(){var w=window;var ic=w.Intercom;if(typeof ic==="function"){ic('reattach_activator');ic('update',intercomSettings);}else{var d=document;var i=function(){i.c(arguments)};i.q=[];i.c=function(args){i.q.push(args)};w.Intercom=i;function l(){var s=d.createElement('script');s.type='text/javascript';s.async=true;s.src='https://static.intercomcdn.com/intercom.v1.js';var x=d.getElementsByTagName('script')[0];x.parentNode.insertBefore(s,x);}if(w.attachEvent){w.attachEvent('onload',l);}else{w.addEventListener('load',l,false);}}})()</script>
""" # noqa
register = Library()
def _hashable_bytes(data):
"""
Coerce strings to hashable bytes.
"""
if isinstance(data, bytes):
return data
elif isinstance(data, str):
return data.encode('ascii') # Fail on anything non-ASCII.
else:
raise TypeError(data)
def intercom_user_hash(data):
"""
Return a SHA-256 HMAC `user_hash` as expected by Intercom, if configured.
Return None if the `INTERCOM_HMAC_SECRET_KEY` setting is not configured.
"""
if getattr(settings, 'INTERCOM_HMAC_SECRET_KEY', None):
return hmac.new(
key=_hashable_bytes(settings.INTERCOM_HMAC_SECRET_KEY),
msg=_hashable_bytes(data),
digestmod=hashlib.sha256,
).hexdigest()
else:
return None
@register.tag
def intercom(parser, token):
"""
Intercom.io template tag.
Renders JavaScript code to intercom.io testing. You must supply
your APP ID account number in the ``INTERCOM_APP_ID``
setting.
"""
bits = token.split_contents()
if len(bits) > 1:
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
return IntercomNode()
class IntercomNode(Node):
def __init__(self):
self.app_id = get_required_setting(
'INTERCOM_APP_ID', APP_ID_RE, "must be a string looking like 'XXXXXXX'"
)
def _identify(self, user):
name = user.get_full_name()
if not name:
name = user.username
return name
def _get_custom_attrs(self, context):
params = {}
for dict_ in context:
for var, val in dict_.items():
if var.startswith('intercom_'):
params[var[9:]] = val
user = get_user_from_context(context)
if user is not None and get_user_is_authenticated(user):
if 'name' not in params:
params['name'] = get_identity(context, 'intercom', self._identify, user)
if 'email' not in params and user.email:
params['email'] = user.email
params.setdefault('user_id', user.pk)
params['created_at'] = int(user.date_joined.timestamp())
else:
params['created_at'] = None
# Generate a user_hash HMAC to verify the user's identity, if configured.
# (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'))
if user_hash_data:
user_hash = intercom_user_hash(str(user_hash_data))
if user_hash is not None:
params.setdefault('user_hash', user_hash)
return params
def render(self, context):
params = self._get_custom_attrs(context)
params['app_id'] = self.app_id
html = TRACKING_CODE % {'settings_json': json.dumps(params, sort_keys=True)}
if is_internal_ip(context, 'INTERCOM'):
html = disable_html(html, 'Intercom')
return html
def contribute_to_analytical(add_node):
IntercomNode()
add_node('body_bottom', IntercomNode)

View file

@ -1,73 +0,0 @@
"""
KISSinsights template tags.
"""
import re
from django.template import Library, Node, TemplateSyntaxError
from analytical.utils import get_identity, get_required_setting
ACCOUNT_NUMBER_RE = re.compile(r'^\d+$')
SITE_CODE_RE = re.compile(r'^[\w]+$')
SETUP_CODE = """
<script>var _kiq = _kiq || []; %(commands)s</script>
<script src="//s3.amazonaws.com/ki.js/%(account_number)s/%(site_code)s.js" async="true"></script>
""" # noqa
IDENTIFY_CODE = "_kiq.push(['identify', '%s']);"
SHOW_SURVEY_CODE = "_kiq.push(['showSurvey', %s]);"
SHOW_SURVEY_CONTEXT_KEY = 'kiss_insights_show_survey'
register = Library()
@register.tag
def kiss_insights(parser, token):
"""
KISSinsights set-up template tag.
Renders JavaScript code to set-up surveys. You must supply
your account number and site code in the
``KISS_INSIGHTS_ACCOUNT_NUMBER`` and ``KISS_INSIGHTS_SITE_CODE``
settings.
"""
bits = token.split_contents()
if len(bits) > 1:
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
return KissInsightsNode()
class KissInsightsNode(Node):
def __init__(self):
self.account_number = get_required_setting(
'KISS_INSIGHTS_ACCOUNT_NUMBER',
ACCOUNT_NUMBER_RE,
'must be (a string containing) a number',
)
self.site_code = get_required_setting(
'KISS_INSIGHTS_SITE_CODE',
SITE_CODE_RE,
'must be a string containing three characters',
)
def render(self, context):
commands = []
identity = get_identity(context, 'kiss_insights')
if identity is not None:
commands.append(IDENTIFY_CODE % identity)
try:
commands.append(SHOW_SURVEY_CODE % context[SHOW_SURVEY_CONTEXT_KEY])
except KeyError:
pass
html = SETUP_CODE % {
'account_number': self.account_number,
'site_code': self.site_code,
'commands': ' '.join(commands),
}
return html
def contribute_to_analytical(add_node):
KissInsightsNode() # ensure properly configured
add_node('body_top', KissInsightsNode)

View file

@ -1,114 +0,0 @@
"""
KISSmetrics template tags.
"""
import json
import re
from django.template import Library, Node, TemplateSyntaxError
from analytical.utils import (
disable_html,
get_identity,
get_required_setting,
is_internal_ip,
)
API_KEY_RE = re.compile(r'^[0-9a-f]{40}$')
TRACKING_CODE = """
<script>
var _kmq = _kmq || [];
%(commands)s
function _kms(u){
setTimeout(function(){
var s = document.createElement('script');
s.type = 'text/javascript';
s.async = true;
s.src = u;
var f = document.getElementsByTagName('script')[0];
f.parentNode.insertBefore(s, f);
}, 1);
}
_kms('//i.kissmetrics.com/i.js');
_kms('//doug1izaerwt3.cloudfront.net/%(api_key)s.1.js');
</script>
"""
IDENTIFY_CODE = "_kmq.push(['identify', '%s']);"
EVENT_CODE = "_kmq.push(['record', '%(name)s', %(properties)s]);"
PROPERTY_CODE = "_kmq.push(['set', %(properties)s]);"
ALIAS_CODE = "_kmq.push(['alias', '%s', '%s']);"
EVENT_CONTEXT_KEY = 'kiss_metrics_event'
PROPERTY_CONTEXT_KEY = 'kiss_metrics_properties'
ALIAS_CONTEXT_KEY = 'kiss_metrics_alias'
register = Library()
@register.tag
def kiss_metrics(parser, token):
"""
KISSinsights tracking template tag.
Renders JavaScript code to track page visits. You must supply
your KISSmetrics API key in the ``KISS_METRICS_API_KEY``
setting.
"""
bits = token.split_contents()
if len(bits) > 1:
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
return KissMetricsNode()
class KissMetricsNode(Node):
def __init__(self):
self.api_key = get_required_setting(
'KISS_METRICS_API_KEY',
API_KEY_RE,
'must be a string containing a 40-digit hexadecimal number',
)
def render(self, context):
commands = []
identity = get_identity(context, 'kiss_metrics')
if identity is not None:
commands.append(IDENTIFY_CODE % identity)
try:
properties = context[ALIAS_CONTEXT_KEY]
key, value = properties.popitem()
commands.append(ALIAS_CODE % (key, value))
except KeyError:
pass
try:
name, properties = context[EVENT_CONTEXT_KEY]
commands.append(
EVENT_CODE
% {
'name': name,
'properties': json.dumps(properties, sort_keys=True),
}
)
except KeyError:
pass
try:
properties = context[PROPERTY_CONTEXT_KEY]
commands.append(
PROPERTY_CODE
% {
'properties': json.dumps(properties, sort_keys=True),
}
)
except KeyError:
pass
html = TRACKING_CODE % {
'api_key': self.api_key,
'commands': ' '.join(commands),
}
if is_internal_ip(context, 'KISS_METRICS'):
html = disable_html(html, 'KISSmetrics')
return html
def contribute_to_analytical(add_node):
KissMetricsNode() # ensure properly configured
add_node('head_top', KissMetricsNode)

View file

@ -1,60 +0,0 @@
"""
Lucky Orange template tags and filters.
"""
import re
from django.template import Library, Node, TemplateSyntaxError
from analytical.utils import disable_html, get_required_setting, is_internal_ip
LUCKYORANGE_TRACKING_CODE = """\
<script type='text/javascript'>
window.__lo_site_id = %(LUCKYORANGE_SITE_ID)s;
(function() {
var wa = document.createElement('script'); wa.type = 'text/javascript'; wa.async = true;
wa.src = 'https://d10lpsik1i8c69.cloudfront.net/w.js';
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(wa, s);
})();
</script>
"""
register = Library()
def _validate_no_args(token):
bits = token.split_contents()
if len(bits) > 1:
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
@register.tag
def luckyorange(parser, token):
"""
Lucky Orange template tag.
"""
_validate_no_args(token)
return LuckyOrangeNode()
class LuckyOrangeNode(Node):
def __init__(self):
self.site_id = get_required_setting(
'LUCKYORANGE_SITE_ID',
re.compile(r'^\d+$'),
'must be (a string containing) a number',
)
def render(self, context):
html = LUCKYORANGE_TRACKING_CODE % {'LUCKYORANGE_SITE_ID': self.site_id}
if is_internal_ip(context, 'LUCKYORANGE'):
return disable_html(html, 'Lucky Orange')
else:
return html
def contribute_to_analytical(add_node):
# ensure properly configured
LuckyOrangeNode()
add_node('head_bottom', LuckyOrangeNode)

View file

@ -1,152 +0,0 @@
"""
Matomo template tags and filters.
"""
import re
from collections import namedtuple
from itertools import chain
from django.conf import settings
from django.template import Library, Node, TemplateSyntaxError
from analytical.utils import (
disable_html,
get_identity,
get_required_setting,
is_internal_ip,
)
# 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>
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/matomo.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']);"
GIVE_CONSENT_CLASS = 'matomo_give_consent'
REMOVE_CONSENT_CLASS = 'matomo_remove_consent'
ASK_FOR_CONSENT_CODE = """
_paq.push(['requireConsent']);
var elements = document.getElementsByClassName("{}");
for (var i = 0; i < elements.length; i++) {{
elements[i].addEventListener("click",
function () {{
_paq.push(["forgetConsentGiven"]);
}}
);
}}
var elements = document.getElementsByClassName("{}");
for (var i = 0; i < elements.length; i++) {{
elements[i].addEventListener("click",
function () {{
_paq.push(["rememberConsentGiven"]);
}}
);
}}
""".format(REMOVE_CONSENT_CLASS, GIVE_CONSENT_CLASS)
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)
if getattr(settings, 'MATOMO_ASK_FOR_CONSENT', False):
commands.append(ASK_FOR_CONSENT_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)

View file

@ -1,92 +0,0 @@
"""
Mixpanel template tags and filters.
"""
import json
import re
from django.template import Library, Node, TemplateSyntaxError
from django.utils.safestring import mark_safe
from analytical.utils import (
disable_html,
get_identity,
get_required_setting,
is_internal_ip,
)
MIXPANEL_API_TOKEN_RE = re.compile(r'^[0-9a-f]{32}$')
TRACKING_CODE = """
<script>(function(e,b){if(!b.__SV){var a,f,i,g;window.mixpanel=b;a=e.createElement("script");a.type="text/javascript";a.async=!0;a.src=("https:"===e.location.protocol?"https:":"http:")+'//cdn.mxpnl.com/libs/mixpanel-2.2.min.js';f=e.getElementsByTagName("script")[0];f.parentNode.insertBefore(a,f);b._i=[];b.init=function(a,e,d){function f(b,h){var a=h.split(".");2==a.length&&(b=b[a[0]],h=a[1]);b[h]=function(){b.push([h].concat(Array.prototype.slice.call(arguments,0)))}}var c=b;"undefined"!==
typeof d?c=b[d]=[]:d="mixpanel";c.people=c.people||[];c.toString=function(b){var a="mixpanel";"mixpanel"!==d&&(a+="."+d);b||(a+=" (stub)");return a};c.people.toString=function(){return c.toString(1)+".people (stub)"};i="disable track track_pageview track_links track_forms register register_once alias unregister identify name_tag set_config people.set people.increment people.append people.track_charge people.clear_charges people.delete_user".split(" ");for(g=0;g<i.length;g++)f(c,i[g]);b._i.push([a,
e,d])};b.__SV=1.2}})(document,window.mixpanel||[]);
mixpanel.init('%(token)s');
%(commands)s
</script>
""" # noqa
IDENTIFY_CODE = "mixpanel.identify('%s');"
IDENTIFY_PROPERTIES = 'mixpanel.people.set(%s);'
EVENT_CODE = "mixpanel.track('%(name)s', %(properties)s);"
EVENT_CONTEXT_KEY = 'mixpanel_event'
register = Library()
@register.tag
def mixpanel(parser, token):
"""
Mixpanel tracking template tag.
Renders JavaScript code to track page visits. You must supply
your Mixpanel token in the ``MIXPANEL_API_TOKEN`` setting.
"""
bits = token.split_contents()
if len(bits) > 1:
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
return MixpanelNode()
class MixpanelNode(Node):
def __init__(self):
self._token = get_required_setting(
'MIXPANEL_API_TOKEN',
MIXPANEL_API_TOKEN_RE,
'must be a string containing a 32-digit hexadecimal number',
)
def render(self, context):
commands = []
identity = get_identity(context, 'mixpanel')
if identity is not None:
if isinstance(identity, dict):
commands.append(
IDENTIFY_CODE % identity.get('id', identity.get('username'))
)
commands.append(
IDENTIFY_PROPERTIES % json.dumps(identity, sort_keys=True)
)
else:
commands.append(IDENTIFY_CODE % identity)
try:
name, properties = context[EVENT_CONTEXT_KEY]
commands.append(
EVENT_CODE
% {
'name': name,
'properties': json.dumps(properties, sort_keys=True),
}
)
except KeyError:
pass
html = TRACKING_CODE % {
'token': self._token,
'commands': ' '.join(commands),
}
if is_internal_ip(context, 'MIXPANEL'):
html = disable_html(html, 'Mixpanel')
return mark_safe(html)
def contribute_to_analytical(add_node):
MixpanelNode() # ensure properly configured
add_node('head_bottom', MixpanelNode)

View file

@ -1,126 +0,0 @@
"""
Olark template tags.
"""
import json
import re
from django.template import Library, Node, TemplateSyntaxError
from analytical.utils import get_identity, get_required_setting
SITE_ID_RE = re.compile(r'^\d+-\d+-\d+-\d+$')
SETUP_CODE = """
<script type='text/javascript'>
/*{literal}<![CDATA[*/ window.olark||(function(k){var g=window,j=document,a=g.location.protocol=="https:"?"https:":"http:",i=k.name,b="load",h="addEventListener";(function(){g[i]=function(){(c.s=c.s||[]).push(arguments)};var c=g[i]._={},f=k.methods.length;while(f--){(function(l){g[i][l]=function(){g[i]("call",l,arguments)}})(k.methods[f])}c.l=k.loader;c.i=arguments.callee;c.p={0:+new Date};c.P=function(l){c.p[l]=new Date-c.p[0]};function e(){c.P(b);g[i](b)}g[h]?g[h](b,e,false):g.attachEvent("on"+b,e);c.P(1);var d=j.createElement("script"),m=document.getElementsByTagName("script")[0];d.type="text/javascript";d.async=true;d.src=a+"//"+c.l;m.parentNode.insertBefore(d,m);c.P(2)})()})({loader:(function(a){return "static.olark.com/jsclient/loader1.js?ts="+(a?a[1]:(+new Date))})(document.cookie.match(/olarkld=([0-9]+)/)),name:"olark",methods:["configure","extend","declare","identify"]}); olark.identify('%(site_id)s');/*]]>{/literal}*/
%(extra_code)s
</script>
""" # noqa
NICKNAME_CODE = "olark('api.chat.updateVisitorNickname', {snippet: '%s'});"
NICKNAME_CONTEXT_KEY = 'olark_nickname'
FULLNAME_CODE = "olark('api.visitor.updateFullName', {{fullName: '{0}'}});"
FULLNAME_CONTEXT_KEY = 'olark_fullname'
EMAIL_CODE = "olark('api.visitor.updateEmailAddress', {{emailAddress: '{0}'}});"
EMAIL_CONTEXT_KEY = 'olark_email'
STATUS_CODE = "olark('api.chat.updateVisitorStatus', {snippet: %s});"
STATUS_CONTEXT_KEY = 'olark_status'
MESSAGE_CODE = 'olark.configure(\'locale.%(key)s\', "%(msg)s");'
MESSAGE_KEYS = {
'welcome_title',
'chatting_title',
'unavailable_title',
'busy_title',
'away_message',
'loading_title',
'welcome_message',
'busy_message',
'chat_input_text',
'name_input_text',
'email_input_text',
'offline_note_message',
'send_button_text',
'offline_note_thankyou_text',
'offline_note_error_text',
'offline_note_sending_text',
'operator_is_typing_text',
'operator_has_stopped_typing_text',
'introduction_error_text',
'introduction_messages',
'introduction_submit_button_text',
}
register = Library()
@register.tag
def olark(parser, token):
"""
Olark set-up template tag.
Renders JavaScript code to set-up Olark chat. You must supply
your site ID in the ``OLARK_SITE_ID`` setting.
"""
bits = token.split_contents()
if len(bits) > 1:
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
return OlarkNode()
class OlarkNode(Node):
def __init__(self):
self.site_id = get_required_setting(
'OLARK_SITE_ID',
SITE_ID_RE,
"must be a string looking like 'XXXX-XXX-XX-XXXX'",
)
def render(self, context):
extra_code = []
try:
extra_code.append(NICKNAME_CODE % context[NICKNAME_CONTEXT_KEY])
except KeyError:
identity = get_identity(context, 'olark', self._get_nickname)
if identity is not None:
extra_code.append(NICKNAME_CODE % identity)
try:
extra_code.append(FULLNAME_CODE.format(context[FULLNAME_CONTEXT_KEY]))
except KeyError:
pass
try:
extra_code.append(EMAIL_CODE.format(context[EMAIL_CONTEXT_KEY]))
except KeyError:
pass
try:
extra_code.append(
STATUS_CODE % json.dumps(context[STATUS_CONTEXT_KEY], sort_keys=True)
)
except KeyError:
pass
extra_code.extend(self._get_configuration(context))
html = SETUP_CODE % {
'site_id': self.site_id,
'extra_code': ' '.join(extra_code),
}
return html
def _get_nickname(self, user):
name = user.get_full_name()
if name:
return '%s (%s)' % (name, user.username)
else:
return user.username
def _get_configuration(self, context):
code = []
for dict_ in context:
for var, val in dict_.items():
if var.startswith('olark_'):
key = var[6:]
if key in MESSAGE_KEYS:
code.append(MESSAGE_CODE % {'key': key, 'msg': val})
return code
def contribute_to_analytical(add_node):
OlarkNode() # ensure properly configured
add_node('body_bottom', OlarkNode)

View file

@ -1,50 +0,0 @@
"""
Optimizely template tags and filters.
"""
import re
from django.template import Library, Node, TemplateSyntaxError
from analytical.utils import disable_html, get_required_setting, is_internal_ip
ACCOUNT_NUMBER_RE = re.compile(r'^\d+$')
SETUP_CODE = """<script src="//cdn.optimizely.com/js/%(account_number)s.js"></script>"""
register = Library()
@register.tag
def optimizely(parser, token):
"""
Optimizely template tag.
Renders JavaScript code to set-up A/B testing. You must supply
your Optimizely account number in the ``OPTIMIZELY_ACCOUNT_NUMBER``
setting.
"""
bits = token.split_contents()
if len(bits) > 1:
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
return OptimizelyNode()
class OptimizelyNode(Node):
def __init__(self):
self.account_number = get_required_setting(
'OPTIMIZELY_ACCOUNT_NUMBER',
ACCOUNT_NUMBER_RE,
"must be a string looking like 'XXXXXXX'",
)
def render(self, context):
html = SETUP_CODE % {'account_number': self.account_number}
if is_internal_ip(context, 'OPTIMIZELY'):
html = disable_html(html, 'Optimizely')
return html
def contribute_to_analytical(add_node):
OptimizelyNode() # ensure properly configured
add_node('head_top', OptimizelyNode)

View file

@ -1,88 +0,0 @@
"""
Performable template tags and filters.
"""
import re
from django.template import Library, Node, TemplateSyntaxError
from django.utils.safestring import mark_safe
from analytical.utils import (
disable_html,
get_identity,
get_required_setting,
is_internal_ip,
)
API_KEY_RE = re.compile(r'^\w+$')
SETUP_CODE = """
<script src="//d1nu2rn22elx8m.cloudfront.net/performable/pax/%(api_key)s.js"></script>
""" # noqa
IDENTIFY_CODE = """
<script>
var _paq = _paq || [];
_paq.push(["identify", {identity: "%s"}]);
</script>
"""
EMBED_CODE = """
<script src="//d1nu2rn22elx8m.cloudfront.net/performable/embed/page.js"></script>
<script>
(function() {
var $f = new PerformableEmbed();
$f.initialize({'host': '%(hostname)s', 'page': '%(page_id)s'});
$f.write();
})()
</script>
""" # noqa
register = Library()
@register.tag
def performable(parser, token):
"""
Performable template tag.
Renders JavaScript code to set-up Performable tracking. You must
supply your Performable API key in the ``PERFORMABLE_API_KEY``
setting.
"""
bits = token.split_contents()
if len(bits) > 1:
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
return PerformableNode()
class PerformableNode(Node):
def __init__(self):
self.api_key = get_required_setting(
'PERFORMABLE_API_KEY', API_KEY_RE, "must be a string looking like 'XXXXX'"
)
def render(self, context):
html = SETUP_CODE % {'api_key': self.api_key}
identity = get_identity(context, 'performable')
if identity is not None:
html = '%s%s' % (IDENTIFY_CODE % identity, html)
if is_internal_ip(context, 'PERFORMABLE'):
html = disable_html(html, 'Performable')
return html
@register.simple_tag
def performable_embed(hostname, page_id):
"""
Include a Performable landing page.
"""
return mark_safe(
EMBED_CODE
% {
'hostname': hostname,
'page_id': page_id,
}
)
def contribute_to_analytical(add_node):
PerformableNode() # ensure properly configured
add_node('body_bottom', PerformableNode)

View file

@ -1,67 +0,0 @@
"""
Rating@Mail.ru template tags and filters.
"""
import re
from django.template import Library, Node, TemplateSyntaxError
from analytical.utils import disable_html, get_required_setting, is_internal_ip
COUNTER_ID_RE = re.compile(r'^\d{7}$')
COUNTER_CODE = """
<script>
var _tmr = window._tmr || (window._tmr = []);
_tmr.push({id: "%(counter_id)s", type: "pageView", start: (new Date()).getTime()});
(function (d, w, id) {
if (d.getElementById(id)) return;
var ts = d.createElement("script"); ts.type = "text/javascript"; ts.async = true; ts.id = id;
ts.src = (d.location.protocol == "https:" ? "https:" : "http:") + "//top-fwz1.mail.ru/js/code.js";
var f = function () {var s = d.getElementsByTagName("script")[0]; s.parentNode.insertBefore(ts, s);};
if (w.opera == "[object Opera]") { d.addEventListener("DOMContentLoaded", f, false); } else { f(); }
})(document, window, "topmailru-code");
</script>
<noscript><div style="position:absolute;left:-10000px;">
<img src="//top-fwz1.mail.ru/counter?id=%(counter_id)s;js=na" style="border:0;" height="1" width="1" alt="Rating@Mail.ru" />
</div></noscript>
""" # noqa
register = Library()
@register.tag
def rating_mailru(parser, token):
"""
Rating@Mail.ru counter template tag.
Renders JavaScript code to track page visits. You must supply
your website counter ID (as a string) in the
``RATING_MAILRU_COUNTER_ID`` setting.
"""
bits = token.split_contents()
if len(bits) > 1:
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
return RatingMailruNode()
class RatingMailruNode(Node):
def __init__(self):
self.counter_id = get_required_setting(
'RATING_MAILRU_COUNTER_ID',
COUNTER_ID_RE,
"must be (a string containing) a number'",
)
def render(self, context):
html = COUNTER_CODE % {
'counter_id': self.counter_id,
}
if is_internal_ip(context, 'RATING_MAILRU_METRICA'):
html = disable_html(html, 'Rating@Mail.ru')
return html
def contribute_to_analytical(add_node):
RatingMailruNode() # ensure properly configured
add_node('head_bottom', RatingMailruNode)

View file

@ -1,208 +0,0 @@
"""
SnapEngage template tags.
"""
import re
from django.conf import settings
from django.template import Library, Node, TemplateSyntaxError
from django.utils import translation
from analytical.utils import get_identity, get_required_setting
BUTTON_LOCATION_LEFT = 0
BUTTON_LOCATION_RIGHT = 1
BUTTON_LOCATION_TOP = 2
BUTTON_LOCATION_BOTTOM = 3
BUTTON_STYLE_NONE = 0
BUTTON_STYLE_DEFAULT = 1
BUTTON_STYLE_LIVE = 2
FORM_POSITION_TOP_LEFT = 'tl'
FORM_POSITION_TOP_RIGHT = 'tr'
FORM_POSITION_BOTTOM_LEFT = 'bl'
FORM_POSITION_BOTTOM_RIGHT = 'br'
WIDGET_ID_RE = re.compile(
r'^[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}$'
)
SETUP_CODE = """
<script>
document.write(unescape("%%3Cscript src='" + ((document.location.protocol=="https:")?"https://snapabug.appspot.com":"http://www.snapengage.com") + "/snapabug.js' type='text/javascript'%%3E%%3C/script%%3E"));</script><script>
%(settings_code)s
</script>
""" # noqa
DOMAIN_CODE = 'SnapABug.setDomain("%s");'
SECURE_CONNECTION_CODE = 'SnapABug.setSecureConnexion();'
INIT_CODE = 'SnapABug.init("%s");'
ADDBUTTON_CODE = (
'SnapABug.addButton("%(id)s","%(location)s","%(offset)s"%(dynamic_tail)s);'
)
SETBUTTON_CODE = 'SnapABug.setButton("%s");'
SETEMAIL_CODE = 'SnapABug.setUserEmail("%s"%s);'
SETLOCALE_CODE = 'SnapABug.setLocale("%s");'
FORM_POSITION_CODE = 'SnapABug.setChatFormPosition("%s");'
FORM_TOP_POSITION_CODE = 'SnapABug.setFormTopPosition(%d);'
BUTTONEFFECT_CODE = 'SnapABug.setButtonEffect("%s");'
DISABLE_OFFLINE_CODE = 'SnapABug.allowOffline(false);'
DISABLE_SCREENSHOT_CODE = 'SnapABug.allowScreenshot(false);'
DISABLE_OFFLINE_SCREENSHOT_CODE = 'SnapABug.showScreenshotOption(false);'
DISABLE_PROACTIVE_CHAT_CODE = 'SnapABug.allowProactiveChat(false);'
DISABLE_SOUNDS_CODE = 'SnapABug.allowChatSound(false);'
register = Library()
@register.tag
def snapengage(parser, token):
"""
SnapEngage set-up template tag.
Renders JavaScript code to set-up SnapEngage chat. You must supply
your widget ID in the ``SNAPENGAGE_WIDGET_ID`` setting.
"""
bits = token.split_contents()
if len(bits) > 1:
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
return SnapEngageNode()
class SnapEngageNode(Node):
def __init__(self):
self.widget_id = get_required_setting(
'SNAPENGAGE_WIDGET_ID',
WIDGET_ID_RE,
"must be a string looking like this: 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX'",
)
def render(self, context):
settings_code = []
domain = self._get_setting(context, 'snapengage_domain', 'SNAPENGAGE_DOMAIN')
if domain is not None:
settings_code.append(DOMAIN_CODE % domain)
secure_connection = self._get_setting(
context,
'snapengage_secure_connection',
'SNAPENGAGE_SECURE_CONNECTION',
False,
)
if secure_connection:
settings_code.append(SECURE_CONNECTION_CODE)
email = context.get('snapengage_email')
if email is None:
email = get_identity(context, 'snapengage', lambda u: u.email)
if email is not None:
if self._get_setting(
context, 'snapengage_readonly_email', 'SNAPENGAGE_READONLY_EMAIL', False
):
readonly_tail = ',true'
else:
readonly_tail = ''
settings_code.append(SETEMAIL_CODE % (email, readonly_tail))
locale = self._get_setting(context, 'snapengage_locale', 'SNAPENGAGE_LOCALE')
if locale is None:
locale = translation.to_locale(translation.get_language())
settings_code.append(SETLOCALE_CODE % locale)
form_position = self._get_setting(
context, 'snapengage_form_position', 'SNAPENGAGE_FORM_POSITION'
)
if form_position is not None:
settings_code.append(FORM_POSITION_CODE % form_position)
form_top_position = self._get_setting(
context, 'snapengage_form_top_position', 'SNAPENGAGE_FORM_TOP_POSITION'
)
if form_top_position is not None:
settings_code.append(FORM_TOP_POSITION_CODE % form_top_position)
show_offline = self._get_setting(
context, 'snapengage_show_offline', 'SNAPENGAGE_SHOW_OFFLINE', True
)
if not show_offline:
settings_code.append(DISABLE_OFFLINE_CODE)
screenshots = self._get_setting(
context, 'snapengage_screenshots', 'SNAPENGAGE_SCREENSHOTS', True
)
if not screenshots:
settings_code.append(DISABLE_SCREENSHOT_CODE)
offline_screenshots = self._get_setting(
context,
'snapengage_offline_screenshots',
'SNAPENGAGE_OFFLINE_SCREENSHOTS',
True,
)
if not offline_screenshots:
settings_code.append(DISABLE_OFFLINE_SCREENSHOT_CODE)
if not context.get('snapengage_proactive_chat', True):
settings_code.append(DISABLE_PROACTIVE_CHAT_CODE)
sounds = self._get_setting(
context, 'snapengage_sounds', 'SNAPENGAGE_SOUNDS', True
)
if not sounds:
settings_code.append(DISABLE_SOUNDS_CODE)
button_effect = self._get_setting(
context, 'snapengage_button_effect', 'SNAPENGAGE_BUTTON_EFFECT'
)
if button_effect is not None:
settings_code.append(BUTTONEFFECT_CODE % button_effect)
button = self._get_setting(
context, 'snapengage_button', 'SNAPENGAGE_BUTTON', BUTTON_STYLE_DEFAULT
)
if button == BUTTON_STYLE_NONE:
settings_code.append(INIT_CODE % self.widget_id)
else:
if not isinstance(button, int):
# Assume button as a URL to a custom image
settings_code.append(SETBUTTON_CODE % button)
button_location = self._get_setting(
context,
'snapengage_button_location',
'SNAPENGAGE_BUTTON_LOCATION',
BUTTON_LOCATION_LEFT,
)
button_offset = self._get_setting(
context,
'snapengage_button_location_offset',
'SNAPENGAGE_BUTTON_LOCATION_OFFSET',
'55%',
)
settings_code.append(
ADDBUTTON_CODE
% {
'id': self.widget_id,
'location': button_location,
'offset': button_offset,
'dynamic_tail': ',true' if (button == BUTTON_STYLE_LIVE) else '',
}
)
html = SETUP_CODE % {
'widget_id': self.widget_id,
'settings_code': ' '.join(settings_code),
}
return html
def _get_setting(self, context, context_key, setting=None, default=None):
try:
return context[context_key]
except KeyError:
if setting is not None:
return getattr(settings, setting, default)
else:
return default
def contribute_to_analytical(add_node):
SnapEngageNode() # ensure properly configured
add_node('body_bottom', SnapEngageNode)

View file

@ -1,92 +0,0 @@
"""
Spring Metrics template tags and filters.
"""
import re
from django.template import Library, Node, TemplateSyntaxError
from analytical.utils import (
disable_html,
get_identity,
get_required_setting,
is_internal_ip,
)
TRACKING_ID_RE = re.compile(r'^[0-9a-f]+$')
TRACKING_CODE = """
<script type='text/javascript'>
var _springMetq = _springMetq || [];
_springMetq.push(['id', '%(tracking_id)s']);
(
function(){
var s = document.createElement('script');
s.type = 'text/javascript';
s.async = true;
s.src = ('https:' == document.location.protocol ? 'https://d3rmnwi2tssrfx.cloudfront.net/a.js' : 'http://static.springmetrics.com/a.js');
var x = document.getElementsByTagName('script')[0];
x.parentNode.insertBefore(s, x);
}
)();
%(custom_commands)s
</script>
""" # noqa
register = Library()
@register.tag
def spring_metrics(parser, token):
"""
Spring Metrics tracking template tag.
Renders JavaScript code to track page visits. You must supply
your Spring Metrics Tracking ID in the
``SPRING_METRICS_TRACKING_ID`` setting.
"""
bits = token.split_contents()
if len(bits) > 1:
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
return SpringMetricsNode()
class SpringMetricsNode(Node):
def __init__(self):
self.tracking_id = get_required_setting(
'SPRING_METRICS_TRACKING_ID', TRACKING_ID_RE, 'must be a hexadecimal string'
)
def render(self, context):
custom = {}
for dict_ in context:
for var, val in dict_.items():
if var.startswith('spring_metrics_'):
custom[var[15:]] = val
if 'email' not in custom:
identity = get_identity(context, 'spring_metrics', lambda u: u.email)
if identity is not None:
custom['email'] = identity
html = TRACKING_CODE % {
'tracking_id': self.tracking_id,
'custom_commands': self._generate_custom_javascript(custom),
}
if is_internal_ip(context, 'SPRING_METRICS'):
html = disable_html(html, 'Spring Metrics')
return html
def _generate_custom_javascript(self, params):
commands = []
convert = params.pop('convert', None)
if convert is not None:
commands.append("_springMetq.push(['convert', '%s'])" % convert)
commands.extend(
"_springMetq.push(['setdata', {'%s': '%s'}]);" % (var, val)
for var, val in params.items()
)
return ' '.join(commands)
def contribute_to_analytical(add_node):
SpringMetricsNode() # ensure properly configured
add_node('head_bottom', SpringMetricsNode)

View file

@ -1,90 +0,0 @@
"""
UserVoice template tags.
"""
import json
import re
from django.conf import settings
from django.template import Library, Node, TemplateSyntaxError
from analytical.utils import get_identity, get_required_setting
WIDGET_KEY_RE = re.compile(r'^[a-zA-Z0-9]*$')
TRACKING_CODE = """
<script>
UserVoice=window.UserVoice||[];(function(){
var uv=document.createElement('script');uv.type='text/javascript';
uv.async=true;uv.src='//widget.uservoice.com/%(widget_key)s.js';
var s=document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(uv,s)})();
UserVoice.push(['set', %(options)s]);
%(trigger)s
%(identity)s
</script>
"""
IDENTITY = """UserVoice.push(['identify', %(options)s]);"""
TRIGGER = "UserVoice.push(['addTrigger', {}]);"
register = Library()
@register.tag
def uservoice(parser, token):
"""
UserVoice tracking template tag.
Renders JavaScript code to track page visits. You must supply
your UserVoice Widget Key in the ``USERVOICE_WIDGET_KEY``
setting or the ``uservoice_widget_key`` template context variable.
"""
bits = token.split_contents()
if len(bits) > 1:
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
return UserVoiceNode()
class UserVoiceNode(Node):
def __init__(self):
self.default_widget_key = get_required_setting(
'USERVOICE_WIDGET_KEY', WIDGET_KEY_RE, 'must be an alphanumeric string'
)
def render(self, context):
widget_key = context.get('uservoice_widget_key')
if not widget_key:
widget_key = self.default_widget_key
if not widget_key:
return ''
# default
options = {}
options.update(getattr(settings, 'USERVOICE_WIDGET_OPTIONS', {}))
options.update(context.get('uservoice_widget_options', {}))
identity = get_identity(context, 'uservoice', self._identify)
if identity:
identity = IDENTITY % {'options': json.dumps(identity, sort_keys=True)}
trigger = context.get(
'uservoice_add_trigger', getattr(settings, 'USERVOICE_ADD_TRIGGER', True)
)
html = TRACKING_CODE % {
'widget_key': widget_key,
'options': json.dumps(options, sort_keys=True),
'trigger': TRIGGER if trigger else '',
'identity': identity if identity else '',
}
return html
def _identify(self, user):
name = user.get_full_name()
if not name:
name = user.username
return {'name': name, 'email': user.email}
def contribute_to_analytical(add_node):
UserVoiceNode() # ensure properly configured
add_node('body_bottom', UserVoiceNode)

View file

@ -1,132 +0,0 @@
"""
Woopra template tags and filters.
"""
import json
import re
from contextlib import suppress
from django.conf import settings
from django.template import Library, Node, TemplateSyntaxError
from analytical.utils import (
AnalyticalException,
disable_html,
get_identity,
get_required_setting,
get_user_from_context,
get_user_is_authenticated,
is_internal_ip,
)
DOMAIN_RE = re.compile(r'^\S+$')
TRACKING_CODE = """
<script>
var woo_settings = %(settings)s;
var woo_visitor = %(visitor)s;
!function(){var a,b,c,d=window,e=document,f=arguments,g="script",h=["config","track","trackForm","trackClick","identify","visit","push","call"],i=function(){var a,b=this,c=function(a){b[a]=function(){return b._e.push([a].concat(Array.prototype.slice.call(arguments,0))),b}};for(b._e=[],a=0;a<h.length;a++)c(h[a])};for(d.__woo=d.__woo||{},a=0;a<f.length;a++)d.__woo[f[a]]=d[f[a]]=d[f[a]]||new i;b=e.createElement(g),b.async=1,b.src="//static.woopra.com/js/w.js",c=e.getElementsByTagName(g)[0],c.parentNode.insertBefore(b,c)}("woopra");
woopra.config(woo_settings);
woopra.identify(woo_visitor);
woopra.track();
</script>
""" # noqa
register = Library()
@register.tag
def woopra(parser, token):
"""
Woopra tracking template tag.
Renders JavaScript code to track page visits. You must supply
your Woopra domain in the ``WOOPRA_DOMAIN`` setting.
"""
bits = token.split_contents()
if len(bits) > 1:
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
return WoopraNode()
class WoopraNode(Node):
def __init__(self):
self.domain = get_required_setting(
'WOOPRA_DOMAIN', DOMAIN_RE, 'must be a domain name'
)
def render(self, context):
settings = self._get_settings(context)
visitor = self._get_visitor(context)
html = TRACKING_CODE % {
'settings': json.dumps(settings, sort_keys=True),
'visitor': json.dumps(visitor, sort_keys=True),
}
if is_internal_ip(context, 'WOOPRA'):
html = disable_html(html, 'Woopra')
return html
def _get_settings(self, context):
variables = {'domain': self.domain}
woopra_int_settings = {
'idle_timeout': 'WOOPRA_IDLE_TIMEOUT',
}
woopra_str_settings = {
'cookie_name': 'WOOPRA_COOKIE_NAME',
'cookie_domain': 'WOOPRA_COOKIE_DOMAIN',
'cookie_path': 'WOOPRA_COOKIE_PATH',
'cookie_expire': 'WOOPRA_COOKIE_EXPIRE',
}
woopra_bool_settings = {
'click_tracking': 'WOOPRA_CLICK_TRACKING',
'download_tracking': 'WOOPRA_DOWNLOAD_TRACKING',
'outgoing_tracking': 'WOOPRA_OUTGOING_TRACKING',
'outgoing_ignore_subdomain': 'WOOPRA_OUTGOING_IGNORE_SUBDOMAIN',
'ignore_query_url': 'WOOPRA_IGNORE_QUERY_URL',
'hide_campaign': 'WOOPRA_HIDE_CAMPAIGN',
}
for key, name in woopra_int_settings.items():
with suppress(AttributeError):
variables[key] = getattr(settings, name)
if type(variables[key]) is not int:
raise AnalyticalException(f'{name} must be an int value')
for key, name in woopra_str_settings.items():
with suppress(AttributeError):
variables[key] = getattr(settings, name)
if type(variables[key]) is not str:
raise AnalyticalException(f'{name} must be a string value')
for key, name in woopra_bool_settings.items():
with suppress(AttributeError):
variables[key] = getattr(settings, name)
if type(variables[key]) is not bool:
raise AnalyticalException(f'{name} must be a boolean value')
return variables
def _get_visitor(self, context):
params = {}
for dict_ in context:
for var, val in dict_.items():
if var.startswith('woopra_'):
params[var[7:]] = val
if 'name' not in params and 'email' not in params:
user = get_user_from_context(context)
if user is not None and get_user_is_authenticated(user):
params['name'] = get_identity(context, 'woopra', self._identify, user)
if user.email:
params['email'] = user.email
return params
def _identify(self, user):
name = user.get_full_name()
if not name:
name = user.username
return name
def contribute_to_analytical(add_node):
WoopraNode() # ensure properly configured
add_node('head_bottom', WoopraNode)

View file

@ -1,91 +0,0 @@
"""
Yandex.Metrica template tags and filters.
"""
import json
import re
from django.conf import settings
from django.template import Library, Node, TemplateSyntaxError
from analytical.utils import disable_html, get_required_setting, is_internal_ip
COUNTER_ID_RE = re.compile(r'^\d{8}$')
COUNTER_CODE = """
<script>
(function (d, w, c) {
(w[c] = w[c] || []).push(function() {
try {
w.yaCounter%(counter_id)s = new Ya.Metrika(%(options)s);
} catch(e) { }
});
var n = d.getElementsByTagName("script")[0],
s = d.createElement("script"),
f = function () { n.parentNode.insertBefore(s, n); };
s.type = "text/javascript";
s.async = true;
s.src = "https://mc.yandex.ru/metrika/watch.js";
if (w.opera == "[object Opera]") {
d.addEventListener("DOMContentLoaded", f, false);
} else { f(); }
})(document, window, "yandex_metrika_callbacks");
</script>
<noscript><div><img src="https://mc.yandex.ru/watch/%(counter_id)s" style="position:absolute; left:-9999px;" alt="" /></div></noscript>
""" # noqa
register = Library()
@register.tag
def yandex_metrica(parser, token):
"""
Yandex.Metrica counter template tag.
Renders JavaScript code to track page visits. You must supply
your website counter ID (as a string) in the
``YANDEX_METRICA_COUNTER_ID`` setting.
"""
bits = token.split_contents()
if len(bits) > 1:
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
return YandexMetricaNode()
class YandexMetricaNode(Node):
def __init__(self):
self.counter_id = get_required_setting(
'YANDEX_METRICA_COUNTER_ID',
COUNTER_ID_RE,
"must be (a string containing) a number'",
)
def render(self, context):
options = {
'id': int(self.counter_id),
'clickmap': True,
'trackLinks': True,
'accurateTrackBounce': True,
}
if getattr(settings, 'YANDEX_METRICA_WEBVISOR', False):
options['webvisor'] = True
if getattr(settings, 'YANDEX_METRICA_TRACKHASH', False):
options['trackHash'] = True
if getattr(settings, 'YANDEX_METRICA_NOINDEX', False):
options['ut'] = 'noindex'
if getattr(settings, 'YANDEX_METRICA_ECOMMERCE', False):
options['ecommerce'] = 'dataLayer'
html = COUNTER_CODE % {
'counter_id': self.counter_id,
'options': json.dumps(options),
}
if is_internal_ip(context, 'YANDEX_METRICA'):
html = disable_html(html, 'Yandex.Metrica')
return html
def contribute_to_analytical(add_node):
YandexMetricaNode() # ensure properly configured
add_node('head_bottom', YandexMetricaNode)

View file

@ -0,0 +1,8 @@
"""
Tests for django-analytical.
"""
from analytical.tests.test_services import *
from analytical.tests.test_template_tags import *
from analytical.tests.services import *

View file

@ -0,0 +1,14 @@
"""
Tests for the Analytical analytics services.
"""
from analytical.tests.services.test_base import *
from analytical.tests.services.test_chartbeat import *
from analytical.tests.services.test_clicky import *
from analytical.tests.services.test_console import *
from analytical.tests.services.test_crazy_egg import *
from analytical.tests.services.test_google_analytics import *
from analytical.tests.services.test_kiss_insights import *
from analytical.tests.services.test_kiss_metrics import *
from analytical.tests.services.test_mixpanel import *
from analytical.tests.services.test_optimizely import *

View file

@ -0,0 +1,74 @@
"""
Tests for the base service.
"""
import re
from django.contrib.auth.models import User, AnonymousUser
from django.core.exceptions import ImproperlyConfigured
from django.http import HttpRequest
from django.test import TestCase
from analytical.services.base import AnalyticalService
from analytical.tests.utils import TestSettingsManager
class DummyService(AnalyticalService):
def render_test(self, context):
return context
class BaseServiceTestCase(TestCase):
"""
Tests for the base service.
"""
def setUp(self):
self.settings_manager = TestSettingsManager()
self.service = DummyService()
def tearDown(self):
self.settings_manager.revert()
def test_render(self):
r = self.service.render('test', 'foo')
self.assertEqual('foo', r)
def test_get_required_setting(self):
self.settings_manager.set(TEST='test')
r = self.service.get_required_setting('TEST', re.compile('es'), 'fail')
self.assertEqual('test', r)
def test_get_required_setting_missing(self):
self.settings_manager.delete('TEST')
self.assertRaises(ImproperlyConfigured,
self.service.get_required_setting, 'TEST', re.compile('es'),
'fail')
def test_get_required_setting_wrong(self):
self.settings_manager.set(TEST='test')
self.assertRaises(ImproperlyConfigured,
self.service.get_required_setting, 'TEST', re.compile('foo'),
'fail')
def test_get_identity_none(self):
context = {}
self.assertEqual(None, self.service.get_identity(context))
def test_get_identity_authenticated(self):
context = {'user': User(username='test')}
self.assertEqual('test', self.service.get_identity(context))
def test_get_identity_authenticated_request(self):
req = HttpRequest()
req.user = User(username='test')
context = {'request': req}
self.assertEqual('test', self.service.get_identity(context))
def test_get_identity_anonymous(self):
context = {'user': AnonymousUser()}
self.assertEqual(None, self.service.get_identity(context))
def test_get_identity_non_user(self):
context = {'user': object()}
self.assertEqual(None, self.service.get_identity(context))

View file

@ -0,0 +1,72 @@
"""
Tests for the Chartbeat service.
"""
import re
from django.conf import settings
from django.contrib.sites.models import Site
from django.core.exceptions import ImproperlyConfigured
from django.http import HttpRequest
from django.test import TestCase
from analytical.services.chartbeat import ChartbeatService
from analytical.tests.utils import TestSettingsManager
class ChartbeatTestCase(TestCase):
"""
Tests for the Chartbeat service.
"""
def setUp(self):
self.settings_manager = TestSettingsManager()
self.settings_manager.set(CHARTBEAT_USER_ID='12345')
self.service = ChartbeatService()
def tearDown(self):
self.settings_manager.revert()
def test_empty_locations(self):
self.assertEqual(self.service.render_head_bottom({}), "")
self.assertEqual(self.service.render_body_top({}), "")
def test_no_user_id(self):
self.settings_manager.delete('CHARTBEAT_USER_ID')
self.assertRaises(ImproperlyConfigured, ChartbeatService)
def test_wrong_user_id(self):
self.settings_manager.set(CHARTBEAT_USER_ID='1234')
self.assertRaises(ImproperlyConfigured, ChartbeatService)
self.settings_manager.set(CHARTBEAT_USER_ID='123456')
self.assertRaises(ImproperlyConfigured, ChartbeatService)
def test_rendering_init(self):
r = self.service.render_head_top({})
self.assertTrue('var _sf_startpt=(new Date()).getTime()' in r, r)
def test_rendering_setup(self):
r = self.service.render_body_bottom({'chartbeat_domain': "test.com"})
self.assertTrue(re.search(
'var _sf_async_config={.*"uid": "12345".*};', r), r)
self.assertTrue(re.search(
'var _sf_async_config={.*"domain": "test.com".*};', r), r)
def test_rendering_setup_no_site(self):
installed_apps = [a for a in settings.INSTALLED_APPS
if a != 'django.contrib.sites']
self.settings_manager.set(INSTALLED_APPS=installed_apps)
r = self.service.render_body_bottom({})
self.assertTrue('var _sf_async_config={"uid": "12345"};' in r, r)
def test_rendering_setup_site(self):
installed_apps = list(settings.INSTALLED_APPS)
installed_apps.append('django.contrib.sites')
self.settings_manager.set(INSTALLED_APPS=installed_apps)
site = Site.objects.create(domain="test.com", name="test")
self.settings_manager.set(SITE_ID=site.id)
r = self.service.render_body_bottom({})
self.assertTrue(re.search(
'var _sf_async_config={.*"uid": "12345".*};', r), r)
self.assertTrue(re.search(
'var _sf_async_config={.*"domain": "test.com".*};', r), r)

View file

@ -0,0 +1,60 @@
"""
Tests for the Clicky service.
"""
import re
from django.contrib.auth.models import User
from django.core.exceptions import ImproperlyConfigured
from django.test import TestCase
from analytical.services.clicky import ClickyService
from analytical.tests.utils import TestSettingsManager
class ClickyTestCase(TestCase):
"""
Tests for the Clicky service.
"""
def setUp(self):
self.settings_manager = TestSettingsManager()
self.settings_manager.set(CLICKY_SITE_ID='12345678')
self.service = ClickyService()
def tearDown(self):
self.settings_manager.revert()
def test_empty_locations(self):
self.assertEqual(self.service.render_head_top({}), "")
self.assertEqual(self.service.render_head_bottom({}), "")
self.assertEqual(self.service.render_body_top({}), "")
def test_no_site_id(self):
self.settings_manager.delete('CLICKY_SITE_ID')
self.assertRaises(ImproperlyConfigured, ClickyService)
def test_wrong_site_id(self):
self.settings_manager.set(CLICKY_SITE_ID='1234567')
self.assertRaises(ImproperlyConfigured, ClickyService)
self.settings_manager.set(CLICKY_SITE_ID='123456789')
self.assertRaises(ImproperlyConfigured, ClickyService)
def test_rendering(self):
r = self.service.render_body_bottom({})
self.assertTrue('var clicky_site_id = 12345678;' in r, r)
self.assertTrue('src="http://in.getclicky.com/12345678ns.gif"' in r,
r)
def test_identify(self):
self.settings_manager.set(ANALYTICAL_AUTO_IDENTIFY=True)
r = self.service.render_body_bottom({'user': User(username='test')})
self.assertTrue(
'var clicky_custom = {"session": {"username": "test"}};' in r,
r)
def test_custom(self):
custom = {'var1': 'val1', 'var2': 'val2'}
r = self.service.render_body_bottom({'clicky_custom': custom})
self.assertTrue(re.search('var clicky_custom = {.*'
'"var1": "val1", "var2": "val2".*};', r), r)

View file

@ -0,0 +1,45 @@
"""
Tests for the console debugging service.
"""
from django.contrib.auth.models import User
from django.test import TestCase
from analytical.services.console import ConsoleService
class ConsoleTestCase(TestCase):
"""
Tests for the console debugging service.
"""
def setUp(self):
self.service = ConsoleService()
def test_render_head_top(self):
r = self.service.render_head_top({})
self.assertTrue('rendering analytical_head_top tag' in r, r)
r = self.service.render_head_top({'user': User(username='test')})
self.assertTrue('rendering analytical_head_top tag for user test'
in r, r)
def test_render_head_bottom(self):
r = self.service.render_head_bottom({})
self.assertTrue('rendering analytical_head_bottom tag' in r, r)
r = self.service.render_head_bottom({'user': User(username='test')})
self.assertTrue('rendering analytical_head_bottom tag for user test'
in r, r)
def test_render_body_top(self):
r = self.service.render_body_top({})
self.assertTrue('rendering analytical_body_top tag' in r, r)
r = self.service.render_body_top({'user': User(username='test')})
self.assertTrue('rendering analytical_body_top tag for user test'
in r, r)
def test_render_body_bottom(self):
r = self.service.render_body_bottom({})
self.assertTrue('rendering analytical_body_bottom tag' in r, r)
r = self.service.render_body_bottom({'user': User(username='test')})
self.assertTrue('rendering analytical_body_bottom tag for user test'
in r, r)

View file

@ -0,0 +1,48 @@
"""
Tests for the Crazy Egg service.
"""
from django.core.exceptions import ImproperlyConfigured
from django.test import TestCase
from analytical.services.crazy_egg import CrazyEggService
from analytical.tests.utils import TestSettingsManager
class CrazyEggTestCase(TestCase):
"""
Tests for the Crazy Egg service.
"""
def setUp(self):
self.settings_manager = TestSettingsManager()
self.settings_manager.set(CRAZY_EGG_ACCOUNT_NUMBER='12345678')
self.service = CrazyEggService()
def tearDown(self):
self.settings_manager.revert()
def test_empty_locations(self):
self.assertEqual(self.service.render_head_top({}), "")
self.assertEqual(self.service.render_head_bottom({}), "")
self.assertEqual(self.service.render_body_top({}), "")
def test_no_account_number(self):
self.settings_manager.delete('CRAZY_EGG_ACCOUNT_NUMBER')
self.assertRaises(ImproperlyConfigured, CrazyEggService)
def test_wrong_account_number(self):
self.settings_manager.set(CRAZY_EGG_ACCOUNT_NUMBER='1234567')
self.assertRaises(ImproperlyConfigured, CrazyEggService)
self.settings_manager.set(CRAZY_EGG_ACCOUNT_NUMBER='123456789')
self.assertRaises(ImproperlyConfigured, CrazyEggService)
def test_rendering(self):
r = self.service.render_body_bottom({})
self.assertTrue('/1234/5678.js' in r, r)
def test_uservars(self):
context = {'crazy_egg_uservars': {1: 'foo', 2: 'bar'}}
r = self.service.render_body_bottom(context)
self.assertTrue("CE2.set(1, 'foo');" in r, r)
self.assertTrue("CE2.set(2, 'bar');" in r, r)

View file

@ -0,0 +1,56 @@
"""
Tests for the Google Analytics service.
"""
from django.core.exceptions import ImproperlyConfigured
from django.test import TestCase
from analytical.services.google_analytics import GoogleAnalyticsService
from analytical.tests.utils import TestSettingsManager
class GoogleAnalyticsTestCase(TestCase):
"""
Tests for the Google Analytics service.
"""
def setUp(self):
self.settings_manager = TestSettingsManager()
self.settings_manager.set(GOOGLE_ANALYTICS_PROPERTY_ID='UA-123456-7')
self.service = GoogleAnalyticsService()
def tearDown(self):
self.settings_manager.revert()
def test_empty_locations(self):
self.assertEqual(self.service.render_head_top({}), "")
self.assertEqual(self.service.render_body_top({}), "")
self.assertEqual(self.service.render_body_bottom({}), "")
def test_no_property_id(self):
self.settings_manager.delete('GOOGLE_ANALYTICS_PROPERTY_ID')
self.assertRaises(ImproperlyConfigured, GoogleAnalyticsService)
def test_wrong_property_id(self):
self.settings_manager.set(GOOGLE_ANALYTICS_PROPERTY_ID='wrong')
self.assertRaises(ImproperlyConfigured, GoogleAnalyticsService)
def test_rendering(self):
r = self.service.render_head_bottom({})
self.assertTrue("_gaq.push(['_setAccount', 'UA-123456-7']);" in r, r)
self.assertTrue("_gaq.push(['_trackPageview']);" in r, r)
def test_custom_vars(self):
context = {'google_analytics_custom_vars': [
(1, 'test1', 'foo'),
(5, 'test2', 'bar', 1),
]}
r = self.service.render_head_bottom(context)
self.assertTrue("_gaq.push(['_setCustomVar', 1, 'test1', 'foo', 2]);"
in r, r)
self.assertTrue("_gaq.push(['_setCustomVar', 5, 'test2', 'bar', 1]);"
in r, r)
self.assertRaises(ValueError, self.service.render_head_bottom,
{'google_analytics_custom_vars': [(0, 'test', 'test')]})
self.assertRaises(ValueError, self.service.render_head_bottom,
{'google_analytics_custom_vars': [(6, 'test', 'test')]})

View file

@ -0,0 +1,63 @@
"""
Tests for the KISSinsights service.
"""
from django.contrib.auth.models import User
from django.core.exceptions import ImproperlyConfigured
from django.test import TestCase
from analytical.services.kiss_insights import KissInsightsService
from analytical.tests.utils import TestSettingsManager
class KissInsightsTestCase(TestCase):
"""
Tests for the KISSinsights service.
"""
def setUp(self):
self.settings_manager = TestSettingsManager()
self.settings_manager.set(KISS_INSIGHTS_ACCOUNT_NUMBER='12345')
self.settings_manager.set(KISS_INSIGHTS_SITE_CODE='abc')
self.service = KissInsightsService()
def tearDown(self):
self.settings_manager.revert()
def test_empty_locations(self):
self.assertEqual(self.service.render_head_top({}), "")
self.assertEqual(self.service.render_head_bottom({}), "")
self.assertEqual(self.service.render_body_bottom({}), "")
def test_no_account_number(self):
self.settings_manager.delete('KISS_INSIGHTS_ACCOUNT_NUMBER')
self.assertRaises(ImproperlyConfigured, KissInsightsService)
def test_no_site_code(self):
self.settings_manager.delete('KISS_INSIGHTS_SITE_CODE')
self.assertRaises(ImproperlyConfigured, KissInsightsService)
def test_wrong_account_number(self):
self.settings_manager.set(KISS_INSIGHTS_ACCOUNT_NUMBER='1234')
self.assertRaises(ImproperlyConfigured, KissInsightsService)
self.settings_manager.set(KISS_INSIGHTS_ACCOUNT_NUMBER='123456')
self.assertRaises(ImproperlyConfigured, KissInsightsService)
def test_wrong_site_id(self):
self.settings_manager.set(KISS_INSIGHTS_SITE_CODE='ab')
self.assertRaises(ImproperlyConfigured, KissInsightsService)
self.settings_manager.set(KISS_INSIGHTS_SITE_CODE='abcd')
self.assertRaises(ImproperlyConfigured, KissInsightsService)
def test_rendering(self):
r = self.service.render_body_top({})
self.assertTrue("//s3.amazonaws.com/ki.js/12345/abc.js" in r, r)
def test_identify(self):
self.settings_manager.set(ANALYTICAL_AUTO_IDENTIFY=True)
r = self.service.render_body_top({'user': User(username='test')})
self.assertTrue("_kiq.push(['identify', 'test']);" in r, r)
def test_show_survey(self):
r = self.service.render_body_top({'kiss_insights_show_survey': 1234})
self.assertTrue("_kiq.push(['showSurvey', 1234]);" in r, r)

View file

@ -0,0 +1,58 @@
"""
Tests for the KISSmetrics service.
"""
from django.contrib.auth.models import User
from django.core.exceptions import ImproperlyConfigured
from django.test import TestCase
from analytical.services.kiss_metrics import KissMetricsService
from analytical.tests.utils import TestSettingsManager
class KissMetricsTestCase(TestCase):
"""
Tests for the KISSmetrics service.
"""
def setUp(self):
self.settings_manager = TestSettingsManager()
self.settings_manager.set(KISS_METRICS_API_KEY='0123456789abcdef012345'
'6789abcdef01234567')
self.service = KissMetricsService()
def tearDown(self):
self.settings_manager.revert()
def test_empty_locations(self):
self.assertEqual(self.service.render_head_bottom({}), "")
self.assertEqual(self.service.render_body_top({}), "")
self.assertEqual(self.service.render_body_bottom({}), "")
def test_no_api_key(self):
self.settings_manager.delete('KISS_METRICS_API_KEY')
self.assertRaises(ImproperlyConfigured, KissMetricsService)
def test_wrong_api_key(self):
self.settings_manager.set(KISS_METRICS_API_KEY='0123456789abcdef012345'
'6789abcdef0123456')
self.assertRaises(ImproperlyConfigured, KissMetricsService)
self.settings_manager.set(KISS_METRICS_API_KEY='0123456789abcdef012345'
'6789abcdef012345678')
self.assertRaises(ImproperlyConfigured, KissMetricsService)
def test_rendering(self):
r = self.service.render_head_top({})
self.assertTrue("//doug1izaerwt3.cloudfront.net/0123456789abcdef012345"
"6789abcdef01234567.1.js" in r, r)
def test_identify(self):
self.settings_manager.set(ANALYTICAL_AUTO_IDENTIFY=True)
r = self.service.render_head_top({'user': User(username='test')})
self.assertTrue("_kmq.push(['identify', 'test']);" in r, r)
def test_event(self):
r = self.service.render_event('test_event', {'prop1': 'val1',
'prop2': 'val2'})
self.assertEqual(r, "_kmq.push(['record', 'test_event', "
'{"prop1": "val1", "prop2": "val2"}]);')

View file

@ -0,0 +1,59 @@
"""
Tests for the Mixpanel service.
"""
from django.contrib.auth.models import User
from django.core.exceptions import ImproperlyConfigured
from django.test import TestCase
from analytical.services.mixpanel import MixpanelService
from analytical.tests.utils import TestSettingsManager
class MixpanelTestCase(TestCase):
"""
Tests for the Mixpanel service.
"""
def setUp(self):
self.settings_manager = TestSettingsManager()
self.settings_manager.set(
MIXPANEL_TOKEN='0123456789abcdef0123456789abcdef')
self.service = MixpanelService()
def tearDown(self):
self.settings_manager.revert()
def test_empty_locations(self):
self.assertEqual(self.service.render_head_top({}), "")
self.assertEqual(self.service.render_body_top({}), "")
self.assertEqual(self.service.render_body_bottom({}), "")
def test_no_token(self):
self.settings_manager.delete('MIXPANEL_TOKEN')
self.assertRaises(ImproperlyConfigured, MixpanelService)
def test_wrong_token(self):
self.settings_manager.set(
MIXPANEL_TOKEN='0123456789abcdef0123456789abcde')
self.assertRaises(ImproperlyConfigured, MixpanelService)
self.settings_manager.set(
MIXPANEL_TOKEN='0123456789abcdef0123456789abcdef0')
self.assertRaises(ImproperlyConfigured, MixpanelService)
def test_rendering(self):
r = self.service.render_head_bottom({})
self.assertTrue(
"mpq.push(['init', '0123456789abcdef0123456789abcdef']);" in r,
r)
def test_identify(self):
self.settings_manager.set(ANALYTICAL_AUTO_IDENTIFY=True)
r = self.service.render_head_bottom({'user': User(username='test')})
self.assertTrue("mpq.push(['identify', 'test']);" in r, r)
def test_event(self):
r = self.service.render_event('test_event', {'prop1': 'val1',
'prop2': 'val2'})
self.assertEqual(r, "mpq.push(['track', 'test_event', "
'{"prop1": "val1", "prop2": "val2"}]);')

View file

@ -0,0 +1,42 @@
"""
Tests for the Optimizely service.
"""
from django.core.exceptions import ImproperlyConfigured
from django.test import TestCase
from analytical.services.optimizely import OptimizelyService
from analytical.tests.utils import TestSettingsManager
class OptimizelyTestCase(TestCase):
"""
Tests for the Optimizely service.
"""
def setUp(self):
self.settings_manager = TestSettingsManager()
self.settings_manager.set(OPTIMIZELY_ACCOUNT_NUMBER='1234567')
self.service = OptimizelyService()
def tearDown(self):
self.settings_manager.revert()
def test_empty_locations(self):
self.assertEqual(self.service.render_head_bottom({}), "")
self.assertEqual(self.service.render_body_top({}), "")
self.assertEqual(self.service.render_body_bottom({}), "")
def test_no_account_number(self):
self.settings_manager.delete('OPTIMIZELY_ACCOUNT_NUMBER')
self.assertRaises(ImproperlyConfigured, OptimizelyService)
def test_wrong_account_number(self):
self.settings_manager.set(OPTIMIZELY_ACCOUNT_NUMBER='123456')
self.assertRaises(ImproperlyConfigured, OptimizelyService)
self.settings_manager.set(OPTIMIZELY_ACCOUNT_NUMBER='12345678')
self.assertRaises(ImproperlyConfigured, OptimizelyService)
def test_rendering(self):
self.assertEqual(self.service.render_head_top({}),
'<script src="//cdn.optimizely.com/js/1234567.js"></script>')

View file

@ -0,0 +1,14 @@
"""
django-analytical testing settings.
"""
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': ':memory:',
}
}
INSTALLED_APPS = [
'analytical',
]

View file

@ -0,0 +1,80 @@
"""
Tests for the services package.
"""
from django.core.exceptions import ImproperlyConfigured
from django.test import TestCase
from analytical import services
from analytical.services import load_services
from analytical.tests.utils import TestSettingsManager
from analytical.services.google_analytics import GoogleAnalyticsService
class GetEnabledServicesTestCase(TestCase):
"""
Tests for get_enabled_services.
"""
def setUp(self):
services.enabled_services = None
services.load_services = lambda: 'test'
def tearDown(self):
services.enabled_services = None
services.load_services = load_services
def test_get_enabled_services(self):
result = services.get_enabled_services()
self.assertEqual(result, 'test')
services.load_services = lambda: 'test2'
result = services.get_enabled_services()
self.assertEqual(result, 'test')
class LoadServicesTestCase(TestCase):
"""
Tests for load_services.
"""
def setUp(self):
self.settings_manager = TestSettingsManager()
self.settings_manager.delete('ANALYTICAL_SERVICES')
self.settings_manager.delete('CLICKY_SITE_ID')
self.settings_manager.delete('CHARTBEAT_USER_ID')
self.settings_manager.delete('CRAZY_EGG_ACCOUNT_NUMBER')
self.settings_manager.delete('GOOGLE_ANALYTICS_PROPERTY_ID')
self.settings_manager.delete('KISSINSIGHTS_ACCOUNT_NUMBER')
self.settings_manager.delete('KISSINSIGHTS_SITE_CODE')
self.settings_manager.delete('KISSMETRICS_API_KEY')
self.settings_manager.delete('MIXPANEL_TOKEN')
self.settings_manager.delete('OPTIMIZELY_ACCOUNT_NUMBER')
services.enabled_services = None
def tearDown(self):
self.settings_manager.revert()
services.enabled_services = None
def test_no_services(self):
self.assertEqual(load_services(), [])
def test_enabled_service(self):
self.settings_manager.set(GOOGLE_ANALYTICS_PROPERTY_ID='UA-1234567-8')
results = load_services()
self.assertEqual(len(results), 1, results)
self.assertTrue(isinstance(results[0], GoogleAnalyticsService),
results)
def test_explicit_service(self):
self.settings_manager.set(ANALYTICAL_SERVICES=[
'analytical.services.google_analytics.GoogleAnalyticsService'])
self.settings_manager.set(GOOGLE_ANALYTICS_PROPERTY_ID='UA-1234567-8')
results = load_services()
self.assertEqual(len(results), 1, results)
self.assertTrue(isinstance(results[0], GoogleAnalyticsService),
results)
def test_explicit_service_misconfigured(self):
self.settings_manager.set(ANALYTICAL_SERVICES=[
'analytical.services.google_analytics.GoogleAnalyticsService'])
self.assertRaises(ImproperlyConfigured, load_services)

View file

@ -0,0 +1,90 @@
"""
Tests for the template tags.
"""
from django.http import HttpRequest
from django import template
from django.test import TestCase
from analytical import services
from analytical.tests.utils import TestSettingsManager
class TemplateTagsTestCase(TestCase):
"""
Tests for the template tags.
"""
def setUp(self):
self.settings_manager = TestSettingsManager()
self.settings_manager.set(ANALYTICAL_SERVICES=[
'analytical.services.console.ConsoleService'])
services.enabled_services = None
def tearDown(self):
self.settings_manager.revert()
services.enabled_services = None
def render_location_tag(self, location, context=None):
if context is None: context = {}
t = template.Template(
"{%% load analytical %%}{%% analytical_setup_%s %%}"
% location)
return t.render(template.Context(context))
def test_location_tags(self):
for l in ['head_top', 'head_bottom', 'body_top', 'body_bottom']:
r = self.render_location_tag(l)
self.assertTrue('rendering analytical_%s tag' % l in r, r)
def test_render_internal_ip(self):
self.settings_manager.set(ANALYTICAL_INTERNAL_IPS=['1.1.1.1'])
req = HttpRequest()
req.META['REMOTE_ADDR'] = '1.1.1.1'
for l in ['head_top', 'head_bottom', 'body_top', 'body_bottom']:
r = self.render_location_tag(l, {'request': req})
self.assertTrue('<!-- Analytical disabled on internal IP address'
in r, r)
def test_render_internal_ip_fallback(self):
self.settings_manager.set(INTERNAL_IPS=['1.1.1.1'])
req = HttpRequest()
req.META['REMOTE_ADDR'] = '1.1.1.1'
for l in ['head_top', 'head_bottom', 'body_top', 'body_bottom']:
r = self.render_location_tag(l, {'request': req})
self.assertTrue('<!-- Analytical disabled on internal IP address'
in r, r)
def test_render_internal_ip_forwarded_for(self):
self.settings_manager.set(ANALYTICAL_INTERNAL_IPS=['1.1.1.1'])
req = HttpRequest()
req.META['HTTP_X_FORWARDED_FOR'] = '1.1.1.1'
for l in ['head_top', 'head_bottom', 'body_top', 'body_bottom']:
r = self.render_location_tag(l, {'request': req})
self.assertTrue('<!-- Analytical disabled on internal IP address'
in r, r)
def test_render_different_internal_ip(self):
self.settings_manager.set(ANALYTICAL_INTERNAL_IPS=['1.1.1.1'])
req = HttpRequest()
req.META['REMOTE_ADDR'] = '2.2.2.2'
for l in ['head_top', 'head_bottom', 'body_top', 'body_bottom']:
r = self.render_location_tag(l, {'request': req})
self.assertFalse('<!-- Analytical disabled on internal IP address'
in r, r)
def test_render_internal_ip_empty(self):
self.settings_manager.set(ANALYTICAL_INTERNAL_IPS=['1.1.1.1'])
self.settings_manager.delete('ANALYTICAL_SERVICES')
self.settings_manager.delete('CLICKY_SITE_ID')
self.settings_manager.delete('CRAZY_EGG_ACCOUNT_NUMBER')
self.settings_manager.delete('GOOGLE_ANALYTICS_PROPERTY_ID')
self.settings_manager.delete('KISSINSIGHTS_ACCOUNT_NUMBER')
self.settings_manager.delete('KISSINSIGHTS_SITE_CODE')
self.settings_manager.delete('KISSMETRICS_API_KEY')
self.settings_manager.delete('MIXPANEL_TOKEN')
self.settings_manager.delete('OPTIMIZELY_ACCOUNT_NUMBER')
req = HttpRequest()
req.META['REMOTE_ADDR'] = '1.1.1.1'
for l in ['head_top', 'head_bottom', 'body_top', 'body_bottom']:
self.assertEqual(self.render_location_tag(l, {'request': req}), "")

64
analytical/tests/utils.py Normal file
View file

@ -0,0 +1,64 @@
"""
Testing utilities.
"""
from django.conf import settings
from django.core.management import call_command
from django.db.models import loading
from django.test.simple import run_tests as django_run_tests
def run_tests():
"""
Use the Django test runner to run the tests.
"""
django_run_tests([], verbosity=1, interactive=True)
class TestSettingsManager(object):
"""
From: http://www.djangosnippets.org/snippets/1011/
A class which can modify some Django settings temporarily for a
test and then revert them to their original values later.
Automatically handles resyncing the DB if INSTALLED_APPS is
modified.
"""
NO_SETTING = ('!', None)
def __init__(self):
self._original_settings = {}
def set(self, **kwargs):
for k, v in kwargs.iteritems():
self._original_settings.setdefault(k, getattr(settings, k,
self.NO_SETTING))
setattr(settings, k, v)
if 'INSTALLED_APPS' in kwargs:
self.syncdb()
def delete(self, *args):
for k in args:
try:
self._original_settings.setdefault(k, getattr(settings, k,
self.NO_SETTING))
delattr(settings, k)
except AttributeError:
pass # setting did not exist
def syncdb(self):
loading.cache.loaded = False
call_command('syncdb', verbosity=0, interactive=False)
def revert(self):
for k,v in self._original_settings.iteritems():
if v == self.NO_SETTING:
if hasattr(settings, k):
delattr(settings, k)
else:
setattr(settings, k, v)
if 'INSTALLED_APPS' in self._original_settings:
self.syncdb()
self._original_settings = {}

View file

@ -1,167 +0,0 @@
"""
Utility function for django-analytical.
"""
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
HTML_COMMENT = '<!-- %(service)s disabled on internal IP address\n%(html)s\n-->'
def get_required_setting(setting, value_re, invalid_msg):
"""
Return a constant from ``django.conf.settings``. The `setting`
argument is the constant name, the `value_re` argument is a regular
expression used to validate the setting value and the `invalid_msg`
argument is used as exception message if the value is not valid.
"""
try:
value = getattr(settings, setting)
except AttributeError:
raise AnalyticalException('%s setting: not found' % setting)
if not value:
raise AnalyticalException('%s setting is not set' % setting)
value = str(value)
if not value_re.search(value):
raise AnalyticalException(
"%s setting: %s: '%s'" % (setting, invalid_msg, value)
)
return value
def get_user_from_context(context):
"""
Get the user instance from the template context, if possible.
If the context does not contain a `request` or `user` attribute,
`None` is returned.
"""
try:
return context['user']
except KeyError:
pass
try:
request = context['request']
return request.user
except (KeyError, AttributeError):
pass
return None
def get_user_is_authenticated(user):
"""Check if the user is authenticated.
This is a compatibility function needed to support both Django 1.x and 2.x;
Django 2.x turns the function into a proper boolean so function calls will
fail.
"""
if callable(user.is_authenticated):
return user.is_authenticated()
else:
return user.is_authenticated
def get_identity(context, prefix=None, identity_func=None, user=None):
"""
Get the identity of a logged in user from a template context.
The `prefix` argument is used to provide different identities to
different analytics services. The `identity_func` argument is a
function that returns the identity of the user; by default the
identity is the username.
"""
if prefix is not None:
try:
return context['%s_identity' % prefix]
except KeyError:
pass
try:
return context['analytical_identity']
except KeyError:
pass
if getattr(settings, 'ANALYTICAL_AUTO_IDENTIFY', True):
try:
if user is None:
user = get_user_from_context(context)
if get_user_is_authenticated(user):
if identity_func is not None:
return identity_func(user)
else:
return user.get_username()
except (KeyError, AttributeError):
pass
return None
def get_domain(context, prefix):
"""
Return the domain used for the tracking code. Each service may be
configured with its own domain (called `<name>_domain`), or a
django-analytical-wide domain may be set (using `analytical_domain`.
If no explicit domain is found in either the context or the
settings, try to get the domain from the contrib sites framework.
"""
domain = context.get('%s_domain' % prefix)
if domain is None:
domain = context.get('analytical_domain')
if domain is None:
domain = getattr(settings, '%s_DOMAIN' % prefix.upper(), None)
if domain is None:
domain = getattr(settings, 'ANALYTICAL_DOMAIN', None)
if domain is None:
if 'django.contrib.sites' in settings.INSTALLED_APPS:
from django.contrib.sites.models import Site
try:
domain = Site.objects.get_current().domain
except (ImproperlyConfigured, Site.DoesNotExist):
pass
return domain
def is_internal_ip(context, prefix=None):
"""
Return whether the visitor is coming from an internal IP address,
based on information from the template context.
The prefix is used to allow different analytics services to have
different notions of internal addresses.
"""
try:
request = context['request']
remote_ip = request.META.get('HTTP_X_FORWARDED_FOR', '')
if not remote_ip:
remote_ip = request.META.get('REMOTE_ADDR', '')
if not remote_ip:
return False
internal_ips = None
if prefix is not None:
internal_ips = getattr(settings, '%s_INTERNAL_IPS' % prefix, None)
if internal_ips is None:
internal_ips = getattr(settings, 'ANALYTICAL_INTERNAL_IPS', None)
if internal_ips is None:
internal_ips = getattr(settings, 'INTERNAL_IPS', None)
return remote_ip in (internal_ips or [])
except (KeyError, AttributeError):
return False
def disable_html(html, service):
"""
Disable HTML code by commenting it out.
The `service` argument is used to display a friendly message.
"""
return HTML_COMMENT % {'html': html, 'service': service}
class AnalyticalException(Exception):
"""
Raised when an exception occurs in any django-analytical code that should
be silenced in templates.
"""
silent_variable_failure = True

View file

@ -1,21 +0,0 @@
def setup(app):
app.add_crossref_type(
directivename='setting',
rolename='setting',
indextemplate='pair: %s; setting',
)
app.add_crossref_type(
directivename='templatetag',
rolename='ttag',
indextemplate='pair: %s; template tag',
)
app.add_crossref_type(
directivename='templatefilter',
rolename='tfilter',
indextemplate='pair: %s; template filter',
)
app.add_crossref_type(
directivename='fieldlookup',
rolename='lookup',
indextemplate='pair: %s; field lookup type',
)

View file

@ -1,52 +1,48 @@
# -*- coding: utf-8 -*-
#
# This file is execfile()d with the current directory set to its containing
# directory.
import os
import sys
sys.path.append(os.path.join(os.path.abspath('.'), '_ext'))
import sys, os
sys.path.append(os.path.join(os.path.abspath('.'), '.ext'))
sys.path.append(os.path.dirname(os.path.abspath('.')))
import analytical # noqa
import analytical
# -- General configuration --------------------------------------------------
project = 'django-analytical'
copyright = '2011, Joost Cassee <joost@cassee.net>'
# -- General configuration -----------------------------------------------------
project = u'django-analytical'
copyright = u'2011, Joost Cassee <joost@cassee.net>'
release = analytical.__version__
# The short X.Y version.
version = release.rsplit('.', 1)[0]
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'local']
templates_path = ['_templates']
source_suffix = {'.rst': 'restructuredtext'}
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx']
templates_path = ['.templates']
source_suffix = '.rst'
master_doc = 'index'
add_function_parentheses = True
pygments_style = 'sphinx'
intersphinx_mapping = {
'python': ('https://docs.python.org/3.13', None),
'django': ('https://docs.djangoproject.com/en/stable', None),
'http://docs.python.org/2.6': None,
'http://docs.djangoproject.com/en/1.2': 'http://docs.djangoproject.com/en/1.2/_objects/',
}
# -- Options for HTML output ------------------------------------------------
# -- Options for HTML output ---------------------------------------------------
html_theme = 'default'
html_static_path = ['.static']
htmlhelp_basename = 'analyticaldoc'
# -- Options for LaTeX output -----------------------------------------------
# -- Options for LaTeX output --------------------------------------------------
latex_documents = [
(
'index',
'django-analytical.tex',
'Documentation for django-analytical',
'Joost Cassee',
'manual',
),
('index', 'django-analytical.tex', u'Documentation for django-analytical',
u'Joost Cassee', 'manual'),
]

View file

@ -1,119 +0,0 @@
==========================
Features and customization
==========================
The django-analytical application sets up basic tracking without any
further configuration. This page describes extra features and ways in
which behavior can be customized.
.. _internal-ips:
Internal IP addresses
=====================
Visits by the website developers or internal users are usually not
interesting. The django-analytical will comment out the service
initialization code if the client IP address is detected as one from the
:data:`ANALYTICAL_INTERNAL_IPS` setting. The default value for this
setting is :data:`INTERNAL_IPS`.
Example:
.. code-block:: python
ANALYTICAL_INTERNAL_IPS = ['192.168.1.45', '192.168.1.57']
.. note::
The template tags can only access the visitor IP address if the
HTTP request is present in the template context as the
``request`` variable. For this reason, the
:data:`ANALYTICAL_INTERNAL_IPS` setting only works if you add this
variable to the context yourself when you render the template, or
you use the ``RequestContext`` and add
``'django.core.context_processors.request'`` to the list of
context processors in the ``TEMPLATE_CONTEXT_PROCESSORS``
setting.
.. _identifying-visitors:
Identifying authenticated users
===============================
Some analytics services can track individual users. If the visitor is
logged in through the standard Django authentication system and the
current user is accessible in the template context, the username can be
passed to the analytics services that support identifying users. This
feature is configured by the :data:`ANALYTICAL_AUTO_IDENTIFY` setting
and is enabled by default. To disable:
.. code-block:: python
ANALYTICAL_AUTO_IDENTIFY = False
.. note::
The template tags can only access the visitor username if the
Django user is present in the template context either as the
``user`` variable, or as an attribute on the HTTP request in the
``request`` variable. Use a
:class:`~django.template.RequestContext` to render your
templates and add
``'django.contrib.auth.context_processors.auth'`` or
``'django.core.context_processors.request'`` to the list of
context processors in the :data:`TEMPLATE_CONTEXT_PROCESSORS`
setting. (The first of these is added by default.)
Alternatively, add one of the variables to the context yourself
when you render the template.
Changing the identity
*********************
If you want to override the identity of the logged-in user that the various
providers send you can do it by setting the ``analytical_identity`` context
variable in your view code:
.. code-block:: python
context = RequestContext({'analytical_identity': user.uuid})
return some_template.render(context)
or in the template:
.. code-block:: django
{% with analytical_identity=request.user.uuid|default:None %}
{% analytical_head_top %}
{% endwith %}
or by implementing a context processor, e.g.
.. code-block:: python
# FILE: myproject/context_processors.py
from django.conf import settings
def get_identity(request):
return {
'analytical_identity': 'some-value-here',
}
# FILE: myproject/settings.py
TEMPLATES = [
{
'OPTIONS': {
'context_processors': [
'myproject.context_processors.get_identity',
],
},
},
]
That allows you as a developer to leave your view code untouched and
make sure that the variable is injected for all templates.
If you want to change the identity only for specific provider use the
``*_identity`` context variable, where the ``*`` prefix is the module name
of the specific provider.

View file

@ -1,50 +1,23 @@
===================
History and credits
===================
Changelog
=========
The project follows the `Semantic Versioning`_ specification for its
version numbers. Patch-level increments indicate bug fixes, minor
version increments indicate new functionality and major version
increments indicate backwards incompatible changes.
Version 1.0.0 is the last to support Django < 1.7. Users of older Django
versions should stick to 1.0.0, and are encouraged to upgrade their setups.
Starting with 2.0.0, dropping support for obsolete Django versions is not
considered to be a backward-incompatible change.
.. _`Semantic Versioning`: http://semver.org/
.. include:: ../CHANGELOG.rst
---------
0.1.0
First project release.
Credits
=======
-------
The django-analytical package was originally written by `Joost Cassee`_
and is now maintained by `Peter Bittner`_ and the `Jazzband community`_.
All known contributors are listed as ``authors`` in the `project metadata`_.
django-analytical was written by `Joost Cassee`_. The project source
code is hosted generously hosted by GitHub_.
Included JavaScript code snippets for integration of the analytics services
were written by the respective service providers.
The application was inspired by and uses ideas from Analytical_, Joshua
Krall's all-purpose analytics front-end for Rails.
.. _`Joost Cassee`: https://github.com/jcassee
.. _`Peter Bittner`: https://github.com/bittner
.. _`Jazzband community`: https://jazzband.co/
.. _`project metadata`: https://github.com/jazzband/django-analytical/blob/main/pyproject.toml#L15-L60
.. _`Analytical`: https://github.com/jkrall/analytical
.. _helping-out:
Helping out
===========
.. include:: ../README.rst
:start-after: .. start contribute include
:end-before: .. end contribute include
This application was inspired by and uses ideas from Analytical_,
Joshua Krall's all-purpose analytics front-end for Rails. The work on
Crazy Egg was made possible by `Bateau Knowledge`_.
.. _`Joost Cassee`: mailto:joost@cassee.net
.. _GitHub: http://github.com/
.. _Analytical: https://github.com/jkrall/analytical
.. _`Bateau Knowledge`: http://www.bateauknowledge.nl/

View file

@ -1,25 +1,35 @@
=================
django-analytical
=================
========================================
Analytics service integration for Django
========================================
The django-analytical application integrates analytics services into a
Django_ project.
The django-analytical application integrates various analytics services
into a Django_ project.
.. _Django: https://www.djangoproject.com/
.. _Django: http://www.djangoproject.com/
:Package: https://pypi.org/project/django-analytical/
:Source: https://github.com/jazzband/django-analytical
:Download: http://pypi.python.org/pypi/django-analytical/
:Source: http://github.com/jcassee/django-analytical
Overview
========
.. include:: ../README.rst
:start-after: .. start docs include
:end-before: .. end docs include
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.
To get a feel of how django-analytical works, check out the
:doc:`tutorial`.
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.
Contents
@ -28,10 +38,7 @@ Contents
.. toctree::
:maxdepth: 2
tutorial
install
features
services
services/index
settings
history
license

View file

@ -4,208 +4,141 @@ 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
configuring the services you use in the project settings.
applications, adding the template tags to your base template, and adding
the identifiers for the services you use to the project settings.
#. `Installing the Python package`_
#. `Installing the Django application`_
#. `Adding the template tags to the base template`_
#. `Enabling the services`_
#. `Configuring the application`_
.. _installing-the-package:
Installing the Python package
=============================
To install django-analytical the ``analytical`` package must be added to
the Python path. You can install it directly from PyPI using
``easy_install``:
``easy_install``::
.. code-block:: bash
$ easy_install django-analytical
$ easy_install django-analytical
You can also install directly from source. Download either the latest
stable version from PyPI_ or any release from GitHub_, or use Git to
get the development code:
get the development code::
.. code-block:: bash
$ git clone https://github.com/jcassee/django-analytical.git
$ git clone https://github.com/jazzband/django-analytical.git
.. _PyPI: http://pypi.python.org/pypi/django-analytical/
.. _GitHub: http://github.com/jcassee/django-analytical
.. _PyPI: https://pypi.org/project/django-analytical/
.. _GitHub: http://github.com/jazzband/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
.. code-block:: bash
$ cd django-analytical
$ python setup.py install
.. _installing-the-application:
Installing the Django application
=================================
After you installed django-analytical, add the ``analytical`` Django
After you install django-analytical, add the ``analytical`` Django
application to the list of installed applications in the ``settings.py``
file of your project:
file of your project::
.. code-block:: python
INSTALLED_APPS = [
...
'analytical',
...
]
INSTALLED_APPS = [
...
'analytical',
...
]
.. _adding-the-template-tags:
Adding the template tags to the base template
=============================================
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:
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::
.. code-block:: django
{% load analytical %}
<!DOCTYPE ... >
<html>
<head>
{% analytical_setup_head_top %}
{% load analytical %}
<!DOCTYPE ... >
<html>
<head>
{% analytical_head_top %}
...
...
{% analytical_setup_head_bottom %}
</head>
<body>
{% analytical_setup_body_top %}
{% analytical_head_bottom %}
</head>
<body>
{% analytical_body_top %}
...
...
{% analytical_body_bottom %}
</body>
</html>
Instead of using the generic tags, you can also just use tags specific
for the analytics service(s) you are using. See :ref:`services` for
documentation on using individual services.
{% analytical_setup_body_bottom %}
</body>
</html>
.. _enabling-services:
Enabling the services
=====================
Configuring the application
===========================
Without configuration, the template tags all render the empty string.
Services are configured in the project ``settings.py`` file. The
settings required to enable each service are listed here:
You must enable at least one service, and optionally configure other
django-analytical features.
Enabling services
-----------------
By default, only configured analytics services are installed by the
template tags. You can also use the :data:`ANALYTICAL_SERVICES` setting
to specify the used services explicitly. Services are configured in the
project ``settings.py`` file. The settings required to enable each
service are listed here. See the service documentation for details.
* :doc:`Chartbeat <services/chartbeat>`::
CHARTBEAT_USER_ID = '12345'
* :doc:`Clickmap <services/clickmap>`::
CLICKMAP_TRACKER_CODE = '12345678....912'
CHARTBEAT_USER_ID = '12345'
* :doc:`Clicky <services/clicky>`::
CLICKY_SITE_ID = '12345678'
CLICKY_SITE_ID = '12345678'
* :doc:`Crazy Egg <services/crazy_egg>`::
CRAZY_EGG_ACCOUNT_NUMBER = '12345678'
CRAZY_EGG_ACCOUNT_NUMBER = '12345678'
* :doc:`Facebook Pixel <services/facebook_pixel>`::
* :doc:`Google Analytics <services/google_analytics>`::
FACEBOOK_PIXEL_ID = '1234567890'
* :doc:`Gaug.es <services/gauges>`::
GAUGES_SITE_ID = '0123456789abcdef0123456789abcdef'
* :doc:`Google Analytics (legacy) <services/google_analytics>`::
GOOGLE_ANALYTICS_PROPERTY_ID = 'UA-1234567-8'
* :doc:`Google Analytics (gtag.js) <services/google_analytics_gtag>`::
GOOGLE_ANALYTICS_GTAG_PROPERTY_ID = 'UA-1234567-8'
* :doc:`Google Analytics (analytics.js) <services/google_analytics_js>`::
GOOGLE_ANALYTICS_JS_PROPERTY_ID = 'UA-12345678-9'
* :doc:`HubSpot <services/hubspot>`::
HUBSPOT_PORTAL_ID = '1234'
HUBSPOT_DOMAIN = 'somedomain.web101.hubspot.com'
* :doc:`Intercom <services/intercom>`::
INTERCOM_APP_ID = '0123456789abcdef0123456789abcdef01234567'
GOOGLE_ANALYTICS_PROPERTY_ID = 'UA-1234567-8'
* :doc:`KISSinsights <services/kiss_insights>`::
KISS_INSIGHTS_ACCOUNT_NUMBER = '12345'
KISS_INSIGHTS_SITE_CODE = 'abc'
KISS_INSIGHTS_ACCOUNT_NUMBER = '12345'
KISS_INSIGHTS_SITE_CODE = 'abc'
* :doc:`KISSmetrics <services/kiss_metrics>`::
KISS_METRICS_API_KEY = '0123456789abcdef0123456789abcdef01234567'
* :doc:`Lucky Orange <services/luckyorange>`::
LUCKYORANGE_SITE_ID = '123456'
* :doc:`Matomo (formerly Piwik) <services/matomo>`::
MATOMO_DOMAIN_PATH = 'your.matomo.server/optional/path'
MATOMO_SITE_ID = '123'
KISS_METRICS_API_KEY = '0123456789abcdef0123456789abcdef01234567'
* :doc:`Mixpanel <services/mixpanel>`::
MIXPANEL_API_TOKEN = '0123456789abcdef0123456789abcdef'
* :doc:`Olark <services/olark>`::
OLARK_SITE_ID = '1234-567-89-0123'
MIXPANEL_TOKEN = '0123456789abcdef0123456789abcdef'
* :doc:`Optimizely <services/optimizely>`::
OPTIMIZELY_ACCOUNT_NUMBER = '1234567'
OPTIMIZELY_ACCOUNT_NUMBER = '1234567'
* :doc:`Performable <services/performable>`::
PERFORMABLE_API_KEY = '123abc'
Configuring behavior
--------------------
* :doc:`Rating\@Mail.ru <services/rating_mailru>`::
By default, django-analytical will comment out the service
initialization code if the client IP address is detected as one from the
:data:`ANALYTICAL_INTERNAL_IPS` setting, which is set to
:data:`INTERNAL_IPS` by default.
RATING_MAILRU_COUNTER_ID = '1234567'
* :doc:`SnapEngage <services/snapengage>`::
SNAPENGAGE_WIDGET_ID = 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX'
* :doc:`Woopra <services/woopra>`::
WOOPRA_DOMAIN = 'abcde.com'
* :doc:`Yandex.Metrica <services/yandex_metrica>`::
YANDEX_METRICA_COUNTER_ID = '12345678'
----
The django-analytical application is now set-up to track visitors. For
information about identifying users, further configuration and
customization, see :doc:`features`.
Also, if the visitor is a logged in user and the user is accessible in
the template context, the username is passed to the analytics services
that support identifying users. See :data:`ANALYTICAL_AUTO_IDENTIFY`.

View file

@ -1,16 +0,0 @@
=======
License
=======
The django-analytical package is distributed under the `MIT License`_.
The complete license term are included below. The copyright of the
integration code snippets of individual services rest solely with the
respective service providers.
.. _`MIT License`: http://en.wikipedia.org/wiki/MIT_License
License terms
=============
.. include:: ../LICENSE.txt

View file

@ -1,25 +0,0 @@
.. _services:
========
Services
========
This section describes what features are supported by the different
analytics services. To start using a service, you can either use the
generic installation instructions (see :doc:`install`), or add
service-specific tags to your templates.
If you would like to have another analytics service supported by
django-analytical, please create an issue on the project
`issue tracker`_. See also :ref:`helping-out`.
.. _`issue tracker`: http://github.com/jazzband/django-analytical/issues
Currently supported services:
.. toctree::
:maxdepth: 1
:glob:
services/*

View file

@ -1,4 +1,3 @@
=============================
Chartbeat -- traffic analysis
=============================
@ -9,123 +8,27 @@ slows to a crawl.
.. _Chartbeat: http://www.chartbeat.com/
.. chartbeat-installation:
Installation
============
To start using the Chartbeat 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 Chartbeat template tags to your templates. This
step is only needed if you are not using the generic
:ttag:`analytical.*` tags. If you are, skip to
:ref:`chartbeat-configuration`.
The Chartbeat tracking code is inserted into templates using template
tags. At the top of the template, load the :mod:`chartbeat` template
tag library. Then insert the :ttag:`chartbeat_top` tag at the top of
the head section, and the :ttag:`chartbeat_bottom` tag at the bottom of
the body section::
{% load chartbeat %}
<html>
<head>
{% chartbeat_top %}
...
{% chartbeat_bottom %}
</body>
</html>
Because these tags are used to measure page loading time, it is
important to place them as close as possible to the start and end of the
document.
The Chartbeat service adds code both to the top of the head section and
the bottom of the body section. If the project uses the sites
framework, the domain name of the current website will be passed to
Chartbeat. Otherwise, Chartbeat will detect the domain name from the
URL.
.. _chartbeat-configuration:
Required settings
-----------------
Configuration
=============
.. data:: CHARTBEAT_USER_ID
Before you can use the Chartbeat integration, you must first set your
User ID.
The User ID::
CHARTBEAT_USER_ID = '12345'
.. _chartbeat-user-id:
You can find the User ID by visiting the Chartbeat `Add New Site`_
page. The second code snippet contains a line that looks like this::
Setting the User ID
-------------------
var _sf_async_config={uid:XXXXX,domain:"YYYYYYYYYY"};
Your Chartbeat account has a unique User ID. You can find your User ID
by visiting the Chartbeat `Add New Site`_ page. The second code snippet
contains a line that looks like this::
var _sf_async_config={uid:XXXXX,domain:"YYYYYYYYYY"};
Here, ``XXXXX`` is your User ID. Set :const:`CHARTBEAT_USER_ID` in the
project :file:`settings.py` file::
CHARTBEAT_USER_ID = 'XXXXX'
If you do not set a User ID, the tracking code will not be rendered.
Here, ``XXXXX`` is your User ID.
.. _`Add New Site`: http://chartbeat.com/code/
.. _chartbeat-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:`CHARTBEAT_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.
.. _chartbeat-domain:
Setting the domain
------------------
The JavaScript tracking code can send the website domain to Chartbeat.
If you use multiple subdomains this enables you to treat them as one
website in Chartbeat. If your project uses the sites framework, the
domain name of the current :class:`~django.contrib.sites.models.Site`
will be passed to Chartbeat automatically. You can modify this behavior
using the :const:`CHARTBEAT_AUTO_DOMAIN` setting::
CHARTBEAT_AUTO_DOMAIN = False
Alternatively, you set the domain through the ``chartbeat_domain``
context variable when you render the template::
context = RequestContext({'chartbeat_domain': 'example.com'})
return some_template.render(context)
It is annoying to do this for every view, so you may want to set it in
a context processor that you add to the
:data:`TEMPLATE_CONTEXT_PROCESSORS` list in :file:`settings.py`::
def chartbeat(request):
return {'chartbeat_domain': 'example.com'}
The context domain overrides the domain from the current site. If no
domain is set, either explicitly or implicitly through the sites
framework, then no domain is sent, and Chartbeat will detect the domain
name from the URL. If your website uses just one domain, this will work
just fine.
----
Thanks go to Chartbeat for their support with the development of this
application.

View file

@ -1,78 +0,0 @@
==================================
Clickmap -- visual click tracking
==================================
`Clickmap`_ is a real-time heatmap tool to track mouse clicks and scroll paths of your website visitors.
Gain intelligence about what's hot and what's not, and optimize your conversion with Clickmap.
.. _`Clickmap`: http://www.clickmap.ch/
.. clickmap-installation:
Installation
============
To start using the Clickmap 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 Clickmap 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:`clickmap-configuration`.
The Clickmap JavaScript code is inserted into templates using a template
tag. Load the :mod:`clickmap` template tag library and insert the
:ttag:`clickmap` 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::
{% load clickmap %}
...
{% clickmap %}
</body>
</html>
.. _clickmap-configuration:
Configuration
=============
Before you can use the Clickmap integration, you must first set your
Clickmap Tracker ID. If you don't have a Clickmap account yet,
`sign up`_ to get your Tracker ID.
.. _`sign up`: http://www.clickmap.ch/
.. _clickmap-tracker-id:
Setting the Tracker ID
----------------------
Clickmap gives you a unique Tracker ID, and the :ttag:`clickmap`
tag will include it in the rendered JavaScript code. You can find your
Tracker ID clicking the link named "Tracker" in the dashboard
of your Clickmap account. Set :const:`CLICKMAP_TRACKER_ID` in the project
:file:`settings.py` file::
CLICKMAP_TRACKER_ID = 'XXXXXXXX'
If you do not set an Tracker ID, the tracking code will not be
rendered.
.. _clickmap-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:`ANALYTICAL_INTERNAL_IPS` setting
(which is :const:`INTERNAL_IPS` by default,) the tracking code is
commented out. See :ref:`identifying-visitors` for important information
about detecting the visitor IP address.

View file

@ -1,4 +1,3 @@
==========================
Clicky -- traffic analysis
==========================
@ -9,147 +8,19 @@ designed to be very easy to use.
.. _Clicky: http://getclicky.com/
.. clicky-installation:
Installation
============
To start using the Clicky 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 Clicky 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:`clicky-configuration`.
The Clicky tracking code is inserted into templates using a template
tag. Load the :mod:`clicky` template tag library and insert the
:ttag:`clicky` 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::
{% load clicky %}
...
{% clicky %}
</body>
</html>
The setup code is added to the bottom of the HTML body. By default, the
username of a logged-in user is passed to Clicky. See
:data:`ANALYTICAL_AUTO_IDENTIFY`.
.. _clicky-configuration:
Required settings
-----------------
Configuration
=============
.. data:: CLICKY_SITE_ID
Before you can use the Clicky integration, you must first set your
website Site ID. You can also customize the data that Clicky tracks.
The Clicky site identifier, or Site ID::
CLICKY_SITE_ID = '12345678'
.. _clicky-site-id:
Setting the Site ID
-------------------
Every website you track with Clicky gets its own Site ID, and the
:ttag:`clicky` tag will include it in the rendered JavaScript code.
You can find the Site ID in the *Info* tab of the website *Preferences*
page, in your Clicky account. Set :const:`CLICKY_SITE_ID` in the
project :file:`settings.py` file::
CLICKY_SITE_ID = 'XXXXXXXX'
If you do not set a Site ID, the tracking code will not be rendered.
.. _clicky-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:`CLICKY_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.
.. _clicky-custom-data:
Custom data
-----------
As described in the Clicky `customized tracking`_ documentation page,
the data that is tracked by Clicky can be customized by setting the
:data:`clicky_custom` JavaScript variable before loading the tracking
code. Using template context variables, you can let the :ttag:`clicky`
tag pass custom data to Clicky automatically. You can set the context
variables in your view when you render a template containing the
tracking code::
context = RequestContext({'clicky_title': 'A better page title'})
return some_template.render(context)
It is annoying to do this for every view, so you may want to set custom
properties in a context processor that you add to the
:data:`TEMPLATE_CONTEXT_PROCESSORS` list in :file:`settings.py`::
def clicky_global_properties(request):
return {'clicky_timeout': 10}
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.
Here is a table with the most important variables. All variables listed
on the `customized tracking`_ documentation page can be set by replacing
``clicky_custom.`` with ``clicky_``.
================== =============== ===================================
Context variable Clicky property Description
================== =============== ===================================
``clicky_session`` session_ Session data. A dictionary
containing ``username`` and/or
``group`` keys.
------------------ --------------- -----------------------------------
``clicky_goal`` goal_ A succeeded goal. A dictionary
containing ``id`` and optionally
``revenue`` keys.
------------------ --------------- -----------------------------------
``clicky_split`` split_ Split testing page version. A
dictionary containing ``name``,
``version`` and optionally ``goal``
keys.
------------------ --------------- -----------------------------------
``clicky_href`` href_ The URL as tracked by Clicky.
Default is the page URL.
------------------ --------------- -----------------------------------
``clicky_title`` title_ The page title as tracked by
Clicky. Default is the HTML title.
================== =============== ===================================
.. _`customized tracking`: http://getclicky.com/help/customization
.. _session: http://getclicky.com/help/customization#session
.. _goal: http://getclicky.com/help/customization#goal
.. _href: http://getclicky.com/help/customization#href
.. _title: http://getclicky.com/help/customization#title
.. _split: http://getclicky.com/help/customization#split
.. _clicky-identify-user:
Identifying authenticated users
-------------------------------
If you have not set the session_ property explicitly, the username of an
authenticated user is passed to Clicky automatically. See
:ref:`identifying-visitors`.
----
Thanks go to Clicky for their support with the development of this
application.
You can find the Site ID in the Info tab of the website Preferences
page on your Clicky account.

View file

@ -1,4 +1,3 @@
==================================
Crazy Egg -- visual click tracking
==================================
@ -9,112 +8,14 @@ web pages that are most important to your visitors.
.. _`Crazy Egg`: http://www.crazyegg.com/
.. crazy-egg-installation:
Required settings
-----------------
Installation
============
.. data:: CRAZY_EGG_ACCOUNT_NUMBER
To start using the Crazy Egg 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.
Your Crazy Egg account number::
Next you need to add the Crazy Egg 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:`crazy-egg-configuration`.
CRAZY_EGG_ACCOUNT_NUMBER = '12345678'
The Crazy Egg tracking code is inserted into templates using a template
tag. Load the :mod:`crazy_egg` template tag library and insert the
:ttag:`crazy_egg` 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::
{% load crazy_egg %}
...
{% crazy_egg %}
</body>
</html>
.. _crazy-egg-configuration:
Configuration
=============
Before you can use the Crazy Egg integration, you must first set your
account number. You can also segment the click analysis through user
variables.
.. _crazy-egg-account-number:
Setting the account number
--------------------------
Crazy Egg gives you a unique account number, and the :ttag:`crazy egg`
tag will include it in the rendered JavaScript code. You can find your
account number by clicking the link named "What's my code?" in the
dashboard of your Crazy Egg account. Set
:const:`CRAZY_EGG_ACCOUNT_NUMBER` in the project :file:`settings.py`
file::
CRAZY_EGG_ACCOUNT_NUMBER = 'XXXXXXXX'
If you do not set an account number, the tracking code will not be
rendered.
.. _crazy-egg-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:`CRAZY_EGG_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.
.. _crazy-egg-uservars:
User variables
--------------
Crazy Egg can segment clicks based on `user variables`_. If you want to
set a user variable, use the context variables ``crazy_egg_var1``
through ``crazy_egg_var5`` when you render your template::
context = RequestContext({'crazy_egg_var1': 'red',
'crazy_egg_var2': 'male'})
return some_template.render(context)
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`::
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
context processor, the latter clobbers the former.
.. _`user variables`: https://www.crazyegg.com/help/Setting_Up_A_Page_to_Track/How_do_I_set_the_values_of_User_Var_1_User_Var_2_etc_in_the_confetti_and_overlay_views/
----
The work on Crazy Egg was made possible by `Bateau Knowledge`_. Thanks
go to Crazy Egg for their support with the development of this
application.
.. _`Bateau Knowledge`: http://www.bateauknowledge.nl/
You can find the account number by clicking the link named "What's my
code?" in the dashboard of your Crazy Egg account.

View file

@ -1,84 +0,0 @@
=======================================
Facebook Pixel -- advertising analytics
=======================================
`Facebook Pixel`_ is Facebook's tool for conversion tracking, optimisation and remarketing.
.. _`Facebook Pixel`: https://developers.facebook.com/docs/facebook-pixel/
.. facebook-pixel-installation:
Installation
============
To start using the Facebook Pixel 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 Facebook Pixel 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:`facebook-pixel-configuration`.
The Facebook Pixel code is inserted into templates using template tags.
Because every page that you want to track must have the tag,
it is useful to add it to your base template.
At the top of the template, load the :mod:`facebook_pixel` template tag library.
Then insert the :ttag:`facebook_pixel_head` tag at the bottom of the head section,
and optionally insert the :ttag:`facebook_pixel_body` tag at the bottom of the body section::
{% load facebook_pixel %}
<html>
<head>
...
{% facebook_pixel_head %}
</head>
<body>
...
{% facebook_pixel_body %}
</body>
</html>
.. note::
The :ttag:`facebook_pixel_body` tag code will only be used for browsers with JavaScript disabled.
It can be omitted if you don't need to support them.
.. _facebook-pixel-configuration:
Configuration
=============
Before you can use the Facebook Pixel integration,
you must first set your Pixel ID.
.. _facebook-pixel-id:
Setting the Pixel ID
--------------------
Each Facebook Adverts account you have can have a Pixel ID,
and the :mod:`facebook_pixel` tags will include it in the rendered page.
You can find the Pixel ID on the "Pixels" section of your Facebook Adverts account.
Set :const:`FACEBOOK_PIXEL_ID` in the project :file:`settings.py` file::
FACEBOOK_PIXEL_ID = 'XXXXXXXXXX'
If you do not set a Pixel ID, the code will not be rendered.
.. _facebook-pixel-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:`FACEBOOK_PIXEL_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.

View file

@ -1,93 +0,0 @@
=============================
Gaug.es -- Real-time tracking
=============================
Gaug.es_ is an easy way to implement real-time tracking for multiple
websites.
.. _Gaug.es: http://www.gaug.es/
.. gauges-installation:
Installation
============
To start using the Gaug.es 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 Gaug.es 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:`gauges-configuration`.
The Gaug.es JavaScript code is inserted into templates using a
template tag. Load the :mod:`gauges` template tag library and
insert the :ttag:`gauges` 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 gauges %}
<html>
<head>
{% gauges %}
...
.. _gauges-configuration:
Configuration
=============
Before you can use the Gaug.es integration, you must first set your
site id.
.. _gauges-site-id:
Setting the site id
--------------------------
Gaug.es gives you a unique site id, and the :ttag:`gauges`
tag will include it in the rendered JavaScript code. You can find your
site id by clicking the *Tracking Code* link when logged into
the on the gaug.es website. A page will display containing
HTML code looking like this::
<script>
var _gauges = _gauges || [];
(function() {
var t = document.createElement('script');
t.type = 'text/javascript';
t.async = true;
t.id = 'gauges-tracker';
t.setAttribute('data-site-id', 'XXXXXXXXXXXXXXXXXXXXXXX');
t.src = '//secure.gaug.es/track.js';
var s = document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(t, s);
})();
</script>
The code ``XXXXXXXXXXXXXXXXXXXXXXX`` is your site id. Set
:const:`GAUGES_SITE_ID` in the project :file:`settings.py`
file::
GAUGES_SITE_ID = 'XXXXXXXXXXXXXXXXXXXXXXX'
If you do not set an site id, the JavaScript code will not be
rendered.
.. _gauges-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:`ANALYTICAL_INTERNAL_IPS` setting
(which is :const:`INTERNAL_IPS` by default,) the tracking code is
commented out. See :ref:`identifying-visitors` for important information
about detecting the visitor IP address.

View file

@ -1,6 +1,5 @@
==============================================
Google Analytics (legacy) -- traffic analysis
==============================================
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
@ -10,260 +9,13 @@ features.
.. _`Google Analytics`: http://www.google.com/analytics/
.. google-analytics-installation:
Required settings
-----------------
Installation
============
.. data:: GOOGLE_ANALYTICS_PROPERTY_ID
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.
The Google Analytics web property ID::
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`.
GOOGLE_ANALYTICS_PROPERTY_ID = 'UA-123456-1'
The Google Analytics tracking code is inserted into templates using a
template tag. Load the :mod:`google_analytics` template tag library and
insert the :ttag:`google_analytics` 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 %}
<html>
<head>
...
{% google_analytics %}
</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` 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_PROPERTY_ID` in the
project :file:`settings.py` file::
GOOGLE_ANALYTICS_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.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.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
Tracking site speed
-------------------
You can view page load times in the `Site Speed report`_ by setting the
:const:`GOOGLE_ANALYTICS_SITE_SPEED` configuration setting::
GOOGLE_ANALYTICS_SITE_SPEED = True
By default, page load times are not tracked.
.. _`Site Speed report`: https://support.google.com/analytics/answer/1205784
.. _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` 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', SCOPE_SESSION)})
return some_template.render(context)
The value of the context variable is a tuple *(name, value, [scope])*.
The scope parameter is one of the
:const:`analytical.templatetags.google_analytics.SCOPE_*` constants:
================= ====== =============================================
Constant Value Description
================= ====== =============================================
``SCOPE_VISITOR`` 1 Distinguishes categories of visitors across
multiple sessions.
``SCOPE_SESSION`` 2 Distinguishes different visitor experiences
across sessions.
``SCOPE_PAGE`` 3 Defines page-level activity.
================= ====== =============================================
The default scope is :const:`~analytical.templatetags.google_analytics.SCOPE_PAGE`.
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`: http://code.google.com/apis/analytics/docs/tracking/gaTrackingCustomVariables.html
.. _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
decimal value of with up to two decimal places.
.. _`Sample Rate`: https://developers.google.com/analytics/devguides/collection/gajs/methods/gaJSApiBasicConfiguration#_setsamplerate
.. _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
decimal value of with up to two decimal places.
.. _`Site Speed Sample Rate`: https://developers.google.com/analytics/devguides/collection/gajs/methods/gaJSApiBasicConfiguration#_setsitespeedsamplerate
.. _google-analytics-session-cookie-timeout:
Session Cookie Timeout
----------------------
You can configure the `Session Cookie Timeout`_ feature by setting the
:const:`GOOGLE_ANALYTICS_SESSION_COOKIE_TIMEOUT` configuration setting::
GOOGLE_ANALYTICS_SESSION_COOKIE_TIMEOUT = 3600000
The value is the session cookie timeout in milliseconds or 0 to delete the cookie when the browser is closed.
.. _`Session Cookie Timeout`: https://developers.google.com/analytics/devguides/collection/gajs/methods/gaJSApiBasicConfiguration#_setsessioncookietimeout
.. _google-analytics-visitor-cookie-timeout:
Visitor Cookie Timeout
----------------------
You can configure the `Visitor Cookie Timeout`_ feature by setting the
:const:`GOOGLE_ANALYTICS_VISITOR_COOKIE_TIMEOUT` configuration setting::
GOOGLE_ANALYTICS_VISITOR_COOKIE_TIMEOUT = 3600000
The value is the visitor cookie timeout in milliseconds or 0 to delete the cookie when the browser is closed.
.. _`Visitor Cookie Timeout`: https://developers.google.com/analytics/devguides/collection/gajs/methods/gaJSApiBasicConfiguration#_setvisitorcookietimeout
You can find the web property ID on the overview page of your account.

View file

@ -1,168 +0,0 @@
===============================================
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. The global site tag (`gtag.js`_) is a JavaScript tagging
framework and API that allows you to send event data to Google Analytics,
Google Ads, and Google Marketing Platform.
.. _`Google Analytics`: http://www.google.com/analytics/
.. _`gtag.js`: https://developers.google.com/analytics/devguides/collection/gtagjs/
.. 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-gtag`.
The Google Analytics tracking code is inserted into templates using a
template tag. Load the :mod:`google_analytics_gtag` template tag library and
insert the :ttag:`google_analytics_gtag` 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_gtag %}
<html>
<head>
{% google_analytics_gtag %}
...
</head>
...
.. _google-analytics-configuration-gtag:
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-gtag-property-id:
Setting the property ID
-----------------------
Every website you track with Google Analytics gets its own property ID,
and the :ttag:`google_analytics_gtag` 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_GTAG_PROPERTY_ID` in the
project :file:`settings.py` file::
GOOGLE_ANALYTICS_GTAG_PROPERTY_ID = 'UA-XXXXXX-X'
If you do not set a property ID, the tracking code will not be rendered.
Please note that the accepted Property IDs should be one of the following formats:
- 'UA-XXXXXX-Y'
- 'AW-XXXXXXXXXX'
- 'G-XXXXXXXX'
- 'DC-XXXXXXXX'
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-identify-user:
Identifying authenticated users
-------------------------------
The username of an authenticated user is passed to Google Analytics
automatically as the ``user_id``. See :ref:`identifying-visitors`.
According to `Google Analytics conditions`_ you should avoid
sending Personally Identifiable Information.
Using ``username`` as ``user_id`` might not be the best option.
To avoid that, you can change the identity
by setting ``google_analytics_gtag_identity`` (or ``analytical_identity`` to
affect all providers) context variable:
.. code-block:: python
context = RequestContext({'google_analytics_gtag_identity': user.uuid})
return some_template.render(context)
or in the template:
.. code-block:: django
{% with google_analytics_gtag_identity=request.user.uuid|default:None %}
{% analytical_head_top %}
{% endwith %}
.. _`Google Analytics conditions`: https://developers.google.com/analytics/solutions/crm-integration#user_id
.. _google-analytics-custom-dimensions:
Custom dimensions
----------------
As described in the Google Analytics `custom dimensions`_ documentation
page, you can define custom dimensions which are variables specific to your
business needs. These variables can include both custom event parameters as
well as customer user properties. Using the template context variable
``google_analytics_custom_dimensions``, you can let the :ttag:`google_analytics_gtag`
pass custom dimensions to Google Analytics automatically. The ``google_analytics_custom_dimensions``
variable must be set to a dictionary where the keys are the dimension names
and the values are the dimension values. You can set the context variable in your
view when you render a template containing the tracking code::
.. code-block:: python
context = RequestContext({
'google_analytics_custom_dimensions': {
'gender': 'female',
'country': 'US',
'user_properties': {
'age': 25
}
}
})
return some_template.render(context)
Note that the ``user_properties`` key is used to pass user properties to Google
Analytics. It's not necessary to always use this key, but that'd be the way of
sending user properties to Google Analytics automatically.
You may want to set custom dimensions in a context processor that you add
to the :data:`TEMPLATE_CONTEXT_PROCESSORS` list in :file:`settings.py`::
.. code-block:: python
def google_analytics_segment_language(request):
try:
return {'google_analytics_custom_dimensions': {'language': 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 dimensions`: https://support.google.com/analytics/answer/10075209

View file

@ -1,238 +0,0 @@
====================================================
Google Analytics (analytics.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. The `analytics.js`_ library (also known as "the Google
Analytics tag") is a JavaScript library for measuring how users interact
with your website.
.. _`Google Analytics`: http://www.google.com/analytics/
.. _`analytics.js`: https://developers.google.com/analytics/devguides/collection/analyticsjs/
.. 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-js`.
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-js:
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-js-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-XXXXXXXX-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-js-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-js-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-js-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-js-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-js-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
Custom JavaScript Source
------------------------
You can configure a custom URL for the javascript file by setting the
:const:`GOOGLE_ANALYTICS_JS_SOURCE` configuration setting::
GOOGLE_ANALYTICS_JS_SOURCE = 'https://www.example.com/analytics.js'

View file

@ -1,99 +0,0 @@
===============================
GoSquared -- traffic monitoring
===============================
GoSquared_ provides both real-time traffic monitoring and and trends.
It tells you what is currently happening at your website, what is
popular, locate and identify visitors and track twitter.
.. _GoSquared: http://www.gosquared.com/
Installation
============
To start using the GoSquared 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 GoSquared 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:`gosquared-configuration`.
The GoSquared tracking code is inserted into templates using a template
tag. Load the :mod:`gosquared` template tag library and insert the
:ttag:`gosquared` 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::
{% load gosquared %}
...
{% gosquared %}
</body>
</html>
.. _gosquared-configuration:
Configuration
=============
When you set up a website to be tracked by GoSquared, it assigns the
site a token. You can find the token on the *Tracking Code* tab of your
website settings page. Set :const:`GOSQUARED_SITE_TOKEN` in the project
:file:`settings.py` file::
GOSQUARED_SITE_TOKEN = 'XXX-XXXXXX-X'
If you do not set a site token, the tracking code will not be rendered.
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:`GOSQUARED_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.
Identifying authenticated users
-------------------------------
If your websites identifies visitors, you can pass this information on
to GoSquared to display on the LiveStats dashboard. By default, the
name of an authenticated user is passed to GoSquared automatically. See
:ref:`identifying-visitors`.
You can also send the visitor identity yourself by adding either the
``gosquared_identity`` or the ``analytical_identity`` variable to
the template context. If both variables are set, the former takes
precedence. For example::
context = RequestContext({'gosquared_identity': identity})
return some_template.render(context)
If you can derive the identity from the HTTP request, you can also use
a context processor that you add to the
:data:`TEMPLATE_CONTEXT_PROCESSORS` list in :file:`settings.py`::
def identify(request):
try:
return {'gosquared_identity': request.user.username}
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.
----
Thanks go to GoSquared for their support with the development of this
application.

View file

@ -1,59 +0,0 @@
=====================================
Heap -- analytics and events tracking
=====================================
`Heap`_ automatically captures all user interactions on your site, from the moment of installation forward.
.. _`Heap`: https://heap.io/
.. heap-installation:
Installation
============
To start using the Heap 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.
.. _heap-configuration:
Configuration
=============
Before you can use the Heap integration, you must first get your
Heap Tracker ID. If you don't have a Heap account yet,
`sign up`_ to get your Tracker ID.
.. _`sign up`: https://heap.io/
.. _heap-tracker-id:
Setting the Tracker ID
----------------------
Heap gives you a unique ID. You can find this ID on the Projects page
of your Heap account. Set :const:`HEAP_TRACKER_ID` in the project
:file:`settings.py` file::
HEAP_TRACKER_ID = 'XXXXXXXX'
If you do not set an Tracker ID, the tracking code will not be
rendered.
The tracking code will be added just before the closing head tag.
.. _heap-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:`ANALYTICAL_INTERNAL_IPS` setting
(which is :const:`INTERNAL_IPS` by default,) the tracking code is
commented out. See :ref:`identifying-visitors` for important information
about detecting the visitor IP address.

View file

@ -1,73 +0,0 @@
=====================================
Hotjar -- analytics and user feedback
=====================================
`Hotjar`_ is a website analytics and user feedback tool.
.. _`Hotjar`: https://www.hotjar.com/
.. hotjar-installation:
Installation
============
To start using the Hotjar 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 Hotjar 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:`hotjar-configuration`.
The Hotjar code is inserted into templates using template tags.
Because every page that you want to track must have the tag,
it is useful to add it to your base template.
At the top of the template, load the :mod:`hotjar` template tag library.
Then insert the :ttag:`hotjar` tag at the bottom of the head section::
{% load hotjar %}
<html>
<head>
...
{% hotjar %}
</head>
...
</html>
.. _hotjar-configuration:
Configuration
=============
Before you can use the Hotjar integration, you must first set your Site ID.
.. _hotjar-id:
Setting the Hotjar Site ID
--------------------------
You can find the Hotjar Site ID in the "Sites & Organizations" section of your Hotjar account.
Set :const:`HOTJAR_SITE_ID` in the project :file:`settings.py` file::
HOTJAR_SITE_ID = 'XXXXXXXXX'
If you do not set a Hotjar Site ID, the code will not be rendered.
.. _hotjar-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:`HOTJAR_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.

View file

@ -1,79 +0,0 @@
============================
HubSpot -- inbound marketing
============================
HubSpot_ helps you get found by customers. It provides tools for
content creation, conversion and marketing analysis. HubSpot uses
tracking on your website to measure effect of your marketing efforts.
.. _HubSpot: http://www.hubspot.com/
.. hubspot-installation:
Installation
============
To start using the HubSpot 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 HubSpot 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:`hubspot-configuration`.
The HubSpot tracking code is inserted into templates using a template
tag. Load the :mod:`hubspot` template tag library and insert the
:ttag:`hubspot` 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::
{% load hubspot %}
...
{% hubspot %}
</body>
</html>
.. _hubspot-configuration:
Configuration
=============
Before you can use the HubSpot integration, you must first set your
portal ID, also known as your Hub ID.
.. _hubspot-portal-id:
Setting the portal ID
---------------------
Your HubSpot account has its own portal ID, the :ttag:`hubspot` tag
will include them in the rendered JavaScript code. You can find the
portal ID by accessing your dashboard. Alternatively, read this
`Quick Answer page <http://help.hubspot.com/articles/KCS_Article/Where-can-I-find-my-HUB-ID>`_.
Set :const:`HUBSPOT_PORTAL_ID` in the project :file:`settings.py` file::
HUBSPOT_PORTAL_ID = 'XXXX'
If you do not set the portal ID, the tracking code will not be rendered.
.. deprecated:: 0.18.0
`HUBSPOT_DOMAIN` is no longer required.
.. _hubspot-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:`HUBSPOT_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.

10
docs/services/index.rst Normal file
View file

@ -0,0 +1,10 @@
Services
========
A number of analytics services is supported.
.. toctree::
:maxdepth: 1
:glob:
*

View file

@ -1,166 +0,0 @@
=================================
Intercom.io -- Real-time tracking
=================================
Intercom.io_ is an easy way to implement real-chat and individual
support for a website
.. _Intercom.io: http://www.intercom.io/
.. intercom-installation:
Installation
============
To start using the Intercom.io 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 Intercom.io 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:`intercom-configuration`.
The Intercom.io JavaScript code is inserted into templates using a
template tag. Load the :mod:`intercom` template tag library and
insert the :ttag:`intercom` 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::
{% load intercom %}
<html>
<head></head>
<body>
<!-- Your page -->
{% intercom %}
</body>
</html>
...
.. _intercom-configuration:
Configuration
=============
Before you can use the Intercom.io integration, you must first set your
app id.
.. _intercom-site-id:
Setting the app id
--------------------------
Intercom.io gives you a unique app id, and the :ttag:`intercom`
tag will include it in the rendered JavaScript code. You can find your
app id by clicking the *Tracking Code* link when logged into
the on the intercom.io website. A page will display containing
HTML code looking like this::
<script id="IntercomSettingsScriptTag">
window.intercomSettings = { name: "Jill Doe", email: "jill@example.com", created_at: 1234567890, app_id: "XXXXXXXXXXXXXXXXXXXXXXX" };
</script>
<script>(function(){var w=window;var ic=w.Intercom;if(typeof ic==="function"){ic('reattach_activator');ic('update',intercomSettings);}else{var d=document;var i=function(){i.c(arguments)};i.q=[];i.c=function(args){i.q.push(args)};w.Intercom=i;function l(){var s=d.createElement('script');s.type='text/javascript';s.async=true;s.src='https://static.intercomcdn.com/intercom.v1.js';var x=d.getElementsByTagName('script')[0];x.parentNode.insertBefore(s,x);}if(w.attachEvent){w.attachEvent('onload',l);}else{w.addEventListener('load',l,false);}}})()</script>
The code ``XXXXXXXXXXXXXXXXXXXXXXX`` is your app id. Set
:const:`INTERCOM_APP_ID` in the project :file:`settings.py`
file::
INTERCOM_APP_ID = 'XXXXXXXXXXXXXXXXXXXXXXX'
If you do not set an app id, the JavaScript code will not be
rendered.
Custom data
-----------
As described in the Intercom documentation on `custom visitor data`_,
the data that is tracked by Intercom can be customized. Using template
context variables, you can let the :ttag:`intercom` tag pass custom data
to Intercom automatically. You can set the context variables in your view
when your render a template containing the tracking code::
context = RequestContext({'intercom_cart_value': cart.total_price})
return some_template.render(context)
For some data, it is annoying to do this for every view, so you may want
to set variables in a context processor that you add to the
:data:`TEMPLATE_CONTEXT_PROCESSORS` list in :file:`settings.py`::
from django.utils.hashcompat import md5_constructor as md5
GRAVATAR_URL = 'http://www.gravatar.com/avatar/'
def intercom_custom_data(request):
try:
email = request.user.email
except AttributeError:
return {}
email_hash = md5(email).hexdigest()
avatar_url = "%s%s" % (GRAVATAR_URL, email_hash)
return {'intercom_avatar': avatar_url}
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.
Standard variables that will be displayed in the Intercom live visitor
data are listed in the table below, but you can define any ``intercom_*``
variable you like and have that detail passed from within the visitor
live stream data when viewing Intercom.
==================== ===========================================
Context variable Description
==================== ===========================================
``intercom_name`` The visitor's full name.
-------------------- -------------------------------------------
``intercom_email`` The visitor's email address.
-------------------- -------------------------------------------
``intercom_user_id`` The visitor's user id.
-------------------- -------------------------------------------
``created_at`` The date the visitor created an account
==================== ===========================================
.. _`custom visitor data`: https://www.intercom.com/help/configure-intercom-for-your-product-or-site/customize-intercom-to-be-about-your-users/send-custom-user-attributes-to-intercom
Identifying authenticated users
-------------------------------
If you have not set the ``intercom_name``, ``intercom_email``, or ``intercom_user_id`` variables
explicitly, the username and email address of an authenticated user are
passed to Intercom automatically. See :ref:`identifying-visitors`.
.. _intercom-internal-ips:
Verifying identified users
--------------------------
Intercom supports HMAC authentication of users identified by user ID or email, in order to prevent impersonation.
For more information, see `Enable identity verification on your web product`_ in the Intercom documentation.
To enable this, configure your Intercom account's HMAC secret key::
INTERCOM_HMAC_SECRET_KEY = 'XXXXXXXXXXXXXXXXXXXXXXX'
(You can find this secret key under the "Identity verification" section of your Intercom account settings page.)
.. _`Enable identity verification on your web product`: https://www.intercom.com/help/configure-intercom-for-your-product-or-site/staying-secure/enable-identity-verification-on-your-web-product
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` setting
(which is :const:`INTERNAL_IPS` by default,) the tracking code is
commented out. See :ref:`identifying-visitors` for important information
about detecting the visitor IP address.

View file

@ -1,4 +1,3 @@
================================
KISSinsights -- feedback surveys
================================
@ -8,117 +7,29 @@ the targeted, actionable feedback you need to make your site better.
.. _KISSinsights: http://www.kissinsights.com/
.. kiss-insights-installation:
Installation
============
To start using the KISSinsights 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 KISSinsights 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:`kiss-insights-configuration`.
The KISSinsights survey code is inserted into templates using a template
tag. Load the :mod:`kiss_insights` template tag library and insert the
:ttag:`kiss_insights` 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 body::
{% load kiss_insights %}
...
</head>
<body>
{% kiss_insights %}
...
The code is added to the top of the HTML body. By default, the
username of a logged-in user is passed to KISSinsights. See
:data:`ANALYTICAL_AUTO_IDENTIFY`.
.. _kiss-insights-configuration:
Configuration
=============
Required settings
-----------------
Before you can use the KISSinsights integration, you must first set your
account number and site code.
.. data:: KISSINSIGHTS_ACCOUNT_NUMBER
The KISSinsights account number::
.. _kiss-insights-account-number:
KISSINSIGHTS_ACCOUNT_NUMBER = '12345'
Setting the account number and site code
----------------------------------------
.. data:: KISSINSIGHTS_SITE_CODE
In order to install the survey code, you need to set your KISSinsights
account number and website code. The :ttag:`kiss_insights` tag will
include it in the rendered JavaScript code. You can find the account
number and website code by visiting the code installation page of the
website you want to place the surveys on. You will see some HTML code
with a JavaScript tag with a ``src`` attribute containing
``//s3.amazonaws.com/ki.js/XXXXX/YYY.js``. Here ``XXXXX`` is the
account number and ``YYY`` the website code. Set
:const:`KISS_INSIGHTS_ACCOUNT_NUMBER` and
:const:`KISS_INSIGHTS_WEBSITE_CODE` in the project :file:`settings.py`
file::
The KISSinsights website code::
KISSINSIGHTS_ACCOUNT_NUMBER = 'XXXXX'
KISSINSIGHTS_SITE_CODE = 'XXX'
KISSINSIGHTS_SITE_CODE = 'abc'
If you do not set the account number and website code, the survey code
will not be rendered.
.. _kiss-insights-identity-user:
Identifying authenticated users
-------------------------------
If your websites identifies visitors, you can pass this information on
to KISSinsights so that you can tie survey submissions to customers.
By default, the username of an authenticated user is passed to
KISSinsights automatically. See :ref:`identifying-visitors`.
You can also send the visitor identity yourself by adding either the
``kiss_insights_identity`` or the ``analytical_identity`` variable to
the template context. If both variables are set, the former takes
precedence. For example::
context = RequestContext({'kiss_insights_identity': identity})
return some_template.render(context)
If you can derive the identity from the HTTP request, you can also use
a context processor that you add to the
:data:`TEMPLATE_CONTEXT_PROCESSORS` list in :file:`settings.py`::
def identify(request):
try:
return {'kiss_insights_identity': request.user.email}
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.
.. _kiss-insights-show-survey:
Showing a specific survey
-------------------------
KISSinsights can also be told to show a specific survey. You can let
the :ttag:`kiss_insights` tag include the code to select a survey by
passing the survey ID to the template in the
``kiss_insights_show_survey`` context variable::
context = RequestContext({'kiss_insights_show_survey': 1234})
return some_template.render(context)
For information about how to find the survey ID, see the explanation
on `"How can I show a survey after a custom trigger condition?"`_ on the
KISSinsights help page.
.. _`"How can I show a survey after a custom trigger condition?"`: http://www.kissinsights.com/help#customer-trigger
You can find the account number and website code by visiting the code
installation page of the website you want to place the surveys on. You
will see some HTML code with a Javascript tag with a ``src`` attribute
containing ``//s3.amazonaws.com/ki.js/XXXXX/YYY.js``. Here ``XXXXX`` is
the account number and ``YYY`` the website code.

View file

@ -1,4 +1,3 @@
==============================
KISSmetrics -- funnel analysis
==============================
@ -9,163 +8,19 @@ many drop out at each stage.
.. _KISSmetrics: http://www.kissmetrics.com/
.. kiss-metrics-installation:
Installation
============
To start using the KISSmetrics 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 KISSmetrics 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:`kiss-metrics-configuration`.
The KISSmetrics JavaScript code is inserted into templates using a
template tag. Load the :mod:`kiss_metrics` template tag library and
insert the :ttag:`kiss_metrics` 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 kiss_metrics %}
<html>
<head>
{% kiss_metrics %}
...
The code is added to the top of the HTML head. By default, the
username of a logged-in user is passed to KISSmetrics. See
:data:`ANALYTICAL_AUTO_IDENTIFY`.
.. _kiss-metrics-configuration:
Configuration
=============
Before you can use the KISSmetrics integration, you must first set your
API key.
.. _kiss-metrics-api-key:
Setting the API key
-------------------
Every website you track events for with KISSmetrics gets its own API
key, and the :ttag:`kiss_metrics` tag will include it in the rendered
JavaScript code. You can find the website API key by visiting the
website *Product center* on your KISSmetrics dashboard. Set
:const:`KISS_METRICS_API_KEY` in the project :file:`settings.py` file::
KISS_METRICS_API_KEY = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
If you do not set an API key, the tracking code will not be rendered.
.. _kiss-metrics-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:`KISS_METRICS_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.
.. _kiss-metrics-identify-user:
Identifying users
Required settings
-----------------
If your websites identifies visitors, you can pass this information on
to KISSmetrics so that you can tie events to users. By default, the
username of an authenticated user is passed to KISSmetrics
automatically. See :ref:`identifying-visitors`.
.. data:: KISSMETRICS_API_KEY
You can also send the visitor identity yourself by adding either the
``kiss_metrics_identity`` or the ``analytical_identity`` variable to the
template context. If both variables are set, the former takes
precedence. For example::
The website API key::
context = RequestContext({'kiss_metrics_identity': identity})
return some_template.render(context)
If you can derive the identity from the HTTP request, you can also use
a context processor that you add to the
:data:`TEMPLATE_CONTEXT_PROCESSORS` list in :file:`settings.py`::
def identify(request):
try:
return {'kiss_metrics_identity': request.user.email}
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.
.. _kiss-metrics-alias:
Alias
-----
Alias is used to associate one identity with another.
This most likely will occur if a user is not signed in yet,
you assign them an anonymous identity and record activity for them
and they later sign in and you get a named identity.
For example::
context = RequestContext({
'kiss_metrics_alias': {'my_registered@email' : 'my_user_id'},
})
return some_template.render(context)
The output script tag will then include the corresponding properties as
documented in the `KISSmetrics alias API`_ docs.
.. _`KISSmetrics alias API`: http://support.kissmetrics.com/apis/common-methods#alias
Recording events
----------------
You may tell KISSmetrics about an event by setting a variable in the context.
For example::
context = RequestContext({
'kiss_metrics_event': ['Signed Up', {'Plan' : 'Pro', 'Amount' : 9.99}],
})
return some_template.render(context)
The output script tag will then include the corresponding JavaScript event as
documented in the `KISSmetrics record API`_ docs.
.. _kiss-metrics-properties:
Recording properties
--------------------
You may also set KISSmetrics properties without a corresponding event.
For example::
context = RequestContext({
'kiss_metrics_properties': {'gender': 'Male'},
})
return some_template.render(context)
The output script tag will then include the corresponding properties as
documented in the `KISSmetrics set API`_ docs.
.. _`KISSmetrics set API`: http://support.kissmetrics.com/apis/common-methods#record
.. _`KISSmetrics record API`: http://support.kissmetrics.com/apis/common-methods#set
KISSMETRICS_API_KEY = '1234567890abcdef1234567890abcdef12345678'
You can find the website API key by visiting the website `Product
center` on your KISSmetrics dashboard.

View file

@ -1,75 +0,0 @@
==================================================
Lucky Orange -- All-in-one conversion optimization
==================================================
`Lucky Orange`_ is a website analytics and user feedback tool.
.. _`Lucky Orange`: https://www.luckyorange.com/
.. luckyorange-installation:
Installation
============
To start using the Lucky Orange 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 Lucky Orange 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:`luckyorange-configuration`.
The Lucky Orange tracking code is inserted into templates using template
tags. Because every page that you want to track must have the tag, it
is useful to add it to your base template. At the top of the template,
load the :mod:`luckyorange` template tag library. Then insert the
:ttag:`luckyorange` tag at the bottom of the head section::
{% load luckyorange %}
<html>
<head>
...
{% luckyorange %}
</head>
...
</html>
.. _luckyorange-configuration:
Configuration
=============
Before you can use the Lucky Orange integration, you must first set your
Site ID.
.. _luckyorange-id:
Setting the Lucky Orange Site ID
--------------------------------
You can find the Lucky Orange Site ID in the "Settings" of your Lucky
Orange account, reachable via the gear icon on the top right corner.
Set :const:`LUCKYORANGE_SITE_ID` in the project :file:`settings.py` file::
LUCKYORANGE_SITE_ID = 'XXXXXX'
If you do not set a Lucky Orange Site ID, the code will not be rendered.
.. _luckyorange-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:`LUCKYORANGE_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.

View file

@ -1,182 +0,0 @@
====================================================
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 between 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/
Ask for consent
---------------
If you do not want to track visitors without permission, you can `ask for consent`_ first.
To enable this, set :data:`MATOMO_ASK_FOR_CONSENT` to :const:`True`.
By default, no consent for tracking is needed (i.e. :const:`False`).
To give and remove consent in your page, create DOM elements with the following classes:
`matomo_give_consent` - class name for element to click when visitors want to **give** consent
`matomo_remove_consent` - class name for element to click when visitors want to **remove** consent
Examples::
# button to allow tracking
<button class="matomo_give_consent">Track me!</button>
# button to remove tracking consent
<button class="matomo_remove_consent">Don't track me anymore!</button>
.. _`asking for consent`: https://developer.matomo.org/guides/tracking-javascript-guide#asking-for-consent
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.

View file

@ -1,4 +1,3 @@
==========================
Mixpanel -- event tracking
==========================
@ -8,145 +7,18 @@ analysis of visitor retention or funnels.
.. _Mixpanel: http://www.mixpanel.com/
.. mixpanel-installation:
Installation
============
To start using the Mixpanel 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 Mixpanel 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:`mixpanel-configuration`.
The Mixpanel JavaScript code is inserted into templates using a
template tag. Load the :mod:`mixpanel` template tag library and
insert the :ttag:`mixpanel` 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 mixpanel %}
...
{% mixpanel %}
</head>
<body>
...
The code is added to the bottom of the HTML head. By default, the
username of a logged-in user is passed to Mixpanel. See
:data:`ANALYTICAL_AUTO_IDENTIFY`.
.. _mixpanel-configuration:
Configuration
=============
Before you can use the Mixpanel integration, you must first set your
token.
.. _mixpanel-api-key:
Setting the token
Required settings
-----------------
Every website you track events for with Mixpanel gets its own token,
and the :ttag:`mixpanel` tag will include it in the rendered JavaScript
code. You can find the project token on the Mixpanel *projects* page.
Set :const:`MIXPANEL_API_TOKEN` in the project :file:`settings.py`
file::
.. data:: MIXPANEL_TOKEN
MIXPANEL_API_TOKEN = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
The website project token ::
If you do not set a token, the tracking code will not be rendered.
MIXPANEL_TOKEN = '1234567890abcdef1234567890abcdef'
.. _mixpanel-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:`MIXPANEL_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.
.. _mixpanel-identify-user:
Identifying users
-----------------
If your websites identifies visitors, you can pass this information on
to Mixpanel so that you can tie events to users. By default, the
username of an authenticated user is passed to Mixpanel automatically.
See :ref:`identifying-visitors`.
You can also send the visitor identity yourself by adding either the
``mixpanel_identity`` or the ``analytical_identity`` variable to the
template context. If both variables are set, the former takes
precedence. For example::
context = RequestContext({'mixpanel_identity': identity})
return some_template.render(context)
If you can derive the identity from the HTTP request, you can also use
a context processor that you add to the
:data:`TEMPLATE_CONTEXT_PROCESSORS` list in :file:`settings.py`::
def identify(request):
try:
return {'mixpanel_identity': request.user.email}
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.
Mixpanel can also receive properties for your identified user, using
`mixpanel.people.set`_. If want to send extra properties, just set a
dictionary instead of a string in the ``mixpanel_identity`` context
variable. The key ``id`` or ``username`` will be used as the user unique
id, and any other key-value pair will be passed as *people properties*.
For example::
def identify(request):
try:
return {
'mixpanel_identity': {
'id': request.user.id,
'last_login': str(request.user.last_login),
'date_joined': str(request.user.date_joined),
}
}
except AttributeError:
return {}
.. _`mixpanel.people.set`: https://mixpanel.com/help/reference/javascript-full-api-reference#mixpanel.people.set
.. mixpanel-events:
Tracking events
===============
The django-analytical app integrates the Mixpanel JavaScript API in
templates. To tracking events in views or other parts of Django, you
can use Wes Winham's `mixpanel-celery`_ package.
If you want to track an event in JavaScript, use the asynchronous
notation, as described in the section titled
`"Asynchronous Tracking with JavaScript"`_ in the Mixpanel
documentation. For example::
mixpanel.track("play-game", {"level": "12", "weapon": "sword", "character": "knight"});
.. _mixpanel-celery: http://github.com/winhamwr/mixpanel-celery
.. _`"Asynchronous Tracking with JavaScript"`: http://mixpanel.com/api/docs/guides/integration/js#async
You can find the project token on the Mixpanel `projects` page.

Some files were not shown because too many files have changed in this diff Show more