mirror of
https://github.com/jazzband/django-analytical.git
synced 2026-03-17 06:30:26 +00:00
Compare commits
No commits in common. "main" and "v0.1.0" have entirely different histories.
149 changed files with 1908 additions and 11452 deletions
35
.github/workflows/check.yml
vendored
35
.github/workflows/check.yml
vendored
|
|
@ -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 }}
|
||||
44
.github/workflows/release.yml
vendored
44
.github/workflows/release.yml
vendored
|
|
@ -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
|
||||
58
.github/workflows/test.yml
vendored
58
.github/workflows/test.yml
vendored
|
|
@ -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
22
.gitignore
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
repos: []
|
||||
|
|
@ -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
|
||||
238
CHANGELOG.rst
238
CHANGELOG.rst
|
|
@ -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.
|
||||
|
|
@ -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/
|
||||
|
|
@ -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>`_.
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,3 +1 @@
|
|||
include LICENSE.txt *.rst
|
||||
recursive-include docs *.rst *.py
|
||||
recursive-include tests *.py
|
||||
include LICENSE.txt
|
||||
|
|
|
|||
179
README.rst
179
README.rst
|
|
@ -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/
|
||||
|
|
|
|||
|
|
@ -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'])
|
||||
|
|
|
|||
33
analytical/context_processors.py
Normal file
33
analytical/context_processors.py
Normal 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
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
"""
|
||||
Models for the django-analytical Django application.
|
||||
|
||||
This application currently does not use models.
|
||||
"""
|
||||
66
analytical/services/__init__.py
Normal file
66
analytical/services/__init__.py
Normal 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
|
||||
88
analytical/services/base.py
Normal file
88
analytical/services/base.py
Normal 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}
|
||||
57
analytical/services/chartbeat.py
Normal file
57
analytical/services/chartbeat.py
Normal 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)}
|
||||
47
analytical/services/clicky.py
Normal file
47
analytical/services/clicky.py
Normal 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
|
||||
42
analytical/services/console.py
Normal file
42
analytical/services/console.py
Normal 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
|
||||
31
analytical/services/crazy_egg.py
Normal file
31
analytical/services/crazy_egg.py
Normal 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
|
||||
58
analytical/services/google_analytics.py
Normal file
58
analytical/services/google_analytics.py
Normal 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
|
||||
39
analytical/services/kiss_insights.py
Normal file
39
analytical/services/kiss_insights.py
Normal 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)}
|
||||
51
analytical/services/kiss_metrics.py
Normal file
51
analytical/services/kiss_metrics.py
Normal 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)}
|
||||
45
analytical/services/mixpanel.py
Normal file
45
analytical/services/mixpanel.py
Normal 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)}
|
||||
21
analytical/services/optimizely.py
Normal file
21
analytical/services/optimizely.py
Normal 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}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
8
analytical/tests/__init__.py
Normal file
8
analytical/tests/__init__.py
Normal 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 *
|
||||
14
analytical/tests/services/__init__.py
Normal file
14
analytical/tests/services/__init__.py
Normal 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 *
|
||||
74
analytical/tests/services/test_base.py
Normal file
74
analytical/tests/services/test_base.py
Normal 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))
|
||||
72
analytical/tests/services/test_chartbeat.py
Normal file
72
analytical/tests/services/test_chartbeat.py
Normal 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)
|
||||
60
analytical/tests/services/test_clicky.py
Normal file
60
analytical/tests/services/test_clicky.py
Normal 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)
|
||||
45
analytical/tests/services/test_console.py
Normal file
45
analytical/tests/services/test_console.py
Normal 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)
|
||||
48
analytical/tests/services/test_crazy_egg.py
Normal file
48
analytical/tests/services/test_crazy_egg.py
Normal 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)
|
||||
56
analytical/tests/services/test_google_analytics.py
Normal file
56
analytical/tests/services/test_google_analytics.py
Normal 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')]})
|
||||
63
analytical/tests/services/test_kiss_insights.py
Normal file
63
analytical/tests/services/test_kiss_insights.py
Normal 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)
|
||||
58
analytical/tests/services/test_kiss_metrics.py
Normal file
58
analytical/tests/services/test_kiss_metrics.py
Normal 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"}]);')
|
||||
59
analytical/tests/services/test_mixpanel.py
Normal file
59
analytical/tests/services/test_mixpanel.py
Normal 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"}]);')
|
||||
42
analytical/tests/services/test_optimizely.py
Normal file
42
analytical/tests/services/test_optimizely.py
Normal 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>')
|
||||
14
analytical/tests/settings.py
Normal file
14
analytical/tests/settings.py
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
"""
|
||||
django-analytical testing settings.
|
||||
"""
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': ':memory:',
|
||||
}
|
||||
}
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'analytical',
|
||||
]
|
||||
80
analytical/tests/test_services.py
Normal file
80
analytical/tests/test_services.py
Normal 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)
|
||||
90
analytical/tests/test_template_tags.py
Normal file
90
analytical/tests/test_template_tags.py
Normal 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
64
analytical/tests/utils.py
Normal 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 = {}
|
||||
|
|
@ -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
|
||||
|
|
@ -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',
|
||||
)
|
||||
40
docs/conf.py
40
docs/conf.py
|
|
@ -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'),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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/
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
209
docs/install.rst
209
docs/install.rst
|
|
@ -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`.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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/*
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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'
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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
10
docs/services/index.rst
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
Services
|
||||
========
|
||||
|
||||
A number of analytics services is supported.
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
:glob:
|
||||
|
||||
*
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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
Loading…
Reference in a new issue