Compare commits

...

572 commits
v0.4.0 ... main

Author SHA1 Message Date
Ronard
e6f12719cc
Ask for consent - Matomo (Continuation Jayhaluska's work) (#245)
Some checks failed
Check / build (audit) (push) Has been cancelled
Check / build (docs) (push) Has been cancelled
Check / build (format) (push) Has been cancelled
Check / build (lint) (push) Has been cancelled
Check / build (package) (push) Has been cancelled
Test / python-django (4.2, 3.10) (push) Has been cancelled
Test / python-django (4.2, 3.11) (push) Has been cancelled
Test / python-django (4.2, 3.12) (push) Has been cancelled
Test / python-django (4.2, 3.9) (push) Has been cancelled
Test / python-django (5.1, 3.10) (push) Has been cancelled
Test / python-django (5.1, 3.11) (push) Has been cancelled
Test / python-django (5.1, 3.12) (push) Has been cancelled
Test / python-django (5.1, 3.13) (push) Has been cancelled
Test / python-django (5.2, 3.10) (push) Has been cancelled
Test / python-django (5.2, 3.11) (push) Has been cancelled
Test / python-django (5.2, 3.12) (push) Has been cancelled
Test / python-django (5.2, 3.13) (push) Has been cancelled
Co-authored-by: Julian Haluska <j.haluska@gmx.de>
2026-03-08 17:10:32 +01:00
Peter Bittner
694fc9097a Use up-to-date version of PyPI publish Action
Fixes an outdated reference that aborted the last release attempt.
2025-07-21 18:07:06 +02:00
Peter Bittner
10102d1017 Add Erick Massip as author for contributing via PR #226 2025-07-21 17:11:22 +02:00
Peter Bittner
033d2dc02f Align spelling of JavaScript across all files (docs, docstrings) 2025-07-21 17:11:22 +02:00
Peter Bittner
4ea4605791 Add changelog instructions, remove Gitter badge 2025-07-21 17:11:22 +02:00
Erick Massip
7253a3a048
Fix gtag user_id setup and add support for custom dimensions (#226)
* Fixed user_id setup for gtag according to latest docs
* Added the possibility to include custom dimensions to be sent along with every event
* Added custom dimensions section to the gtag docs
* Added explicit python code blocks in the docs
* Reformat test_tag_google_analytics_gtag.py with Ruff formatter
2025-07-21 11:10:04 +02:00
Peter Bittner
a751ffa5e3 Release v3.2.0 2025-07-11 00:06:19 +02:00
Peter Bittner
8b12e33e69 Add more config options for Woopra
Closes #100
2025-07-09 18:17:27 +02:00
Peter Bittner
e0f95bc86b Fix deprecated set-output for pip-cache in GHA jobs
See:
- https://github.blog/changelog/2022-10-11-github-actions-deprecating-save-state-and-set-output-commands/
- https://github.com/actions/setup-python#caching-packages-dependencies
- https://github.com/actions/setup-python/blob/main/docs/advanced-usage.md#caching-packages
2025-07-09 13:58:57 +02:00
Peter Bittner
110d5bf361 Fix broken/missing author credits in documentation
Fixes broken text include in History chapter introduced via commit 7704f8c.
2025-07-09 13:58:57 +02:00
Peter Bittner
0f099b8ab4 Configure coverage via pyproject.toml, show results in CI 2025-07-09 13:58:57 +02:00
Peter Bittner
18b5a73bd3 Fix confusingly swapped display on GHA 2025-07-09 09:03:56 +02:00
Peter Bittner
df5b04b932 Remove duplicate table entry (tox-gh-actions) 2025-07-09 09:03:56 +02:00
Peter Bittner
7704f8c56b Remove AUTHORS file to avoid confusion 2025-07-09 09:03:56 +02:00
Peter Bittner
25d08a8633 Simplify formatting of Intercom test code 2025-07-09 09:03:56 +02:00
Peter Bittner
a246d62ae0 Address warnings in docs build process
Adjusts copyright following https://reuse.software/faq/#years-copyright
2025-07-09 09:03:56 +02:00
Peter Bittner
0d87a76da0 Adhere to new license specification in metadata
See https://packaging.python.org/en/latest/guides/writing-pyproject-toml/#license

Note that this must drop (the end-of-life) Python 3.8, because
setuptools>=80 requires Python 3.9+ and older setuptools versions
require the license field to be a table instead of a string value.
2025-07-09 09:03:56 +02:00
Peter Bittner
6850710413 Remove uv lockfile upon cleanup to avoid audit to freak out 2025-07-09 09:03:56 +02:00
Peter Bittner
4540999dc1 Remove obsolete type attribute in script tags 2025-07-08 12:22:45 +02:00
Peter Bittner
d3ddf1ac6a Update outdated PyPI URL references 2025-07-08 12:22:45 +02:00
Peter Bittner
1dd5b2ac62 Fix Sphinx documentation issues, add RTD configuration 2025-07-08 12:22:45 +02:00
Peter Bittner
a112ec445f Reformat code using Ruff 2025-07-08 12:22:45 +02:00
Peter Bittner
82fbbeb6b2 Migrate packaging from setup.py to pyproject.toml 2025-07-08 12:22:45 +02:00
Jannis Leidel
8d6419eec8
Merge pull request #220 from jazzband/cleanup/setup-cfg
Remove apparently obsolete setup.cfg
2023-05-02 10:23:20 +02:00
Tim Gates
6e367cdd2d docs: fix simple typo, convesions -> conversions
There is a small typo in docs/services/spring_metrics.rst.

Should read `conversions` rather than `convesions`.

Signed-off-by: Tim Gates <tim.gates@iress.com>
2022-08-03 01:18:16 +02:00
Petr Dlouhý
2163c77684
Add the missing get_identity prefix to google_analytics_gtag, test it (#217) 2022-07-19 08:58:28 +02:00
Petr Dlouhý
858a05307b Update docs/features.rst
Co-authored-by: Peter Bittner <django@bittner.it>
2022-07-18 16:54:29 +02:00
Petr Dlouhý
19c43bef4a Update docs/features.rst
Co-authored-by: Peter Bittner <django@bittner.it>
2022-07-18 16:54:29 +02:00
Petr Dlouhý
5ad082df48 Update docs/features.rst
Co-authored-by: Peter Bittner <django@bittner.it>
2022-07-18 16:54:29 +02:00
Petr Dlouhý
47a5a4d6a9 more fixes to the docs 2022-07-18 16:54:29 +02:00
Petr Dlouhý
f56a779b7a Update docs/features.rst
Co-authored-by: Peter Bittner <django@bittner.it>
2022-07-18 16:54:29 +02:00
Petr Dlouhý
a82bde708a Update docs/services/google_analytics_gtag.rst
Co-authored-by: Peter Bittner <django@bittner.it>
2022-07-18 16:54:29 +02:00
Petr Dlouhý
83e4361d2a Update docs/features.rst
Co-authored-by: Peter Bittner <django@bittner.it>
2022-07-18 16:54:29 +02:00
Petr Dlouhý
25979fd0c2 Update docs/features.rst
Co-authored-by: Peter Bittner <django@bittner.it>
2022-07-18 16:54:29 +02:00
Petr Dlouhý
96f1ba98b1 add more information about overriding identity to documentation 2022-07-18 16:54:29 +02:00
Petr Dlouhý
7e28ee31f1 add custom identity test 2022-07-16 13:25:39 +02:00
Peter Bittner
e596e20183 Remove apparently obsolete setup.cfg 2022-07-14 14:17:22 +02:00
Peter Bittner
248e04aef5 Fix outdated URLs that point to the Clickmap service
Fixes #182
2022-07-14 10:56:02 +02:00
Peter Bittner
f487cb895c Fix Matomo noscript tag
Reference: https://matomo.org/faq/how-to/faq_176/
2022-07-06 23:52:03 +02:00
Peter Bittner
688524305f Remove deprecated Piwik integration
Please use our Matomo integration instead
2022-07-06 23:52:03 +02:00
Kevin Olbrich
ba59f396df add GOOGLE_ANALYTICS_JS_SOURCE setting to allow custom source URL
fixes https://github.com/jazzband/django-analytical/issues/212
2022-07-06 08:30:22 +02:00
Peter Bittner
844a178c04 Simplify copyright notice
Co-authored-by: David Smith <39445562+smithdc1@users.noreply.github.com>
2022-03-15 09:34:50 +01:00
Peter Bittner
ce90933ab1 Release v3.1.0
Removed optional license field from metadata, following recommendations from packaging guide.
See https://packaging.python.org/en/latest/guides/distributing-packages-using-setuptools/#license
2022-03-15 09:34:50 +01:00
Peter Bittner
a8be4ea814 Readability counts, code must be self-explanatory 2022-03-14 22:51:05 +01:00
Peter Bittner
d82ca0aed5 Use context manager for file reading 2022-03-11 11:16:31 +01:00
Peter Bittner
52aa88f08a Use pathlib instead of os.path (we only support Python 3.6+) 2022-03-11 10:15:45 +01:00
Peter Bittner
023702fa75 Consolidate tox.ini and pyproject.toml 2022-03-10 15:50:29 +01:00
Jannis Leidel
1d9a3b5626
Merge pull request #205 from jazzband/feature/rename-default-branch
Rename default branch (master ➜ main)
2022-02-06 21:08:58 +01:00
Peter Bittner
2fd2279ecb Rename default branch (master ➜ main)
Following the example of other popular free software projects, we also rename
our default branch to remove terminology stemming from colonialism and slavery.
2022-02-05 18:42:08 +01:00
Peter Bittner
a021e07e15
Improve GHA test jobs, move tool config to pyproject.toml (#202)
* Move Tox tool config settings to pyproject.toml
* Restore older Bandit version for predictive setup
   Bandit UX is seriously broken. Unfortunately, only <1.6 works predictably.
* Exclude unsupported Python/Django combinations
   Use a more self-explanatory build job name for tests
* Help pytest find the Django test project settings
* Use clean range for Django 4.0
* Keep load on the GHA runners low, use newer Python for checks
2022-02-04 19:04:03 +01:00
merc1er
051d46ceda Fix typo 2022-02-04 03:12:45 +01:00
Peter Bittner
8d449868da Use job name suggested by @jezdez
Co-authored-by: Jannis Leidel <jannis@leidel.info>
2021-11-27 19:14:13 +01:00
Peter Bittner
44e642c34b Add clean Tox target (run tox -e clean) 2021-11-27 19:14:13 +01:00
Peter Bittner
41b8c243d4 Add checks as a separate workflow
Run test suite only for PRs and master
2021-11-27 19:14:13 +01:00
Peter Bittner
5daa0c5329 Split up test matrix into Python-Django combinations 2021-11-27 19:14:13 +01:00
Jannis Leidel
e4399d63a1
Merge pull request #201 from jazzband/fix/missing-pre-commit-config
Fix pre-commit-ci complaint about missing config file
2021-11-26 17:02:41 +01:00
Peter Bittner
0b162449c5 Fix pre-commit-ci complaint about missing config file 2021-11-26 14:23:19 +01:00
Garrett Coakley
58aa95df34
Added Heap integration (#194)
Heap integration
2021-11-26 14:13:59 +01:00
David Smith
aa29a051bd Dropped Django 3.1 support 2021-11-26 01:29:40 +01:00
David Smith
dbd7414b33 Added Python 3.10 support. 2021-11-26 01:29:40 +01:00
Jannis Leidel
1923108e93
Merge pull request #199 from jazzband/jazzband/sync/default
Jazzband: Synced file(s) with jazzband/.github
2021-10-22 17:47:07 +02:00
jazzband-bot
a615b954ae Jazzband: Created local 'CODE_OF_CONDUCT.md' from remote 'CODE_OF_CONDUCT.md' 2021-10-21 14:33:50 +00:00
David Smith
392cbba489 Updated supported Django versions 2021-10-05 09:30:53 +02:00
David Smith
37009f2e08 Implemented isort 2021-02-21 17:28:27 +01:00
David Smith
f0e7ebe731 Converted remaining tests to Pytest style 2021-02-18 09:34:01 +01:00
David Smith
baaa1c4a8b Migrate tests to Pytest plain asserts 2021-02-18 09:34:01 +01:00
Peter Bittner
896a19196f Release v3.0.0 2020-12-06 23:14:25 +01:00
Peter Bittner
8cd75d9e60 Add business code for Lucky Orange 2020-12-06 20:45:08 +01:00
Peter Bittner
a3fe981f76 Add tests for Lucky Orange 2020-12-06 20:45:08 +01:00
Peter Bittner
514780aeb2 Add documentation for Lucky Orange 2020-12-06 20:45:08 +01:00
Peter Bittner
7f1358dcb6 Hard-require Python 3.6+, add link to docs on PyPI 2020-12-06 16:26:54 +01:00
Pi Delport
5ea7d15868 Coverage config: Remove stale omit entry 2020-12-06 16:26:54 +01:00
Pi Delport
47fa937910 Include tests in source manifest 2020-12-06 16:26:54 +01:00
Peter Bittner
bf471d7dfc Allow test suite to run with pytest 2020-12-06 16:26:54 +01:00
Peter Bittner
a9d7d17ce6 Move tests out of analytical module 2020-12-06 16:26:54 +01:00
Peter Bittner
ba8982be0a Add missing Google Analytics instructions to install section 2020-12-06 13:21:12 +01:00
Peter Bittner
00bc123b88 Reduce indentation of Hotjar JS code 2020-12-05 21:11:06 +01:00
David Smith
4515fcd1be Added quotes to Python versions 2020-12-03 23:01:10 +01:00
David Smith
9163294603 Updated supported versions of Python and Django
Dropped Python2.7/3.5 and Django 1.11 from test matrix

Removed Py2 code and updated for Py3 features

Replaced assertRaisesRegexp with AsserRaisesRegex

Updated setup.py for currently supported versions

Removed _timestamp
2020-12-03 23:01:10 +01:00
David Smith
3eb17007ad Refactored setup.py to remove cmdclass and test packages 2020-11-30 23:02:04 +01:00
David Smith
df7ed2d132 Fix flake8 errors 2020-11-30 23:02:04 +01:00
David Smith
10d109eb6c Use pytest as test runner
* Used pytest to run tests within tox environments

* Removed test runner within setup.py
2020-11-30 23:02:04 +01:00
Jannis Leidel
146a96fca0
Quote Python versions. 2020-11-26 09:41:44 +01:00
Jannis Leidel
98e7e24e3e
Merge pull request #169 from jazzband/gha
Add initial GitHub Actions workflow.
2020-11-25 21:47:50 +01:00
Jannis Leidel
161983fad5
Merge branch 'master' into gha 2020-11-25 21:43:33 +01:00
Jannis Leidel
197cdbcfad
Add Jazzband release workflow. 2020-11-25 21:18:45 +01:00
Jannis Leidel
7580bc1619
Ignore it for real? 2020-11-25 21:15:45 +01:00
Jannis Leidel
de2ed4bafc
Ignore bandit errors. 2020-11-25 21:11:14 +01:00
Jannis Leidel
2322f7a0bf
Fix config section names. 2020-11-25 21:04:49 +01:00
Jannis Leidel
1ebc2a23ea
Add gh-action mapping. 2020-11-25 21:01:53 +01:00
Jannis Leidel
b9320b41d9
Add initial GitHub Actions workflow. 2020-11-25 20:56:03 +01:00
Peter Bittner
e517bce3a6 Add SnapEngage details to Installation section
Fixes #166
2020-11-25 17:00:22 +01:00
Joost Cassee
ef40ba6ff5
Merge pull request #164 from taharushain/master
Updated regex for gtag to accept measurement ID
2020-08-02 12:39:40 +02:00
Taha Rushain
ee956230ea Breaking long strings into multiple lines 2020-07-19 02:05:12 +05:00
Taha Rushain
d98e815493 Breaking long strings into multiple lines 2020-07-19 01:57:56 +05:00
Taha Rushain
1cdfb0ed0d Breaking long strings into multiple lines 2020-07-19 01:49:38 +05:00
Taha Rushain
3292b664ec Updated Gtag implementation for mutiple id patterns 2020-07-17 19:24:51 +05:00
Taha Rushain
ac2ebf375c Updated regex for gtag to accept measurement ID 2020-07-06 01:38:40 +05:00
Joost Cassee
7e68563849
Merge pull request #162 from jazzband/piwik-warning
Only show piwik warning if using piwik
2020-07-03 10:29:23 +02:00
Hugo Osvaldo Barrera
788cab447e Only show piwik warning if using piwik
Code so far emits a warning to all users about about Piwik being
deprecated, even if Piwik is not being used.

Scope the warning so it only triggers for people to whom it's relevant.
2020-07-02 17:34:23 +02:00
Sean Wallace
88197ca17e Fix flake8. 2020-06-28 21:00:41 +02:00
Sean Wallace
438b1408fa Add user_id to Google Analytics GTag 2020-06-28 21:00:41 +02:00
Peter Bittner
962af837af Include deploy stage (excluded by error) 2020-04-15 16:44:18 +02:00
Peter Bittner
c10759d378 Bump version, release v2.6.0 2020-04-15 14:51:05 +02:00
Peter Bittner
d9b21e96c6 Test against Django 3.0, Python 3.8
Modernize Travis CI setup
2020-04-15 14:51:05 +02:00
Peter Bittner
77fdb51432 Explain the three Google Analytics options 2020-04-15 14:51:05 +02:00
Marc Bourqui
1ce265d2d9 Update test to comply with flake8 2020-04-11 18:43:31 +02:00
Marc Bourqui
d14d4727ee Add tests for new Google Analytics gtag, #108 2020-04-11 18:43:31 +02:00
Marc Bourqui
63adb0fbba Register new Google Analytics gtag, #108 2020-04-11 18:43:31 +02:00
Marc Bourqui
0ea5d39155 Fix typos 2020-04-11 18:43:31 +02:00
Marc Bourqui
756ac787e8 Add support for gtag.js 2020-04-11 18:43:31 +02:00
Tim Gates
637805e003 docs: Fix simple typo, betweeen -> between
There is a small typo in docs/services/matomo.rst, docs/services/piwik.rst.

Should read `between` rather than `betweeen`.
2020-03-03 09:01:27 +01:00
Joost Cassee
5487fd677b
Merge pull request #153 from vdboor/fix/google-method-ordering2
Make sure new Google Analytics anonimyze IP calls also happen before sending pageviews
2019-04-26 23:53:04 +02:00
Joost Cassee
724e4ddac4
Merge branch 'master' into fix/google-method-ordering2 2019-04-24 00:00:09 +02:00
Jannis Leidel
c7f8dc21ad
Updated Travis config with Jazzband release credentials. 2019-04-23 11:23:13 +02:00
Diederik van der Boor
84e7dab3d2
Make sure Google Analytics calls for ga('set', ...) happen before ga('send', ...)
The changes for the old ga.js code were addressed in #127,
before the new analytics.js code was merged by #135 (commits mention #108)
The display_features and commands could be merged to avoid unneeded blank lines.
2019-04-20 08:53:31 +02:00
Scott Karlin
b96bf0050a Update copyright line 2019-04-10 09:26:33 +02:00
Scott Karlin
d5de14ebdc Update authors and license date 2019-04-10 09:26:33 +02:00
Scott Karlin
02b0768c97 Correct code alignment (PEP8) 2019-04-10 09:26:33 +02:00
Scott Karlin
ef0b19ccb2 Clarify Piwik deprecation status 2019-04-10 09:26:33 +02:00
Scott Karlin
3b1ab2bbed Matomo still uses "piwik" in img tag 2019-04-10 09:26:33 +02:00
Scott Karlin
e5f8c199dc Create Matomo module; leave Piwik to be deprecated
With the rebranding of Piwik to Matomo, this commit:
* copies the piwik module to matomo and rebrands
* notes that the piwik module is deprecated
* updates the javascript to the current Matomo version

Implements #132
2019-04-10 09:26:33 +02:00
Peter Bittner
bfe92c716c Ensure Tox targets clean and docs are working
Update packaging information (Django 2.2, license)

Remove problematic install_requires
2019-04-09 14:43:50 +02:00
Peter Bittner
367606c12c Remove code-health button (Codacy/Landscape) for now 2019-04-09 09:27:33 +02:00
Peter Bittner
dc975aa2d3 Use Shields.io badges uniformly 2019-04-09 09:27:33 +02:00
Peter Bittner
29a95f0369 Allow Bandit to fail (for now) on Travis 2019-04-09 09:27:33 +02:00
Peter Bittner
3680872619 Travis still doesn't support Python 3.7 in the default image 2019-04-09 09:27:33 +02:00
Peter Bittner
4e874d88ae Add Django 2.2 to the test matrix
Remove all officially unsupported Python+Django combinations

Add Bandit check, add Codacy badge

Add docs target in Tox configuration

Use build stages with Travis CI
2019-04-09 09:27:33 +02:00
Peter Bittner
4a07cb35e4 Replace deprecated readme check
Make flake8 ignore build folders
2019-04-08 20:44:22 +02:00
Joost Cassee
c4934b4c96 Use xenial dist for Travis readme check
This is required because Django now requires Sqlite version 3.8.3 or
higher.
2019-04-08 20:44:22 +02:00
Joost Cassee
81502c3d68 Fix flake8 warnings by removing typing hints
Unfortunately, typing does not exist in Python 2 so we cannot fix the
warnings by importing the types.
2019-04-08 20:44:22 +02:00
Jannis Leidel
456ab03a7d
Merge pull request #139 from jazzband/prepare-jazzband
Implement Jazzband guidelines
2018-11-30 10:18:37 +01:00
Joost Cassee
005c00c272 Mention Jazzband in authors 2018-11-28 21:57:30 +01:00
Joost Cassee
cb02a33b9f Add contributing guidelines 2018-11-28 21:46:13 +01:00
Joost Cassee
b6ed31e034 Add Jazzband badge to readme 2018-11-28 21:42:13 +01:00
Joost Cassee
fb4f1c1b40 Change all links from .../jcassee/d-a to jazzband 2018-11-28 21:36:23 +01:00
Joost Cassee
1164f75969 Change my name link to the GitHub profile 2018-11-26 14:16:49 +01:00
Joost Cassee
ba89b80695 Fix typo in test function name and update authors 2018-11-16 11:02:13 +01:00
Joost Cassee
1ac44107df Bump version to 2.5.0 2018-11-15 21:18:07 +01:00
Joost Cassee
abde59822d Fix flake8 warning 2018-11-15 21:03:18 +01:00
Joost Cassee
a5ca5c7ae1 Update changelog and authors list 2018-11-15 20:57:33 +01:00
Joost Cassee
4f4de63759 Merge remote-tracking branch 'anteru/master' 2018-11-15 20:43:47 +01:00
Diederik van der Boor
deba4e0021 Remove unneeded spaces in _gaq.push commands 2018-11-15 20:35:03 +01:00
Diederik van der Boor
d2c782c1d5 Make sure Google Analytics calls like _anonymizeIp settings happen before calling _trackPageview 2018-11-15 20:35:03 +01:00
Joost Cassee
32c45f2f0c
Merge pull request #135 from EDSI-Tech/issue-108
Basic support of Google analytics.js
2018-11-15 20:19:09 +01:00
Joost Cassee
e91615b754
Merge pull request #137 from pjdelport/add-hotjar-support
Add Hotjar support
2018-11-15 20:16:42 +01:00
Joost Cassee
d08da39fb1
Merge pull request #134 from pjdelport/intercom-hmac-identity-verification
Support Intercom HMAC identity verification
2018-11-15 20:10:39 +01:00
Pi Delport
3007bca61e README: Add Hotjar to list 2018-11-15 17:37:38 +02:00
Pi Delport
de3c2d73d1 Add docs for Hotjar 2018-11-15 17:37:38 +02:00
Pi Delport
c2a11ce794 Add initial Hotjar tracking code support 2018-11-15 17:37:33 +02:00
Pi Delport
2d8cbd25bd Address flake8 W605 (invalid escape sequence) 2018-11-15 15:05:38 +01:00
Marc Bourqui
736472587d Fix missing protocol in script url, #108 2018-09-26 13:43:46 +02:00
Marc Bourqui
11e0372017 Fix docs, #108 2018-09-26 13:39:33 +02:00
Marc Bourqui
bf3a1aebde Fix flake8 errors, #108 2018-09-24 17:26:15 +02:00
Marc Bourqui
d7fd927b2c Set distinct property id context var, #108 2018-09-24 17:18:04 +02:00
Marc Bourqui
d33d380dd4 Fix tests, #108 2018-09-24 17:07:20 +02:00
Marc Bourqui
771848b428 Fix missing quotes for string value, #108 2018-09-24 17:07:01 +02:00
Marc Bourqui
30e9ddc7cf Fix missing escaping curly braces, #108 2018-09-24 16:32:37 +02:00
Marc Bourqui
b837a20ed4 Fix: global name 'commands' is not defined, #108 2018-09-24 15:52:08 +02:00
Marc Bourqui
d76c72e2a5 Update docs, #108 2018-09-24 15:51:54 +02:00
Marc Bourqui
f8c4f6bf47 Basic support of Google analytics.js, #108 2018-09-24 15:25:16 +02:00
Pi Delport
1b7429c3e1 (Python 2 compatibility for datetime.timestamp) 2018-08-22 18:51:43 +02:00
Pi Delport
4b4f26f54e Intercom: Add support for HMAC authentication of identified users
Documentation:
https://www.intercom.com/help/configure-intercom-for-your-product-or-site/staying-secure/enable-identity-verification-on-your-web-product
2018-08-22 18:43:07 +02:00
Pi Delport
47cf9aac3e Intercom: Set user_id field for authenticated users
This is one of Intercom's core user detail fields.
2018-08-22 18:20:39 +02:00
Joost Cassee
128c535550
Merge pull request #130 from pjdelport/enable-intercom-anonymous-users
Enable Intercom for anonymous users
2018-08-22 16:32:14 +02:00
Pi Delport
c34d429f57 Enable Intercom for anonymous users
Update tests to add conventional test_render_internal_ip.
2018-08-22 16:17:25 +02:00
Pi Delport
0a549c8ee6 Drop Python 3.3, add Python 3.7 and Django 2.1
This is a combination of 5 commits.

Commit message #1:

    Drop support for Python 3.3

    Setuptools and Tox no longer support or work on Python 3.3.

Commit message #2:

    Tox / Travis: Add Python 3.7 and Django 2.1

Commit message #3:

    Tox: Django 2.1 drops support for Python 3.4

Commit message #4:

    Travis: Add workaround for Python 3.7

    Upstream issue: https://github.com/travis-ci/travis-ci/issues/9815

Commit message #5:

    Travis: Enable pip caching
2018-08-22 15:50:32 +02:00
Nikolay Korotkiy
926b46dc03 docs: Piwik is now Matomo 2018-08-20 19:31:06 +02:00
Pi Delport
8f4a62ac6d Intercom docs: Fix broken link for custom data 2018-08-20 19:03:36 +02:00
Matthäus G. Chajdas
7b3c107758 Add PIWIK_DISABLE_COOKIES.
This option disables cookies -- it's off by default.
2018-05-06 09:34:05 +02:00
Joost Cassee
ac66302ddc Update changelog and bump version to 2.4.0 2017-12-07 09:19:19 +01:00
Peter Bittner
3653b388fd
Beautification (fix a missing blank) 2017-12-05 23:53:07 +01:00
Joost Cassee
dfc8baf2ee
Merge pull request #121 from Anteru/master
Use user.is_authenticated for Django 2.0 compatibility.
2017-12-05 09:37:28 +01:00
Matthäus G. Chajdas
a9d0befa46 Add support for Django <2 and 2; update tox test matrix. 2017-12-03 18:28:38 +01:00
Matthäus G. Chajdas
4c72289ac8 Use user.is_authenticated for Django 2.0 compatibility. 2017-12-02 22:39:03 +01:00
Joost Cassee
7bc1467374
Merge pull request #119 from bittner/master
Use more targeted approach for README conversion check
2017-11-24 20:39:12 +01:00
Peter Bittner
2e9329dbb8 Use more targeted approach for README conversion check 2017-11-23 00:56:11 +01:00
Joost Cassee
be7b530f7e
Merge pull request #118 from bittner/feature/add-checkdocs-tox-target
Check PyPI package description generation w/ tox
2017-11-18 22:41:08 +01:00
Peter Bittner
ef44d0ccf1 Check PyPI package description generation w/ tox 2017-11-18 19:56:33 +01:00
Peter Bittner
9738f31688 Move package description to module docstring 2017-10-22 14:28:25 +02:00
Peter Bittner
4e8644fbdc Fix non-critical issues forgotten for v2.3.0 2017-10-22 14:28:25 +02:00
Joost Cassee
347df54502 Use readthedocs.io instead of pythonhosted.org 2017-10-19 00:36:50 +02:00
Joost Cassee
77e1d6f95a Bump version to 2.3.0 and update changelog 2017-10-18 23:50:02 +02:00
Joost Cassee
6f71ddf04f Merge pull request #115 from pjdelport/add-facebook-pixel-support
Add Facebook Pixel support
2017-10-18 21:06:52 +02:00
Joost Cassee
5f0b1128f2 Merge pull request #114 from pjdelport/update-tests-for-django-1.11
Update tests for Django 1.10 & 1.11
2017-10-18 21:06:11 +02:00
Pi Delport
2ea2f824e8 Add some remaining test coverage
This should bring the facebook_pixel module up to 100%.
2017-10-18 12:36:18 +02:00
Pi Delport
be564dd2b8 (Ignore coverage for intentionally unimplemented property) 2017-10-18 12:34:22 +02:00
Pi Delport
621d207944 (Use more compact format, for readability) 2017-10-18 10:47:59 +02:00
Pi Delport
969377b39a Tox: Expand Django versions in envlist 2017-10-17 17:29:11 +02:00
Pi Delport
a7246fbe44 Add initial Facebook Pixel docs 2017-10-17 16:46:14 +02:00
Pi Delport
70b2ff9081 README: Add Facebook Pixel 2017-10-17 16:07:40 +02:00
Pi Delport
d19e1bfdb7 Package classifiers: Add Python 3.6, Django 1.10 & 1.11 2017-10-17 16:02:40 +02:00
Pi Delport
cd9394467e Add initial Facebook Pixel support 2017-10-17 16:00:54 +02:00
Pi Delport
2ee99a656b (Update my name, while here) 2017-10-17 14:12:35 +02:00
Pi Delport
4157c55e26 Tox, Travis: Update for Python 3.6, Django 1.10 & 1.11 2017-10-17 14:07:20 +02:00
Pi Delport
4ada8ff81a Test settings: Add TEMPLATES for Django 1.10+ 2017-10-17 13:35:38 +02:00
Peter Bittner
c7b14bdf5c Drop Python 3.2 support
Refactor Travis CI configuration

Add flake8 checks, address all complaints
2017-10-16 12:11:23 +02:00
Joost Cassee
bec45403ee Merge pull request #112 from bogdanbodnar/patch-2
Fix holding config variable in database through proxy objects
2017-08-17 16:52:49 +02:00
Bodnar Bogdan
9df1182877 Fix exception message 2017-08-16 10:41:09 +02:00
Bodnar Bogdan
22403f9a03 Fix uservoice tests
Rewrite test_empty_key in the same way as in the other modules.
Delete test_overridden_empty_key, because the Node (In this case, the UserVoiceNode) should not be created if there is no configuration initially.
2017-08-16 10:40:56 +02:00
Bodnar Bogdan
add3baff74 Fix holding config variable in database through proxy objects
A config variable may not necessarily be None when not configured. Can be stored in the database with the help of proxy objects, which can  return the None value when accessed.
For example, to integrate with jazzband/django-constance.
2017-08-15 17:52:12 +02:00
Joost Cassee
e1d060398c Merge pull request #111 from bogdanbodnar/patch-1
Fix `CHARTBEAT_USER_ID` variable name in documentation.
2017-08-15 16:06:54 +02:00
Bodnar Bogdan
eaa9610cca Fix variable name 2017-08-15 15:54:20 +02:00
Martey Dodoo
8785a55a5a Fix "Version 2.2.2" heading in CHANGELOG. 2017-03-21 01:40:01 +01:00
Joost Cassee
0b38497524 Merge pull request #105 from Vitiell0/master
Updated regex for Intercom APP_ID and tests for Intercom
2017-02-22 15:11:11 +01:00
Daniel Vitiello
67ee8bdb50 Updated regex for Intercom APP_ID and tests for Intercom 2017-02-15 16:32:29 -06:00
Peter Bittner
7da975c08c Merge pull request #102 from bittner/feature/overhaul-docs-syntax-and-spacing
Overhaul reST syntax of documents in docs home
2017-01-29 22:41:10 +01:00
Peter Bittner
7d96f8e618 Use code-block directive for snippets
Correct spacing between sentences.

Update URLs (http -> https)
2017-01-29 21:55:27 +01:00
Joost Cassee
e8e2d84158 Merge pull request #101 from garrettr/fix-piwik-tracking-pixel-insecure-origin
Use protocol-relative URL for Piwik fallback img
2017-01-20 22:41:59 +01:00
Garrett Robinson
6f1db73026 Address review comments from @bittner
- Remove comments from Piwik tracking JS
- Remove unnecessary wrapping from lines in tests
2017-01-13 18:09:01 -08:00
Garrett Robinson
7ef1c9fd31 Update Piwik tracking code
Updates Piwik TRACKING_CODE to be based on the latest tracking code from
https://developer.piwik.org/guides/tracking-javascript-guide.
2017-01-13 12:48:09 -08:00
Garrett Robinson
6eeb809ea2 Fix unit tests 2017-01-11 20:22:34 -05:00
Garrett Robinson
212394a30d Use protocol-relative URL for Piwik fallback img
Avoids mixed content issues when using django-analytical on sites served
over HTTPS. Using a protocol-relative URL is also recommended in the
Piwik documentation: https://issues.piwik.org/344.

Fixes #96.
2017-01-11 19:45:15 -05:00
Joost Cassee
d9b482b8c7 Bump version to 2.2.2 and update changelog 2016-10-01 14:58:31 +02:00
Joost Cassee
5a82e9b446 Merge pull request #98 from ianalexr/master
Added optional port to Piwik domain path
2016-09-30 18:20:37 +02:00
Ian Ramsay
4221a1a3ed Added optional port to Piwik domain path 2016-09-30 10:02:57 -04:00
Joost Cassee
1cd95cd77b Update changelog and bump version to 2.2.1 2016-05-27 10:34:35 +02:00
Joost Cassee
9cf5c4995e Merge pull request #95 from saschwarz/gaflags_fix
GA flags with incorrect tags
2016-05-27 07:29:44 +02:00
Steve Schwarz
d579ec2be0 Corrected key name 2016-05-26 21:42:56 -05:00
Steve Schwarz
2f22762010 Merge remote-tracking branch 'jcassee/master' into gaflags_fix 2016-05-26 21:39:19 -05:00
Joost Cassee
f869323aeb Merge pull request #94 from sikmir/fix-doc
Escape the '@' symbol in doc
2016-05-26 09:40:53 +02:00
Nikolay Korotkiy
71e8538451 Escape the '@' symbol in doc 2016-05-26 02:42:19 +03:00
Joost Cassee
1cb458fe7c Fix links in Google Analytics documentation 2016-05-25 20:26:59 +02:00
Joost Cassee
eadd182fd0 Update changelog and bump version to 2.2.0 2016-05-25 20:22:15 +02:00
Joost Cassee
fb24d003f2 Merge pull request #92 from buildingenergy/master
Add woopra to window for SPA analytics
2016-05-25 20:16:07 +02:00
Aleck Landgraf
109de4c1d7 removes crud per PR comments 2016-05-25 10:43:11 -07:00
Joost Cassee
0da24961a6 Update changelog and bump version to 2.1.0 2016-05-25 10:44:18 +02:00
Joost Cassee
826725f2ce Fix Sphinx warning about code-block in install.rst 2016-05-25 10:43:57 +02:00
Joost Cassee
31f9dbf3ad Merge pull request #88 from sikmir/raiting-mailru
Add support for the Rating@Mail.ru service
2016-05-25 10:24:05 +02:00
Nikolay Korotkiy
8b232d06cb Add support for the Raiting@Mail.ru service 2016-05-25 11:05:01 +03:00
Joost Cassee
cb3accc26b Merge pull request #84 from sikmir/yandex-metrica
Support Yandex.Metrica
2016-05-25 09:58:22 +02:00
Peter Bittner
6de1b138f9 Merge pull request #93 from bittner/master
Remove Reinvigorate from README (slipped through PR #91)
2016-05-25 03:20:12 +02:00
Peter Bittner
3c118b2f37 Remove Reinvigorate from README (slipped through PR #91) 2016-05-25 03:18:33 +02:00
Aleck Landgraf
20e6a38163 updates to the latest woopra snippet which adds woopra to the window for tracking custom events, like SPA routing 2016-05-24 14:37:54 -07:00
Nikolay Korotkiy
fa034b0b10 Add support for the Yandex.Metrica service 2016-05-20 09:16:27 +03:00
Joost Cassee
b9953ba629 Merge pull request #89 from saschwarz/gaflags
Google Analytics _set*SampleRate and _set*CookieTimeout
2016-05-19 10:11:15 +02:00
Joost Cassee
4ceb9aa2f1 Merge pull request #91 from bittner/feature/remove-obsolete-reinvigorate-net
Remove Reinvigorate template tag, tests and docs
2016-05-19 10:09:30 +02:00
Joost Cassee
b2cf42d138 Merge pull request #90 from bittner/feature/fix-more-code-smells
Fix more code smells reported by Landscape
2016-05-19 10:08:59 +02:00
Steve Schwarz
7a6f24173b Added GA SAMPLE_RATEs and COOKIE_TIMEOUTs 2016-05-18 23:30:56 -05:00
Peter Bittner
bf96a8fe50 Fix some white space and highlighting in docs 2016-05-19 00:14:08 +02:00
Peter Bittner
849c551d55 Remove dead code (unsupported Python 2.6) 2016-05-19 00:04:15 +02:00
Peter Bittner
83312e01f9 Remove Reinvigorate template tag, tests and docs
Closes #82
2016-05-18 23:41:58 +02:00
Peter Bittner
0a4a9b7220 Fix W0622 (redefined-builtin), correct white space 2016-05-18 23:13:03 +02:00
Peter Bittner
66f4aea54a Fix W0612 (unused-variable) x3, correct white space 2016-05-18 23:12:29 +02:00
Joost Cassee
6f35fbb774 Merge pull request #80 from bittner/feature/fix-code-smells
Fix some code smells reported by Landscape
2016-05-18 16:29:17 +02:00
Peter Bittner
7847d22f95 [GoSquared] Remove settings override with no effect 2016-05-18 15:34:18 +02:00
Peter Bittner
709b8edcf4 [GoSquared] Test user is None in _identify 2016-05-18 14:58:41 +02:00
Peter Bittner
f832ec6179 Remove dead code (unsupported Python/Django versions) 2016-05-18 14:51:52 +02:00
Peter Bittner
14c1772641 [Woopra] Fix code smell "redefine built-in" and flake8 complaints 2016-05-18 14:10:25 +02:00
Peter Bittner
a1d3bbc09f [Spring Metrics] Fix code smell "redefine built-in" and flake8 complaints 2016-05-18 14:10:25 +02:00
Peter Bittner
9e6f7c8723 [Intercom] Fix code smell "redefine built-in" and flake8 complaints 2016-05-18 14:10:25 +02:00
Peter Bittner
56bcb46bb2 [GoSquared] Fix code smell "unused import" and flake8 complaints 2016-05-18 14:10:25 +02:00
Peter Bittner
54b7dfce2c [Google Analytics] Fix code smell "redefine built-in" and flake8 complaints 2016-05-18 14:10:25 +02:00
Peter Bittner
ea6b49f354 [Crazy Egg] Fix code smell "redefine built-in" and flake8 complaints 2016-05-18 14:10:25 +02:00
Joost Cassee
e5b0d6a262 Merge pull request #86 from bittner/master
Add "Python versions" badge (README)
2016-05-18 11:28:11 +02:00
Peter Bittner
f35511b33e Pin virtualenv version for Python 3.2 support 2016-05-15 18:18:05 +02:00
Peter Bittner
f04ba7d125 Pin pip version compatible with Python 3.2 (3rd try)
See problems in issue #84
2016-04-17 23:12:29 +02:00
Peter Bittner
7243f34243 Pin pip version compatible with Python 3.2 (2nd try)
See problems in issue #84
2016-04-17 19:10:50 +02:00
Peter Bittner
1bc53c3cc5 Pin pip version compatible with Python 3.2
See problems in issue #84
2016-04-17 18:46:01 +02:00
Peter Bittner
c05e294250 Add "Python versions" badge (README) 2016-04-17 11:51:27 +02:00
Joost Cassee
740cf0f11d Merge pull request #79 from bittner/feature/move-comment-on-versions-to-changelog
Move comment on Django support from README to history
2016-01-18 09:58:36 +01:00
Peter Bittner
5e9e205e78 Move comment on Django support from README to history 2016-01-17 23:41:56 +01:00
Joost Cassee
6e18be8771 Add note on obsolete Django versions and semver 2016-01-16 23:42:29 +01:00
Joost Cassee
50bebf3181 Update copyright dates and fix history include 2016-01-16 23:38:01 +01:00
Joost Cassee
3aba89dce8 Update documentation links to Django 1.9 2016-01-16 23:20:02 +01:00
Joost Cassee
586b164084 Bump version to 2.0.0 2016-01-16 23:10:53 +01:00
Joost Cassee
1eff2f4881 Merge pull request #78 from bittner/master
Add py32-django18 combination (tox)
2016-01-16 23:10:30 +01:00
Peter Bittner
25f45ec19a Add py32-django18 combination (tox) 2016-01-07 00:17:36 +01:00
Joost Cassee
735d86d16f Update trove classifiers in setup.py 2016-01-06 15:39:25 +01:00
Joost Cassee
de1d29f3c9 Fix missing max_length in test user identity field 2016-01-06 15:22:38 +01:00
Joost Cassee
f7a55746b4 Update changelog and contributors 2016-01-06 15:22:19 +01:00
Joost Cassee
9fe81545fd Fix typos in tox.ini 2016-01-06 15:17:41 +01:00
Joost Cassee
f84872a0ab Merge branch 'django19' of https://github.com/hobarrera/django-analytical
Conflicts:
	tox.ini
2016-01-06 14:58:43 +01:00
Joost Cassee
cf9c581de1 Merge pull request #75 from orcasgit/custom-username
support alternative username fields
2016-01-06 14:46:04 +01:00
Joost Cassee
7fcadd3000 Merge pull request #77 from bittner/master
Allow local host names for Piwik servers
2016-01-06 14:17:45 +01:00
Peter Bittner
2510333c5d Allow local host names for Piwik servers
Closes #76
2016-01-04 17:49:48 +01:00
Hugo Osvaldo Barrera
d067769947 Sort entries in autogenerated travis configuration 2015-12-21 14:26:40 -03:00
Hugo Osvaldo Barrera
21328da7ba Mention supported django versions in README and docs 2015-12-21 14:26:40 -03:00
Hugo Osvaldo Barrera
330949ecb7 Fix python35 failures on travis 2015-12-21 14:26:39 -03:00
Hugo Osvaldo Barrera
9144118f80 Add missing apps to chartbeat tests settings 2015-12-21 14:26:39 -03:00
Hugo Osvaldo Barrera
f8b820c1db Only import django.contrib.site if it's in use
Since django1.7, importing models for uninitialized apps is not
supported, so Site if the app has been configured/initialized.
2015-12-21 14:26:39 -03:00
Hugo Osvaldo Barrera
d7fcdb0346 Make mixpanel dj19-compatible
Looks like django>=1.9 uses `token` somewhere for simpletags, so use
`_token` for our own to avoid it getting overwritten.
2015-12-21 14:26:39 -03:00
Hugo Osvaldo Barrera
d42e6d348b Escape performable tags
See this entry[1] in the django 1.9 release notes for details.

[1]: https://docs.djangoproject.com/en/1.9/releases/1.9/#simple-tag-now-wraps-tag-output-in-conditional-escape
2015-12-21 14:26:39 -03:00
Hugo Osvaldo Barrera
b833f37f95 Tests migrate instead of syncdb
The latter is obsolete and have been replaced by the former.
2015-12-21 14:26:39 -03:00
Hugo Osvaldo Barrera
87be940822 Drop support for unsupported django versions
Since django 1.8 is an LTS release, we're dropping support for older
versions.

Since django 1.7 is still relatively recent, we'll be keeping support
for it for now.
2015-12-21 14:26:39 -03:00
Hugo Osvaldo Barrera
e2f25d43cd Fix crash on setup.py when NOT running tests 2015-12-21 14:26:34 -03:00
Brad Pitcher
5af731ab9d support custom username fields 2015-12-20 09:40:08 -08:00
Joost Cassee
25236d524a Merge pull request #74 from orcasgit/empty-override
override internal ips with empty list
2015-12-18 11:59:39 +01:00
Brad Pitcher
4273525668 override internal ips with empty list 2015-12-17 13:48:24 -08:00
Hugo Osvaldo Barrera
0725045330 Ignore vim swap files 2015-12-06 11:37:11 -03:00
Hugo Osvaldo Barrera
29a18306bb Configure django before using it 2015-12-06 11:37:11 -03:00
Hugo Osvaldo Barrera
7f039cce3f Remove bogus import 2015-12-06 11:37:11 -03:00
Hugo Osvaldo Barrera
5a336088f6 List missing dependencies 2015-12-06 11:37:11 -03:00
Hugo Osvaldo Barrera
8742050a0c Eliminate race condition during test startup 2015-12-06 11:37:11 -03:00
Hugo Osvaldo Barrera
8a16cea356 Don't fail tests if coverall fails 2015-12-06 11:37:11 -03:00
Hugo Osvaldo Barrera
bdd1a171f2 Add django1.9 to the test matrix 2015-12-06 11:37:04 -03:00
Hugo Osvaldo Barrera
ecb4da9ffb Remove redundant basepython declarations 2015-12-06 11:34:54 -03:00
Joost Cassee
032a157c6f Merge pull request #71 from JamesPaden/master
Fix link to Gaug.es in README
2015-11-05 21:47:38 +01:00
James Paden
f6ac6d61ee Fix link to Gaug.es in README 2015-11-05 14:27:20 -05:00
James Paden
0182cc7abb Fix link to Gaug.es in README 2015-11-05 14:26:41 -05:00
Joost Cassee
2625271654 Merge pull request #70 from bittner/master
Also test Python 3.2 with tox
2015-09-27 07:53:05 +02:00
Peter Bittner
8a95cd0acf Add Python 3.2 to setup infos (PyPI classifiers)
Update URLs (http -> https, old download URL obsolete)
2015-09-27 00:06:45 +02:00
Peter Bittner
1909663966 Avoid u-prefix for unicode strings (Python 3.2 compatibility) 2015-09-26 23:47:05 +02:00
Peter Bittner
2a5015fed2 Pin coverage version (workaround for Python 3.2 test execution on Travis-CI)
see https://bitbucket.org/ned/coveragepy/issues/407/coverage-failing-on-python-325-using#comment-22049180
2015-09-26 11:10:37 +02:00
Peter Bittner
d8c5852abb Also test Python 3.2 with tox
Match python/django test combinations with Pip packages on PyPI

see https://pypi.python.org/pypi/Django/1.x (with x in 4..8)
2015-09-25 17:33:54 +02:00
Joost Cassee
aabe85b925 Merge pull request #69 from bittner/master
Add Gitter badge, update some URLs
2015-09-25 09:40:46 +02:00
Peter Bittner
6411469886 Add Gitter badge, update some URLs 2015-09-24 22:07:37 +02:00
Joost Cassee
0889583e81 Merge pull request #66 from ericdwang/master
Update locations of constants in Google Analytics documentation
2015-09-16 09:54:19 +02:00
Joost Cassee
6ccc5327f3 Merge pull request #65 from bittner/feature/remove-django19-deprecation-warning
Fix RemovedInDjango19Warning (django.utils.importlib)
2015-09-16 09:49:32 +02:00
Joost Cassee
c6de68c037 Merge pull request #64 from bittner/feature/flake8-blank-lines
Fix flake8 complaints (whitespace), add another badge
2015-09-16 09:47:44 +02:00
Eric Wang
b30503be93 Update locations of constants in Google Analytics documentation 2015-09-12 17:12:54 -07:00
Peter Bittner
3e93cc5c4b Add code health badge (Landscape.io) 2015-09-08 22:43:02 +02:00
Peter Bittner
e2ad1ceced Fix flake8 complaints (E225, E261, E302, W291, W292, W293) 2015-09-08 22:35:06 +02:00
Peter Bittner
87bb09c66e Fix RemovedInDjango19Warning (django.utils.importlib) 2015-09-08 18:46:35 +02:00
Joost Cassee
cfdc330d25 Update development status classifier in setup.py 2015-08-24 20:29:20 +02:00
Joost Cassee
11507a9a3f Bump version and update changelog 2015-08-24 15:34:01 +02:00
Peter Bittner
da1dd33e0e Merge pull request #63 from apocquet/master
Add Piwik user variables support and identity tracking
2015-08-24 11:48:50 +02:00
Alexandre Pocquet
a6c91d19d4 Reformat Piwik docstring 2015-08-24 11:32:54 +02:00
Alexandre Pocquet
903ad4eb04 Update code style 2015-08-21 11:05:59 +02:00
Alexandre Pocquet
f1fe8ca740 Update Piwik documentation 2015-08-21 11:04:55 +02:00
Alexandre Pocquet
f27d946ccf Add support for Piwik user identity tracking
The 'piwik_identity' (or the default 'analytical_identity') context variables are used to call Piwik's 'setUserId' function.
If neither are found, use what is returned by get_identity.
2015-08-19 16:55:32 +02:00
Alexandre Pocquet
8af3181752 Fix rendered javascript indentation 2015-08-19 15:25:51 +02:00
Alexandre Pocquet
89f51ab594 Add Piwik user variables support 2015-08-19 15:06:46 +02:00
Joost Cassee
d84dc23bd4 Merge pull request #62 from pjdelport/patch-1
Update link to Google's Site Speed documentation
2015-07-01 07:27:23 +02:00
Piet Delport
56705cc3e6 Update link to Google's Site Speed documentation
The old link just 404s at the moment.
2015-07-01 07:12:16 +02:00
Peter Bittner
15a5b1881d Merge pull request #61 from jcassee/feature/coverage
Calculate coverage and send to Coveralls.io
2015-05-18 22:24:48 +02:00
Joost Cassee
fff72f7598 Add coverage badge to README 2015-05-18 11:45:47 +02:00
Joost Cassee
a9c9598e55 Send coverage info to Coveralls.io on Travis 2015-05-18 11:33:53 +02:00
Joost Cassee
bde02a3adf Fix docs include directive 2015-05-17 15:20:47 +02:00
Joost Cassee
c6f24decb5 Add changelog entry for last commit, bump version 2015-05-17 15:13:51 +02:00
Peter Bittner
3f22d2881f Merge pull request #60 from mgaitan/update_setup
mark the package python 3 compatible
2015-05-15 23:14:48 +02:00
Martín Gaitán
8838b97167 mark the package python 3 compatible 2015-05-15 11:11:46 -03:00
Joost Cassee
f391a1d27c Merge pull request #58 from jcassee/remove-wrong-branch-name-from-build-status-badge
Remove wrong branch name from Build Status badge
2015-04-27 22:00:39 +02:00
Joost Cassee
8fd4361bfe Merge pull request #57 from jcassee/flake8-code-not-tests
Fix flake8 complaints (except for templatetags and tests)
2015-04-27 22:00:18 +02:00
Peter Bittner
b002debe67 Remove wrong branch name from Build Status badge
* Travis/Github sometimes yields a broken badge image with a query parameter (should now be better)
* The image's ALT text was "develop", which is wrong of course
2015-04-25 02:20:22 +02:00
Peter Bittner
88a7cb625f Fix line-too-long complaints (flake8) 2015-04-25 02:00:39 +02:00
Peter Bittner
99ce2f5fcb Merge branch 'master' into flake8-code-not-tests 2015-04-25 01:49:55 +02:00
Joost Cassee
9792c6c531 Merge pull request #56 from jcassee/beautiful-badges
Add beautiful badges to README
2015-04-23 10:58:01 +02:00
Peter Bittner
7c02c9364c Fix typo 2015-04-23 01:38:00 +02:00
Peter Bittner
b968f12c4e Add beautiful badges to README
Clickable badges:

* current PyPI package version number
* Travis-CI build status
* downloads per month from PyPI
* license info
2015-04-23 01:33:41 +02:00
Joost Cassee
4fc1f74319 Update changelog with Django 1.8 compatibility 2015-04-21 01:15:06 +02:00
Joost Cassee
f2f0f34076 Merge pull request #55 from jcassee/test-with-django-18-too
Also test with Django 1.8 (Travis-CI)
2015-04-21 01:10:56 +02:00
Peter Bittner
001d44b10f Make Travis-CI use Tox for test matrix execution
see http://jsatt.com/blog/using-tox-with-travis-ci-to-test-django-apps/
2015-04-21 00:20:02 +02:00
Peter Bittner
8103e5d94a Refactor tox.ini with conditional settings
see https://testrun.org/tox/latest/config.html#generating-environments-conditional-settings

Also test Django 1.8 with tox
2015-04-21 00:00:12 +02:00
Peter Bittner
58d83fe5aa Indentation corrections (PEP8)
Correct all flake8 complaints except: in tests, in templatetags, E501 line too long
2015-04-20 23:08:19 +02:00
Peter Bittner
fa3cdf108c Also test with Django 1.8 (Travis-CI)
Plus white-space changes to match typically used YAML syntax (indentation of lists).

This commit may close issue #54 if all goes well.
2015-04-20 22:36:59 +02:00
Joost Cassee
e46804810f Merge pull request #53 from jcassee/update-readme-co-maintainer
Update README (co-maintainer, headers)
2015-04-20 14:58:46 +02:00
Peter Bittner
79051b28a7 Move link to correct section 2015-04-20 14:42:23 +02:00
Peter Bittner
d99146da0b Remove co-maintainer note, add headers 2015-04-20 14:38:37 +02:00
Joost Cassee
e44141be80 Update Clickmap id regular expression 2015-04-19 11:08:26 +02:00
Joost Cassee
72b17165cb Update intersphinx mapping to Django 1.8 2015-04-19 10:51:20 +02:00
Joost Cassee
adb184adfd Update changelog and bump version 2015-04-19 10:49:25 +02:00
Joost Cassee
aab1cfc508 Merge pull request #52 from amadornimbis/master
Adding python 3 compatibility
2015-04-19 10:36:06 +02:00
Eric Amador
e5f88b543f django 1.4 doesn't have python 3.x support. 2015-04-18 01:13:36 -04:00
Eric Amador
5855a1693a django 1.4 doesn't have python 3.x support. 2015-04-18 01:11:43 -04:00
Eric Amador
ff25a20ab7 Adding python 3.3 and 3.4 to Travis. 2015-04-18 00:58:42 -04:00
Eric Amador
67f29fdb2b Use assertRaisesRegex when available. 2015-04-18 00:42:54 -04:00
Eric Amador
dcf51bc6ca assertEqual was deprecated in python 3.2 2015-04-18 00:33:56 -04:00
Eric Amador
dc031d9e01 Make exception handling python 3 compatible. 2015-04-18 00:22:38 -04:00
Eric Amador
b8aa694d07 Updating tox file for python 3.3 and 3.4. Removing unused targets. 2015-04-18 00:22:11 -04:00
Joost Cassee
871a6efd1b Add note about looking for a maintainer 2015-03-09 20:51:42 +01:00
Joost Cassee
d1b28ef809 Update intersphinx links to Django 1.7 2015-03-09 20:51:28 +01:00
Joost Cassee
31b06af0ce Update changelog, authors and version 2015-03-09 20:51:14 +01:00
Joost Cassee
c5c886d5bf Fix up docs style 2015-03-09 20:50:54 +01:00
Joost Cassee
54d922496d Merge pull request #50 from mgaitan/mixpanel_identify
update mixpanel indetify code.
2015-03-09 20:36:13 +01:00
Joost Cassee
fe608af67c Merge pull request #49 from craigbruce/master
Add Django 1.7 support
2015-03-09 20:34:09 +01:00
Joost Cassee
abb8e4cbbf Merge pull request #48 from mgaitan/uservoice_identify_users
Identifying authenticated users in Uservoice
2015-03-09 20:32:45 +01:00
Joost Cassee
447ea49f98 Merge pull request #45 from 7wonders/master
Fixes #44
2015-03-09 20:32:12 +01:00
Joost Cassee
6f65f836d0 Merge pull request #43 from bittner/master
Corrected verb, added link to PRs
2015-03-09 20:31:41 +01:00
Martín Gaitán
e1bfe4c40a send user properties 2014-09-19 13:22:50 -03:00
Martín Gaitán
b8f1203bca update test 2014-09-09 09:02:42 -03:00
Martín Gaitán
6992f170de update test 2014-09-09 09:01:43 -03:00
Martín Gaitán
3f31081113 update mixpanel indetify code. See https://mixpanel.com/help/reference/javascript-full-api-reference#mixpanel.identify for reference 2014-09-09 08:58:56 -03:00
Craig Bruce
51a08742c5 removed custom override_settings, using django instead. Removed with/without app decorators as only use in a few places. Updated all tests to use django provided override_settings. 2014-09-05 12:26:08 -07:00
Craig Bruce
3e417d0357 added Django 1.7 to tox and travis 2014-09-05 11:42:31 -07:00
Craig Bruce
c929410aef incorrect variable names/syntax (this should not of worked) 2014-09-05 11:40:00 -07:00
Craig Bruce
38b9cc70e6 fixed various typos, this would of failed previously 2014-09-05 11:23:20 -07:00
Craig Bruce
7ccd84d1d3 added default middleware for django 1.7 2014-09-05 11:22:25 -07:00
Craig Bruce
33af2c6184 minor pep8 fix 2014-09-05 10:32:10 -07:00
Craig Bruce
186587a506 added py2.6/2.7 classifiers as travis-ci tests these versions 2014-09-05 09:49:11 -07:00
Martín Gaitán
3c1206db11 Identifying users 2014-08-25 22:33:53 -03:00
Scott Adams
228244cf94 Account for possible foreign characters in name 2014-08-13 18:27:17 +02:00
Scott Adams
b383b178bf Fixes #44
By passing olark_fullname and/or olark_email contexts it will pre-fill the olark full name and email fields.
2014-08-13 11:59:06 +02:00
Peter Bittner
940a4315b3 Corrected grammar, added link to PRs 2014-07-11 09:08:11 +02:00
Joost Cassee
cf80f86b18 Merge pull request #41 from bittner/master
Fixed grammar in Piwik documentation
2014-07-07 14:53:35 +02:00
Peter Bittner
969fbea602 Fixed grammar in Piwik documentation 2014-07-07 09:01:09 +02:00
Joost Cassee
7ac9b38ee5 Small fixes after Piwik merge
- Update changelog and add Peter Bittner to authors
- Fix documentation typo
- Bump version
2014-07-06 21:21:49 +02:00
Joost Cassee
bf0da79846 Merge pull request #40 from bittner/master
Added Piwik open source web analytics
2014-07-06 21:14:28 +02:00
Peter Bittner
6cf24c4709 Added tests for Piwik 2014-07-05 00:32:59 +02:00
Peter Bittner
e49da10e01 Added Piwik open source web analytics 2014-07-05 00:02:16 +02:00
Peter Bittner
60ba045b75 Ignore PyCharm and Geany IDE project files 2014-07-04 09:11:20 +02:00
Joost Cassee
c28f509ff9 Add Django dependency in setup.py 2014-06-01 22:46:48 +02:00
Joost Cassee
8770c37b33 Bump version to 0.18.0 and credit Craig Bruce 2014-06-01 22:45:37 +02:00
Joost Cassee
d21847d1e2 Merge pull request #39 from craigbruce/master
Updated HubSpot tracking code. Updated test/docs/changelog to reflect th...
2014-06-01 22:36:38 +02:00
Craig Bruce
1ebe4f676b Updated HubSpot tracking code. Updated test/docs/changelog to reflect this. HUBSPOT_DOMAIN is no longer required. 2014-05-26 16:57:49 -06:00
Joost Cassee
8561069582 Update changelog 2014-05-01 23:28:57 +02:00
Joost Cassee
6105127d93 Remove Tox from Travis-CI test setup 2014-05-01 23:21:27 +02:00
Joost Cassee
60c2d03c50 Merge pull request #37 from greenkahuna/master
Fixes default name key for intercom.
2014-05-01 17:01:07 +02:00
skoczen
dc13c2a89f Version bump. 2014-04-30 16:29:42 -07:00
skoczen
f24f03c3f8 Fixes full_name -> name.
That's embarrassing.
2014-04-30 16:27:57 -07:00
Joost Cassee
170f8babad Fix Travis and comment out failing test
Django 1.6 does not allow dynamic changes to the install apps list
anymore, at least not in the same way as 1.5 did. No time to fix the
test, so cowardly commenting it out.
2014-04-29 01:45:47 +02:00
Joost Cassee
af4f5ab675 Fix Tox configuration in .travis.yml 2014-04-29 01:27:38 +02:00
Joost Cassee
12a24c0d17 Small fixes to tox and docs
- Remove testing environments for Django 1.2 and 1.3
- Update Django Sphinx links to 1.6
- Fix docs warning in intercom.rst
2014-04-29 01:21:40 +02:00
Joost Cassee
f70b3e6132 Fix typo in tox.ini 2014-04-29 01:17:41 +02:00
Joost Cassee
54a4c67a07 Add Django 1.6 to Tox environments 2014-04-29 01:14:04 +02:00
Joost Cassee
cef2956edb Merge branch 'master' of github.com:greenkahuna/django-analytical into greenkahuna-master 2014-04-29 01:05:22 +02:00
skoczen
7751777b4e Fixes up extra slashes. 2014-04-28 15:54:39 -07:00
skoczen
559cd3677f sort_keys all the things! 2014-04-28 15:52:36 -07:00
Joost Cassee
0ba4bc4456 Remove reference to simplejson 2014-04-28 23:19:44 +02:00
Joost Cassee
da01567365 Add Martín Gaitán to authors list 2014-04-28 22:58:04 +02:00
Joost Cassee
2b323bdb72 Merge branch 'uservoice' of github.com:mgaitan/django-analytical into mgaitan-uservoice 2014-04-28 22:53:26 +02:00
skoczen
a94f0d8bb0 Adds support for custom variables, in a similar pattern to woopra's tag. 2014-04-28 10:10:23 -07:00
skoczen
6ebdb3de98 Properly moves to body_bottom. 2014-04-25 10:48:18 -07:00
skoczen
d76a0981b8 Adds intercom.io support, fixes inconsent fails from json ordering. 2014-04-25 10:46:30 -07:00
Joost Cassee
6763d3e06b Merge pull request #33 from craigbruce/master
Replaced simplejson with json
2013-12-20 01:54:41 -08:00
Craig Bruce
90ff353de2 replaced simplejson with json 2013-12-19 22:00:28 -07:00
Martín Gaitán
ba2c13c57c assertIn fallback for py2.6/dj1.2 2013-11-26 20:34:33 -03:00
Martín Gaitán
064dd5b681 empty key returns no code 2013-11-26 20:18:48 -03:00
Martín Gaitán
010612b115 update tests 2013-11-26 14:49:45 -03:00
Martín Gaitán
afcda9e8bd updating tests 2013-11-26 14:34:31 -03:00
Martín Gaitán
b83f1b5c1a doc typo 2013-11-26 14:16:47 -03:00
Martín Gaitán
e781f3ed93 trigger custom 2013-11-26 14:15:29 -03:00
Martín Gaitán
e1983ad8a9 flatten confiuration. update doc 2013-11-26 13:55:46 -03:00
Martín Gaitán
d24802b358 update docs 2013-11-26 12:44:05 -03:00
Martín Gaitán
63020a2062 Updated uservoice service tracking code.
Now all custom options are in `USERVOICE_WIDGET_OPTIONS`. This could be overriden per-view
with a `uservoice_widget_option` context variable.

For example, to override the default trigger position you could set:

	USERVOICE_WIDGET_OPTIONS = (('addTrigger',
					{"trigger_position": "bottom-left",
                                         "mode": "contact"}),)

`USERVOICE_WIDGET_OPTIONS` accepts any Uservoice's JS SDK configuration
See https://developer.uservoice.com/docs/widgets/overview/
2013-11-26 12:18:12 -03:00
Joost Cassee
be591d66f1 Create Travis-CI configuration 2013-11-21 15:12:20 +01:00
Joost Cassee
02d5664705 Fix Max Arnold's name in changelog 2013-11-21 14:10:34 +01:00
Joost Cassee
6343b103f3 Bump version and update changelog 2013-11-19 07:44:55 +01:00
Joost Cassee
1b647249e6 Merge pull request #31 from max-arnold/master
Add support for GA Display Advertising features
2013-11-18 22:42:06 -08:00
Max Arnold
2a13008ffc improve variable names and snippet readability 2013-11-19 10:07:55 +07:00
Max Arnold
440d078d86 add support for GA Display Advertising features
https://support.google.com/analytics/answer/3450482

The Google Analytics Display Advertising features include the following:
- Demographics and Interests reporting
- Remarketing with Google Analytics
- DoubleClick Campaign Manager integration (for Google Analytics Premium)
2013-10-26 10:17:38 +07:00
Joost Cassee
11719a99f6 Fix Reinvigorate link
reinvigorate.com -> reinvigorate.net
2013-07-17 23:21:33 +02:00
Joost Cassee
8e3c6ef34d Update Sphinx links to Django 1.5 and Python 2.7 2013-04-11 04:07:17 +02:00
Joost Cassee
b5b4716db8 Update Clickmap integration and changelog 2013-04-11 04:02:50 +02:00
Joost Cassee
5dacedf9a4 Merge https://github.com/arteria/django-analytical 2013-04-11 03:51:48 +02:00
Joost Cassee
9fb770953d Merge pull request #25 from sidmitra/master
Upgrading mixpanel js script code to v2.1
2013-04-10 18:37:38 -07:00
Walter Renner
a631acb591 Added tests for Clickmap integration 2013-04-05 14:23:34 +02:00
Walter Renner
fe12e97fba Added documentation for Clickmap integration 2013-04-05 13:55:00 +02:00
Sid Mitra
480b92c326 Upgrading mixpanel js script code to v2.2 2013-04-05 01:30:34 +05:30
Joost Cassee
e1242f37d1 Update changelog 2013-03-25 01:12:23 +01:00
Joost Cassee
c2f9713a6c Bump version 2013-03-25 01:10:26 +01:00
Joost Cassee
341b57f683 Merge branch 'master' of github.com:jcassee/django-analytical 2013-03-25 01:10:15 +01:00
Joost Cassee
7870252ba2 Merge pull request #23 from tinnet/feature/django1.5-testing-in-tox
Add django 1.5 to tox
2013-03-24 17:09:37 -07:00
Joost Cassee
f8cf9d39a7 Update version and changelog 2013-03-25 00:58:14 +01:00
Joost Cassee
782c8d90cf Merge branch 'master' of github.com:jcassee/django-analytical 2013-03-25 00:50:15 +01:00
Joost Cassee
cb71b7ca0a Merge pull request #22 from tinnet/feature/ga-anonymize-ip
Add 'IP Anonymization' setting to GA tracking pixel
2013-03-24 16:34:18 -07:00
Joost Cassee
a57fd27164 Add reminder to UserVoice docs 2013-03-25 00:25:17 +01:00
Tinnet Coronam
f11a35e4ed use final django 1.5 2013-03-08 16:32:52 +01:00
Tinnet Coronam
a88d7272a6 added documentation for ANONYMIZE_IP 2013-03-08 16:04:12 +01:00
Tinnet Coronam
f4f99f6d6d add test for ip anonymization 2013-01-14 09:25:31 +01:00
Tinnet Coronam
2fd6dc1f77 add ip anonymization setting to google analytics 2013-01-14 09:18:27 +01:00
Tinnet Coronam
92d6183481 added django 1.5RC testing to tox 2013-01-11 16:16:48 +01:00
Philippe O. Wagner
b18c659206 Initial integration of Clickmap into django-analytical. Basically working but tests and documentation is missing currently. 2012-12-05 08:22:17 +01:00
Joost Cassee
0754b5fd47 Add note on identifying users in installation docs 2012-10-18 09:39:31 +02:00
Joost Cassee
9bef77b3b4 Merge pull request #21 from sidmitra/master
Upgraded mixpanel js code to v2.1
2012-10-04 01:59:36 -07:00
Sid Mitra
2d5f8dd220 Upgrading mixpanel js script code to v2.1 2012-10-04 09:36:09 +05:30
Joost Cassee
a663463198 Update changelog and bump version 2012-08-19 22:11:27 +02:00
Simon Ye
8cdd68011e Update mixpanel to latest code. 2012-08-17 19:09:20 -07:00
Joost Cassee
d89b9f56af Bump version 2012-08-14 08:51:13 +02:00
Joost Cassee
60f32daf07 Update Sphinx link to Django docs to 1.4 2012-08-14 08:48:21 +02:00
Joost Cassee
652a084532 Update changelog 2012-08-14 08:46:35 +02:00
Joost Cassee
3eb0ba07f9 Merge https://github.com/xthepoet/django-analytical 2012-08-14 08:30:57 +02:00
xthepoet
9031022553 Update docs/services/kiss_metrics.rst 2012-08-13 08:21:46 -07:00
xthepoet
d12bb6de2d Update docs/services/kiss_metrics.rst 2012-08-13 08:17:56 -07:00
Joost Cassee
f96df3d8c6 Update changelog 2012-08-13 14:43:47 +02:00
Joost Cassee
5a906c3cfc Merge pull request #18 from pjdelport/django-1.4
Django 1.4 updates
2012-08-13 05:39:04 -07:00
Piet Delport
1c9e478cc9 Add SECRET_KEY to the test settings.
Running without a SECRET_KEY becomes a DeprecationWarning in Django 1.4,
and will start raising an ImproperlyConfigured error in Django 1.5.

Details: https://docs.djangoproject.com/en/1.4/releases/1.4/#secret-key-setting-is-required
2012-08-13 13:09:35 +02:00
Joost Cassee
2786f28664 Merge pull request #17 from pjdelport/master
Minor doc fix: django-analytics -> django-analytical
2012-08-13 03:34:17 -07:00
Piet Delport
9b83ef1c3e Fix name: django-analytics -> django-analytical 2012-08-13 12:30:47 +02:00
Piet Delport
5a1cceced4 Tox: Update for Django 1.4. 2012-08-13 12:26:17 +02:00
Joost Cassee
2e872396b9 Merge pull request #16 from pjdelport/master
Fix typo.
2012-08-02 03:26:21 -07:00
Piet Delport
d38d30c846 Fix typo. 2012-08-02 12:14:21 +02:00
xthepoet
255dd4f387 added alias tag for kissmetrics 2012-07-16 01:12:52 -04:00
Joost Cassee
361c2e861f Fix mixpanel-celery reference typo 2012-06-03 21:18:00 +02:00
Joost Cassee
7b2c45d9e9 Fix Gaug.es link in README 2012-02-27 01:41:26 +01:00
Joost Cassee
2285a0ab76 Fix several small UserVoice bugs 2012-02-27 01:32:42 +01:00
Joost Cassee
fb98371a01 Add UserVoice service 2012-02-27 01:32:42 +01:00
Joost Cassee
8e115b3c65 Add UserVoice service documentation 2012-02-27 01:30:51 +01:00
Joost Cassee
3c4f165eb7 Credit Steven Skoczen with Gaug.es support 2012-02-25 23:09:02 +01:00
Joost Cassee
3f345342a3 Merge pull request #15 from skoczen/master
Added support for gaug.es

Contributed by Steven Skoczen
2012-02-25 13:56:27 -08:00
Steven Skoczen
10c48b645f added support for gaug.es 2012-02-24 15:14:17 -08:00
Joost Cassee
a00d36d29e Bump version, update changelog 2012-02-08 00:27:07 +01:00
Joost Cassee
0e8b4b244a Add tests for anonymous user 2012-02-08 00:26:51 +01:00
Joost Cassee
3503561666 Fix Spring Metrics custom variables 2012-02-07 23:50:54 +01:00
Joost Cassee
aa00d33aec Add tests for Woopra anonymous user (and fix bug) 2012-02-01 00:16:51 +01:00
Joost Cassee
90dda41bfa Bump version and update changelog 2012-02-01 00:10:25 +01:00
Joost Cassee
7836e83c17 Merge pull request #14 from skoczen/master
Fix of woopra for anonymous users

Patch by Steven Skoczen.
2012-01-31 15:08:04 -08:00
Steven Skoczen
3f4c9d8e9b Updated fix to be less redundant 2012-01-31 15:04:33 -08:00
Steven Skoczen
e5cba81f07 Fixed woopra for non-logged in users. 2012-01-31 14:17:16 -08:00
Joost Cassee
31ffca4c2f Merge branch 'master' of github.com:jcassee/django-analytical 2011-10-30 17:23:14 +01:00
Joost Cassee
128014db99 Add support for the Spring Metrics service 2011-10-30 17:22:02 +01:00
Joost Cassee
ffde47442d Fix typo in Clicky docs 2011-10-30 16:39:33 +01:00
Joost Cassee
9e9aa950e8 Update changelog and add Uros Trebec to authors 2011-10-06 18:30:42 +02:00
Joost Cassee
9d701f5d34 Cosmetic changes to site speed tracking 2011-10-06 18:29:52 +02:00
Joost Cassee
d78f5f7264 Improve Google Analytics variable test 2011-10-06 18:15:31 +02:00
Joost Cassee
3fa241f3bb Merge pull request #12 from failedguidedog/feat/ga_page_load_time
Add Google Analytics '_trackPageLoadTime' feature
2011-10-06 09:11:44 -07:00
Uros Trebec
fa0e578fdd Added a default setting "PAGE_LOAD_TIME = False" in google_analytics.py templatetags 2011-10-03 20:04:34 +02:00
Uros Trebec
a4b076f9e8 Added a 'test_track_page_load_time' to test '_trackPageLoadTime' enabled by GOOGLE_ANALYTICS_PAGE_LOAD_TIME 2011-10-03 20:04:34 +02:00
Uros Trebec
1eab45d6b2 Added a 'GOOGLE_ANALYTICS_PAGE_LOAD_TIME' option to insert '_trackPageLoadTime' in google_analytics.py templatetag 2011-10-03 20:03:18 +02:00
Joost Cassee
95f0307787 Update changelog and bump version 2011-09-19 22:36:47 +02:00
Joost Cassee
b8ffab47f1 Merge pull request #9 from poswald/master
Allow sending events and properties to KISSmetrics
2011-09-19 13:34:43 -07:00
Paul Oswald
ebfebef65d Rename context var to 'kiss_metrics_properties'; add link to KISSmetrics docs. 2011-09-19 10:04:50 +09:00
Paul Oswald
56653912f1 Add a unit test for KISSmetrics properties 2011-09-18 11:48:36 +09:00
Joost Cassee
bb3b75a1ee Bump version to 0.10.0 2011-09-18 00:36:20 +02:00
Joost Cassee
b9c2b88d22 Merge remote-tracking branch 'edavis/setting-deleted' 2011-09-18 00:12:30 +02:00
Joost Cassee
1ae5d5c4d7 Add multiple domains support for Google Analytics 2011-09-17 23:13:25 +02:00
Eric Davis
264f984421 Clarify where assertRaisesRegexp comes from 2011-09-17 12:29:15 -07:00
Eric Davis
da5f722424 Add tox.ini 2011-09-17 11:27:02 -07:00
Eric Davis
75443cd014 Properly set the exit code when running tests
run_tests returns the number of failed tests.  Supplying this number
to sys.exit lets other programs know if there were any test failures.

Also, run the tests with the default verbosity.
2011-09-17 11:26:19 -07:00
Eric Davis
c88fbd6c75 Make SettingDeleted tests compatible with Python 2.6 and Django 1.2
Using assertRaises as a context manager only became possible in Python
2.7 or when using Django 1.3 (thanks to unittest2).  This change lets
the tests pass when using Python 2.6 and Django 1.2.

Also, test for the existence of assertRaisesRegexp and use it if it
exists, otherwise use assertRaises.
2011-09-17 11:13:15 -07:00
Paul Oswald
f85b62758d Allow setting properties in context; Document properties and events. 2011-09-18 01:07:03 +09:00
Eric Davis
39af53a8c8 Fix recursion error when copying settings._wrapped
https://github.com/jcassee/django-analytical/issues/8 for more information
2011-09-16 16:26:40 -07:00
Eric Davis
96ee34e671 Use assertRaisesRegexp for get_required_setting test 2011-09-16 16:26:40 -07:00
Joost Cassee
f24998ef2d Update changelog 2011-09-16 21:23:28 +02:00
Joost Cassee
be25e2cfd4 Merge remote-tracking branch 'edavis/setting-deleted' 2011-09-16 21:21:19 +02:00
Eric Davis
e8ce3be1b0 Add some tests for SETTING_DELETED 2011-09-12 09:25:46 -07:00
Eric Davis
ac304371f5 Better handle deleting non-existent attributes 2011-09-12 09:17:48 -07:00
Eric Davis
d217672224 Copy the wrapped settings object when creating UserSettingsHolder
This is needed otherwise deleted settings don't get added back for
later tests
2011-09-12 09:16:15 -07:00
Eric Davis
c1926dc77e Clean up docstring for override_settings 2011-09-11 22:34:24 -07:00
Eric Davis
2a1e2d3f68 Delete settings if set to SETTING_DELETED
Prior to this, SETTING_DELETED was successfully returning values but
was raising AnalyticalException because it failed a subsequent regexp
check.

With this, delete the attribute so that settings.KEY raises an
AttributeError, as expected.
2011-09-11 22:31:43 -07:00
Eric Davis
948c6c7b83 Display original class name when using override_settings 2011-09-10 22:17:58 -07:00
Joost Cassee
b6b4a82cac Fix changelog typo 2011-08-27 08:18:19 +02:00
Joost Cassee
56e3d85e15 Bump version in changelog 2011-08-27 08:12:43 +02:00
Joost Cassee
8784df4524 Add support for the SnapEngage service 2011-08-22 23:21:57 +02:00
Joost Cassee
499a9c3cd8 Update changelog and bump version 2011-08-19 07:59:02 +02:00
Julien Grenier
e2e01490dc This is the updated code from the mixpanel API Integration code. (see http://mixpanel.com/api/docs/guides/integration/js) 2011-08-16 14:07:08 -03:00
Joost Cassee
f6a634a64e Small changes to enumerate compatibility
Also update the changelog and version number.
2011-07-21 07:17:01 +02:00
Joost Cassee
861e3b7947 Merge branch 'master' of github.com:jcassee/django-analytical 2011-07-21 07:09:50 +02:00
Joost Cassee
1160a10a3d Add 'and others' to copyright 2011-07-21 07:09:37 +02:00
Joost Cassee
a24682aff3 Merge pull request #5 from rasca/master
Add enumerate function for Python 2.5 compatibility
2011-07-20 22:02:35 -07:00
Iván Raskovsky
a0c6d992ef added python 2.5 compatibility to google_analytics 2011-07-18 15:21:57 -03:00
Joost Cassee
2e4cb7fff9 Update changelog and increment version 2011-07-05 18:01:44 +02:00
Joost Cassee
aa9f1c0a2f Add Eric Davis to contributors 2011-07-05 07:22:06 +02:00
Joost Cassee
d5917b2e89 Update Django docs link to 1.3 2011-07-05 07:21:41 +02:00
Joost Cassee
13d4051592 Merge branch 'master' of github.com:jcassee/django-analytical
Remove settings manager from testcase
2011-07-05 07:12:58 +02:00
Joost Cassee
f0d4a412c2 Test updates for override_settings decorator 2011-07-04 14:07:56 +02:00
Joost Cassee
52e54c8db8 Merge pull request #4 from edavis/tests
Fix tests
2011-07-04 05:02:24 -07:00
Eric Davis
c09e02f833 Use a plain TestCase for the two suites that alter INSTALLED_APPS
And move the syncdb call to setUp in ChartbeatTagTestCaseWithSites
2011-06-29 12:15:10 -07:00
Eric Davis
6187229e3e Add test for CHARTBEAT_AUTO_DOMAIN
Even if 'django.contrib.sites' is in INSTALLED_APPS, don't add the
domain if CHARTBEAT_AUTO_DOMAIN is False
2011-06-29 12:07:37 -07:00
Eric Davis
249dac1467 CHARTBEAT_USER_ID is already set at the class level 2011-06-29 12:06:46 -07:00
Eric Davis
3391d78773 Check for 'django.contrib.sites' when getting the domain for chartbeat 2011-06-29 11:54:57 -07:00
Eric Davis
b712121a22 Clean up chartbeat tests
Must call 'syncdb' inside test_rendering_setup_site because we add
'django.contrib.sites' to INSTALLED_APPS
2011-06-29 10:49:37 -07:00
Eric Davis
5bf02a8a07 Backport an updated override_settings now included in Django trunk
Works better when decorating a TestCase
2011-06-29 10:47:37 -07:00
Eric Davis
7fafba34b4 run_tests is depreciated. Use DjangoTestSuiteRunner 2011-06-29 10:46:27 -07:00
Joost Cassee
60fc527ef0 Add @with/out_apps TestCase decorators
Add decorators for adding and removing apps from INSTALLED_APPS in test
cases.
2011-06-29 08:10:02 +02:00
Joost Cassee
e848c599c3 Merge pull request #3 from edavis/master
Add override_settings to fix changes to settings.INSTALLED_APPS.
Update clicky tests.
2011-06-28 22:22:39 -07:00
Eric Davis
2efd00dfbe Update tests to reflect changes in 70ff4f6 2011-06-28 11:49:29 -07:00
Eric Davis
5d0e7226ad Use override_settings with Chartbeat's test_rendering_setup_site too 2011-06-28 11:49:19 -07:00
Eric Davis
a76bb3a3a6 Drop django.contrib.sites from INSTALLED_APPS before the test even runs
An attempt at resolving https://github.com/jcassee/django-analytical/issues/2
2011-06-28 10:57:58 -07:00
Joost Cassee
70ff4f6826 Support multiple Clicky codes; increment version
Clicky has updated their Javascript code to support logging to multiple
sites at the same time.
2011-06-05 15:17:41 +02:00
Joost Cassee
3e5e0c25f2 Fix MANIFEST bug
An old MANIFEST file caused GoSquared support to be missing from the
source distribution.
2011-04-23 00:14:18 +02:00
Joost Cassee
d40b73f23c Bump version to 0.8.0 2011-04-01 22:42:03 +02:00
Joost Cassee
3a02a73d94 Add GoSquared support 2011-04-01 22:41:36 +02:00
Joost Cassee
41c7484495 Fix Woopra documentation typos 2011-04-01 22:02:35 +02:00
Joost Cassee
756b887dfd Fix Clicky template tags 2011-04-01 21:54:16 +02:00
Joost Cassee
4f82bd876a Update Clicky tracking code to use relative URLs 2011-03-30 15:11:47 +02:00
Joost Cassee
babfad16ac Add support for the Woopra service
Also bump the version to 0.7.0.
2011-03-15 16:48:23 +01:00
Joost Cassee
e4a53def30 Fix mixpanel-celery rST target name 2011-03-15 14:13:06 +01:00
Joost Cassee
81e1fb3463 Fix Crazy Egg template tag <script> tag 2011-03-15 13:15:07 +01:00
Joost Cassee
a4f41ee004 Add message customization to Olark 2011-03-15 13:14:29 +01:00
Joost Cassee
ae89a6965c Add Olark and Reinvigorate to the install docs 2011-03-15 12:58:52 +01:00
Joost Cassee
27b272f6fd Rename MIXPANEL_TOKEN setting
Wes Winham's mixpanel-celery package uses the MIXPANEL_API_TOKEN
setting. Use the same name for the Mixpanel token setting.
2011-03-15 12:55:07 +01:00
Joost Cassee
8649c07c24 Relax service code validation 2011-02-25 00:28:59 +01:00
Joost Cassee
c23b0abc02 Fix version and changelog
Version skipped 0.6.0 and the changelog was not updated.
2011-02-25 00:21:20 +01:00
Joost Cassee
19d6cbaa48 Add newest services to analytics tag 2011-02-24 22:32:50 +01:00
Joost Cassee
369d890452 Bump version to 0.7.0 2011-02-24 22:25:26 +01:00
Joost Cassee
707b8bbe72 Add Reinvigorate to README, fix typo in docs 2011-02-24 22:23:40 +01:00
Joost Cassee
4f2d95f928 Add support for Olark 2011-02-24 22:22:28 +01:00
Joost Cassee
5ac645aebb Fix tutorial typos, uncomment download URL in setup.py 2011-02-24 17:36:25 +01:00
Joost Cassee
8f9b5104da Add Reinvigorate service
This commit also contains a number of typo fixes in the documentation
and bumps the version.
2011-02-22 10:44:53 +01:00
Joost Cassee
327157d610 Fix small code style issues based on pylint
Also update patch version number.
2011-02-18 13:57:04 +01:00
Joost Cassee
246c72cc03 Split off Geckoboard support into django-geckoboard 2011-02-15 16:07:32 +01:00
137 changed files with 9430 additions and 2258 deletions

35
.github/workflows/check.yml vendored Normal file
View file

@ -0,0 +1,35 @@
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 Normal file
View file

@ -0,0 +1,44 @@
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 Normal file
View file

@ -0,0 +1,58 @@
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 }}

28
.gitignore vendored
View file

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

1
.pre-commit-config.yaml Normal file
View file

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

18
.readthedocs.yaml Normal file
View file

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

View file

@ -1,12 +0,0 @@
The django-analytical package is was written by `Joost Cassee`_.
Included Javascript code snippets for integration of the analytics
services was 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.
The work on Crazy Egg was made possible by `Bateau Knowledge`_.
.. _`Joost Cassee`: mailto:joost@cassee.net
.. _Analytical: https://github.com/jkrall/analytical
.. _`Bateau Knowledge`: http://www.bateauknowledge.nl/

View file

@ -1,3 +1,225 @@
(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.

46
CODE_OF_CONDUCT.md Normal file
View file

@ -0,0 +1,46 @@
# 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/

5
CONTRIBUTING.rst Normal file
View file

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

View file

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

View file

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

View file

@ -1,59 +1,141 @@
django-analytical
=================
The django-analytical application integrates analytics services into a
Django_ project.
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.
Currently supported services:
* `Chartbeat`_ traffic analysis
* `Clicky`_ traffic analysis
* `Crazy Egg`_ visual click tracking
* `Geckoboard`_ status board
* `Google Analytics`_ traffic analysis
* `HubSpot`_ inbound marketing
* `KISSinsights`_ feedback surveys
* `KISSmetrics`_ funnel analysis
* `Mixpanel`_ event tracking
* `Optimizely`_ A/B testing
* `Performable`_ web analytics and landing pages
The documentation can be found in the ``docs`` directory or `read
online`_. The source code and issue tracker are generously `hosted by
GitHub`_.
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 send a pull request. Please do create
an issue to discuss your plans.
.. _Django: http://www.djangoproject.com/
.. _Chartbeat: http://www.chartbeat.com/
.. _Clicky: http://getclicky.com/
.. _`Crazy Egg`: http://www.crazyegg.com/
.. _Geckoboard: http://www.geckoboard.com/
.. _`Google Analytics`: http://www.google.com/analytics/
.. _HubSpot: http://www.hubspot.com/
.. _KISSinsights: http://www.kissinsights.com/
.. _KISSmetrics: http://www.kissmetrics.com/
.. _Mixpanel: http://www.mixpanel.com/
.. _Optimizely: http://www.optimizely.com/
.. _Performable: http://www.performable.com/
.. _`read online`: http://packages.python.org/django-analytical/
.. _`hosted by GitHub`: http://github.com/jcassee/django-analytical
.. _`issue tracker`: http://github.com/jcassee/django-analytical/issues
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

View file

@ -1,15 +1,5 @@
"""
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/
Analytics service integration for Django projects.
"""
__author__ = "Joost Cassee"
__email__ = "joost@cassee.net"
__version__ = "0.4.0"
__copyright__ = "Copyright (C) 2011 Joost Cassee"
__license__ = "MIT License"
__version__ = '3.2.0'

View file

@ -1,296 +0,0 @@
"""
Geckoboard decorators.
"""
import base64
from xml.dom.minidom import Document
try:
from functools import wraps
except ImportError:
from django.utils.functional import wraps # Python 2.4 fallback
from django.conf import settings
from django.http import HttpResponse, HttpResponseForbidden
from django.views.decorators.csrf import csrf_exempt
from django.utils.datastructures import SortedDict
from django.utils.decorators import available_attrs
from django.utils import simplejson
TEXT_NONE = 0
TEXT_INFO = 2
TEXT_WARN = 1
class WidgetDecorator(object):
"""
Geckoboard widget decorator.
The decorated view must return a data structure suitable for
serialization to XML or JSON for Geckoboard. See the Geckoboard
API docs or the source of extending classes for details.
If the GECKOBOARD_API_KEY setting is used, the request must contain
the correct API key, or a 403 Forbidden response is returned.
"""
def __call__(self, view_func):
def _wrapped_view(request, *args, **kwargs):
if not _is_api_key_correct(request):
return HttpResponseForbidden("Geckoboard API key incorrect")
view_result = view_func(request, *args, **kwargs)
data = self._convert_view_result(view_result)
content = _render(request, data)
return HttpResponse(content)
wrapper = wraps(view_func, assigned=available_attrs(view_func))
return csrf_exempt(wrapper(_wrapped_view))
def _convert_view_result(self, data):
# Extending classes do view result mangling here.
return data
widget = WidgetDecorator()
class NumberWidgetDecorator(WidgetDecorator):
"""
Geckoboard number widget decorator.
The decorated view must return either a single value, or a list or
tuple with one or two values. The first (or only) value represents
the current value, the second value represents the previous value.
"""
def _convert_view_result(self, result):
if not isinstance(result, (tuple, list)):
result = [result]
return {'item': [{'value': v} for v in result]}
number = NumberWidgetDecorator()
class RAGWidgetDecorator(WidgetDecorator):
"""
Geckoboard red-amber-green widget decorator.
The decorated view must return a tuple or list with three values,
or three tuples (value, text). The values represent numbers shown
in red, amber and green respectively. The text parameter is
optional and will be displayed next to the value in the dashboard.
"""
def _convert_view_result(self, result):
items = []
for elem in result:
if not isinstance(elem, (tuple, list)):
elem = [elem]
item = SortedDict()
if elem[0] is None:
item['value'] = ''
else:
item['value'] = elem[0]
if len(elem) > 1:
item['text'] = elem[1]
items.append(item)
return {'item': items}
rag = RAGWidgetDecorator()
class TextWidgetDecorator(WidgetDecorator):
"""
Geckoboard text widget decorator.
The decorated view must return a list or tuple of strings, or tuples
(string, type). The type parameter tells Geckoboard how to display
the text. Use TEXT_INFO for informational messages, TEXT_WARN for
warnings and TEXT_NONE for plain text (the default).
"""
def _convert_view_result(self, result):
items = []
if not isinstance(result, (tuple, list)):
result = [result]
for elem in result:
if not isinstance(elem, (tuple, list)):
elem = [elem]
item = SortedDict()
item['text'] = elem[0]
if len(elem) > 1:
item['type'] = elem[1]
else:
item['type'] = TEXT_NONE
items.append(item)
return {'item': items}
text = TextWidgetDecorator()
class PieChartWidgetDecorator(WidgetDecorator):
"""
Geckoboard pie chart widget decorator.
The decorated view must return a list or tuple of tuples
(value, label, color). The color parameter is a string 'RRGGBB[TT]'
representing red, green, blue and optionally transparency.
"""
def _convert_view_result(self, result):
items = []
for elem in result:
if not isinstance(elem, (tuple, list)):
elem = [elem]
item = SortedDict()
item['value'] = elem[0]
if len(elem) > 1:
item['label'] = elem[1]
if len(elem) > 2:
item['colour'] = elem[2]
items.append(item)
return {'item': items}
pie_chart = PieChartWidgetDecorator()
class LineChartWidgetDecorator(WidgetDecorator):
"""
Geckoboard line chart widget decorator.
The decorated view must return a tuple (values, x_axis, y_axis,
color). The value parameter is a tuple or list of data points. The
x-axis parameter is a label string, or a tuple or list of strings,
that will be placed on the X-axis. The y-axis parameter works
similarly for the Y-axis. If there are more axis labels, they are
placed evenly along the axis. The color parameter is a string
'RRGGBB[TT]' representing red, green, blue and optionally
transparency.
"""
def _convert_view_result(self, result):
data = SortedDict()
data['item'] = result[0]
data['settings'] = SortedDict()
if len(result) > 1:
x_axis = result[1]
if x_axis is None:
x_axis = ''
if not isinstance(x_axis, (tuple, list)):
x_axis = [x_axis]
data['settings']['axisx'] = x_axis
if len(result) > 2:
y_axis = result[2]
if y_axis is None:
y_axis = ''
if not isinstance(y_axis, (tuple, list)):
y_axis = [y_axis]
data['settings']['axisy'] = y_axis
if len(result) > 3:
data['settings']['colour'] = result[3]
return data
line_chart = LineChartWidgetDecorator()
class GeckOMeterWidgetDecorator(WidgetDecorator):
"""
Geckoboard Geck-O-Meter widget decorator.
The decorated view must return a tuple (value, min, max). The value
parameter represents the current value. The min and max parameters
represent the minimum and maximum value respectively. They are
either a value, or a tuple (value, text). If used, the text
parameter will be displayed next to the minimum or maximum value.
"""
def _convert_view_result(self, result):
value, min, max = result
data = SortedDict()
data['item'] = value
data['max'] = SortedDict()
data['min'] = SortedDict()
if not isinstance(max, (tuple, list)):
max = [max]
data['max']['value'] = max[0]
if len(max) > 1:
data['max']['text'] = max[1]
if not isinstance(min, (tuple, list)):
min = [min]
data['min']['value'] = min[0]
if len(min) > 1:
data['min']['text'] = min[1]
return data
geck_o_meter = GeckOMeterWidgetDecorator()
def _is_api_key_correct(request):
"""Return whether the Geckoboard API key on the request is correct."""
api_key = getattr(settings, 'GECKOBOARD_API_KEY', None)
if api_key is None:
return True
auth = request.META.get('HTTP_AUTHORIZATION', '').split()
if len(auth) == 2:
if auth[0].lower() == 'basic':
request_key = base64.b64decode(auth[1])
return request_key == api_key
return False
def _render(request, data):
format = request.POST.get('format', '')
if not format:
format = request.GET.get('format', '')
if format == '2':
return _render_json(data)
else:
return _render_xml(data)
def _render_json(data):
return simplejson.dumps(data)
def _render_xml(data):
doc = Document()
root = doc.createElement('root')
doc.appendChild(root)
_build_xml(doc, root, data)
return doc.toxml()
def _build_xml(doc, parent, data):
if isinstance(data, (tuple, list)):
_build_list_xml(doc, parent, data)
elif isinstance(data, dict):
_build_dict_xml(doc, parent, data)
else:
_build_str_xml(doc, parent, data)
def _build_str_xml(doc, parent, data):
parent.appendChild(doc.createTextNode(str(data)))
def _build_list_xml(doc, parent, data):
for item in data:
_build_xml(doc, parent, item)
def _build_dict_xml(doc, parent, data):
for tag, item in data.items():
if isinstance(item, (list, tuple)):
for subitem in item:
elem = doc.createElement(tag)
_build_xml(doc, elem, subitem)
parent.appendChild(elem)
else:
elem = doc.createElement(tag)
_build_xml(doc, elem, item)
parent.appendChild(elem)
class GeckoboardException(Exception):
"""
Represents an error with the Geckoboard decorators.
"""

View file

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

View file

@ -2,31 +2,47 @@
Analytical template tags and filters.
"""
from __future__ import absolute_import
import logging
from importlib import import_module
from django import template
from django.template import Node, TemplateSyntaxError
from django.utils.importlib import import_module
from analytical.utils import AnalyticalException
from analytical.utils import AnalyticalException
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.optimizely'
'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()
@ -37,8 +53,10 @@ def _location_tag(location):
if len(bits) > 1:
raise TemplateSyntaxError("'%s' tag takes no arguments" % bits[0])
return AnalyticalNode(location)
return analytical_tag
for loc in TAG_LOCATIONS:
register.tag('analytical_%s' % loc, _location_tag(loc))
@ -48,27 +66,31 @@ class AnalyticalNode(Node):
self.nodes = [node_cls() for node_cls in template_nodes[location]]
def render(self, context):
return "".join([node.render(context) for node in self.nodes])
return ''.join([node.render(context) for node in self.nodes])
def _load_template_nodes():
template_nodes = dict((l, dict((p, []) for p in TAG_POSITIONS))
for l in TAG_LOCATIONS)
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)
try:
module.contribute_to_analytical(add_node_cls)
except AnalyticalException, e:
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), [])
template_nodes[location] = sum(
(template_nodes[location][p] for p in TAG_POSITIONS), []
)
return template_nodes
def _import_tag_module(path):
app_name, lib_name = path.rsplit('.', 1)
return import_module("%s.templatetags.%s" % (app_name, lib_name))
return import_module('%s.templatetags.%s' % (app_name, lib_name))
template_nodes = _load_template_nodes()

View file

@ -2,23 +2,19 @@
Chartbeat template tags and filters.
"""
from __future__ import absolute_import
import json
import re
from django.conf import settings
from django.contrib.sites.models import Site
from django.core.exceptions import ImproperlyConfigured
from django.template import Library, Node, TemplateSyntaxError
from django.utils import simplejson
from analytical.utils import is_internal_ip, disable_html, get_required_setting
from analytical.utils import disable_html, get_required_setting, is_internal_ip
USER_ID_RE = re.compile(r'^\d{5}$')
INIT_CODE = """<script type="text/javascript">var _sf_startpt=(new Date()).getTime()</script>"""
USER_ID_RE = re.compile(r'^\d+$')
INIT_CODE = """<script>var _sf_startpt=(new Date()).getTime()</script>"""
SETUP_CODE = """
<script type="text/javascript">
<script>
var _sf_async_config=%(config)s;
(function(){
function loadChartbeat() {
@ -36,7 +32,7 @@ SETUP_CODE = """
loadChartbeat : function() { oldonload(); loadChartbeat(); };
})();
</script>
"""
""" # noqa
DOMAIN_CONTEXT_KEY = 'chartbeat_domain'
@ -48,17 +44,18 @@ def chartbeat_top(parser, token):
"""
Top Chartbeat template tag.
Render the top Javascript code for Chartbeat.
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, self.name)
return disable_html(INIT_CODE, 'Chartbeat')
return INIT_CODE
@ -67,7 +64,7 @@ def chartbeat_bottom(parser, token):
"""
Bottom Chartbeat template tag.
Render the bottom Javascript code for Chartbeat. You must supply
Render the bottom JavaScript code for Chartbeat. You must supply
your Chartbeat User ID (as a string) in the ``CHARTBEAT_USER_ID``
setting.
"""
@ -76,23 +73,19 @@ def chartbeat_bottom(parser, token):
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 an five-digit number")
'CHARTBEAT_USER_ID', USER_ID_RE, 'must be (a string containing) a number'
)
def render(self, context):
config = {'uid': self.user_id}
domain = context.get(DOMAIN_CONTEXT_KEY)
if domain is None and getattr(settings, 'CHARTBEAT_AUTO_DOMAIN', True):
try:
domain = Site.objects.get_current().domain
except (ImproperlyConfigured, Site.DoesNotExist):
pass
domain = _get_domain(context)
if domain is not None:
config['domain'] = domain
html = SETUP_CODE % {'config': simplejson.dumps(config)}
html = SETUP_CODE % {'config': json.dumps(config, sort_keys=True)}
if is_internal_ip(context, 'CHARTBEAT'):
html = disable_html(html, 'Chartbeat')
return html
@ -102,3 +95,20 @@ def contribute_to_analytical(add_node):
ChartbeatBottomNode() # ensure properly configured
add_node('head_top', ChartbeatTopNode, 'first')
add_node('body_bottom', ChartbeatBottomNode, 'last')
def _get_domain(context):
domain = context.get(DOMAIN_CONTEXT_KEY)
if domain is not None:
return domain
else:
if 'django.contrib.sites' not in settings.INSTALLED_APPS:
return
elif getattr(settings, 'CHARTBEAT_AUTO_DOMAIN', True):
from django.contrib.sites.models import Site
try:
return Site.objects.get_current().domain
except (ImproperlyConfigured, Site.DoesNotExist): # pylint: disable=E1101
return

View file

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

View file

@ -2,34 +2,35 @@
Clicky template tags and filters.
"""
from __future__ import absolute_import
import json
import re
from django.template import Library, Node, TemplateSyntaxError
from django.utils import simplejson
from analytical.utils import get_identity, is_internal_ip, disable_html, \
get_required_setting
from analytical.utils import (
disable_html,
get_identity,
get_required_setting,
is_internal_ip,
)
SITE_ID_RE = re.compile(r'^\d{8}$')
SITE_ID_RE = re.compile(r'^\d+$')
TRACKING_CODE = """
<script type="text/javascript">
<script>
var clicky = { log: function(){ return; }, goal: function(){ return; }};
var clicky_site_id = %(site_id)s;
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 = ( document.location.protocol == 'https:' ? 'https://static.getclicky.com/js' : 'http://static.getclicky.com/js' );
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="http://in.getclicky.com/%(site_id)sns.gif" /></p></noscript>
"""
<noscript><p><img alt="Clicky" width="1" height="1" src="//in.getclicky.com/%(site_id)sns.gif" /></p></noscript>
""" # noqa
register = Library()
@ -39,7 +40,7 @@ def clicky(parser, token):
"""
Clicky tracking template tag.
Renders Javascript code to track page visits. You must supply
Renders JavaScript code to track page visits. You must supply
your Clicky Site ID (as a string) in the ``CLICKY_SITE_ID``
setting.
"""
@ -48,10 +49,12 @@ def clicky(parser, token):
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 an eight-digit number")
self.site_id = get_required_setting(
'CLICKY_SITE_ID', SITE_ID_RE, 'must be a (string containing) a number'
)
def render(self, context):
custom = {}
@ -64,8 +67,10 @@ class ClickyNode(Node):
if identity is not None:
custom.setdefault('session', {})['username'] = identity
html = TRACKING_CODE % {'site_id': self.site_id,
'custom': simplejson.dumps(custom)}
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

View file

@ -2,17 +2,17 @@
Crazy Egg template tags and filters.
"""
from __future__ import absolute_import
import re
from django.template import Library, Node, TemplateSyntaxError
from analytical.utils import is_internal_ip, disable_html, get_required_setting
from analytical.utils import disable_html, get_required_setting, is_internal_ip
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>"""
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');"
@ -24,7 +24,7 @@ def crazy_egg(parser, token):
"""
Crazy Egg tracking template tag.
Renders Javascript code to track page clicks. You must supply
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.
"""
@ -33,22 +33,32 @@ def crazy_egg(parser, token):
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 an eight-digit number")
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:]}
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))
vars = [(i, v) for i, v in enumerate(values, 1) if v is not None]
if vars:
js = " ".join(USERVAR_CODE % {'varnr': varnr, 'value': value}
for (varnr, value) in vars)
html = '%s\n<script type="text/javascript">%s</script>' \
% (html, js)
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

View file

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

View file

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

View file

@ -1,14 +1,26 @@
"""
Google Analytics template tags and filters.
DEPRECATED
"""
from __future__ import absolute_import
import decimal
import re
from django.conf import settings
from django.template import Library, Node, TemplateSyntaxError
from analytical.utils import is_internal_ip, disable_html, get_required_setting
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
@ -16,23 +28,40 @@ SCOPE_PAGE = 3
PROPERTY_ID_RE = re.compile(r'^UA-\d+-\d+$')
SETUP_CODE = """
<script type="text/javascript">
<script>
var _gaq = _gaq || [];
_gaq.push(['_setAccount', '%(property_id)s']);
_gaq.push(['_trackPageview']);
%(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';
ga.src = ('https:' == document.location.protocol ? %(source_scheme)s) + %(source_url)s;
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
})();
</script>
"""
CUSTOM_VAR_CODE = "_gaq.push(['_setCustomVar', %(index)s, '%(name)s', " \
"'%(value)s', %(scope)s]);"
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()
@ -42,7 +71,7 @@ def google_analytics(parser, token):
"""
Google Analytics tracking template tag.
Renders Javascript code to track page visits. You must supply
Renders JavaScript code to track page visits. You must supply
your website property ID (as a string) in the
``GOOGLE_ANALYTICS_PROPERTY_ID`` setting.
"""
@ -51,33 +80,124 @@ def google_analytics(parser, token):
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'")
'GOOGLE_ANALYTICS_PROPERTY_ID',
PROPERTY_ID_RE,
"must be a string looking like 'UA-XXXXXX-Y'",
)
def render(self, context):
commands = self._get_custom_var_commands(context)
html = SETUP_CODE % {'property_id': self.property_id,
'commands': " ".join(commands)}
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_custom_var_commands(self, context):
values = (context.get('google_analytics_var%s' % i)
for i in range(1, 6))
vars = [(i, v) for i, v in enumerate(values, 1) if v is not None]
def _get_domain_commands(self, context):
commands = []
for index, var in vars:
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 % locals())
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

View file

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

View file

@ -0,0 +1,176 @@
"""
Google Analytics template tags and filters, using the new analytics.js library.
"""
import decimal
import re
from django.conf import settings
from django.template import Library, Node, TemplateSyntaxError
from analytical.utils import (
AnalyticalException,
disable_html,
get_domain,
get_required_setting,
is_internal_ip,
)
TRACK_SINGLE_DOMAIN = 1
TRACK_MULTIPLE_SUBDOMAINS = 2
TRACK_MULTIPLE_DOMAINS = 3
PROPERTY_ID_RE = re.compile(r'^UA-\d+-\d+$')
SETUP_CODE = """
<script>
(function(i,s,o,g,r,a,m){{i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){{
(i[r].q=i[r].q||[]).push(arguments)}},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
}})(window,document,'script','{js_source}','ga');
ga('create', '{property_id}', 'auto', {create_fields});
{commands}ga('send', 'pageview');
</script>
"""
REQUIRE_DISPLAY_FEATURES = "ga('require', 'displayfeatures');\n"
CUSTOM_VAR_CODE = "ga('set', '{name}', {value});\n"
ANONYMIZE_IP_CODE = "ga('set', 'anonymizeIp', true);\n"
register = Library()
@register.tag
def google_analytics_js(parser, token):
"""
Google Analytics tracking template tag.
Renders JavaScript code to track page visits. You must supply
your website property ID (as a string) in the
``GOOGLE_ANALYTICS_JS_PROPERTY_ID`` setting.
"""
bits = token.split_contents()
if len(bits) > 1:
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
return GoogleAnalyticsJsNode()
class GoogleAnalyticsJsNode(Node):
def __init__(self):
self.property_id = get_required_setting(
'GOOGLE_ANALYTICS_JS_PROPERTY_ID',
PROPERTY_ID_RE,
"must be a string looking like 'UA-XXXXXX-Y'",
)
def render(self, context):
import json
create_fields = self._get_domain_fields(context)
create_fields.update(self._get_other_create_fields(context))
commands = self._get_custom_var_commands(context)
commands.extend(self._get_other_commands(context))
display_features = getattr(
settings, 'GOOGLE_ANALYTICS_DISPLAY_ADVERTISING', False
)
if display_features:
commands.insert(0, REQUIRE_DISPLAY_FEATURES)
js_source = getattr(
settings,
'GOOGLE_ANALYTICS_JS_SOURCE',
'https://www.google-analytics.com/analytics.js',
)
html = SETUP_CODE.format(
property_id=self.property_id,
create_fields=json.dumps(create_fields),
commands=''.join(commands),
js_source=js_source,
)
if is_internal_ip(context, 'GOOGLE_ANALYTICS'):
html = disable_html(html, 'Google Analytics')
return html
def _get_domain_fields(self, context):
domain_fields = {}
tracking_type = getattr(
settings, 'GOOGLE_ANALYTICS_TRACKING_STYLE', TRACK_SINGLE_DOMAIN
)
if tracking_type == TRACK_SINGLE_DOMAIN:
pass
else:
domain = get_domain(context, 'google_analytics')
if domain is None:
raise AnalyticalException(
'tracking multiple domains with Google Analytics requires a domain name'
)
domain_fields['legacyCookieDomain'] = domain
if tracking_type == TRACK_MULTIPLE_DOMAINS:
domain_fields['allowLinker'] = True
return domain_fields
def _get_other_create_fields(self, context):
other_fields = {}
site_speed_sample_rate = getattr(
settings, 'GOOGLE_ANALYTICS_SITE_SPEED_SAMPLE_RATE', False
)
if site_speed_sample_rate is not False:
value = int(decimal.Decimal(site_speed_sample_rate))
if not 0 <= value <= 100:
raise AnalyticalException(
"'GOOGLE_ANALYTICS_SITE_SPEED_SAMPLE_RATE' must be >= 0 and <= 100"
)
other_fields['siteSpeedSampleRate'] = value
sample_rate = getattr(settings, 'GOOGLE_ANALYTICS_SAMPLE_RATE', False)
if sample_rate is not False:
value = int(decimal.Decimal(sample_rate))
if not 0 <= value <= 100:
raise AnalyticalException(
"'GOOGLE_ANALYTICS_SAMPLE_RATE' must be >= 0 and <= 100"
)
other_fields['sampleRate'] = value
cookie_expires = getattr(settings, 'GOOGLE_ANALYTICS_COOKIE_EXPIRATION', False)
if cookie_expires is not False:
value = int(decimal.Decimal(cookie_expires))
if value < 0:
raise AnalyticalException(
"'GOOGLE_ANALYTICS_COOKIE_EXPIRATION' must be >= 0"
)
other_fields['cookieExpires'] = value
return other_fields
def _get_custom_var_commands(self, context):
values = (context.get('google_analytics_var%s' % i) for i in range(1, 6))
params = [(i, v) for i, v in enumerate(values, 1) if v is not None]
commands = []
for _, var in params:
name = var[0]
value = var[1]
try:
float(value)
except ValueError:
value = f"'{value}'"
commands.append(
CUSTOM_VAR_CODE.format(
name=name,
value=value,
)
)
return commands
def _get_other_commands(self, context):
commands = []
if getattr(settings, 'GOOGLE_ANALYTICS_ANONYMIZE_IP', False):
commands.append(ANONYMIZE_IP_CODE)
return commands
def contribute_to_analytical(add_node):
GoogleAnalyticsJsNode() # ensure properly configured
add_node('head_bottom', GoogleAnalyticsJsNode)

View file

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

View file

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

View file

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

View file

@ -2,26 +2,25 @@
HubSpot template tags and filters.
"""
from __future__ import absolute_import
import re
from django.template import Library, Node, TemplateSyntaxError
from analytical.utils import is_internal_ip, disable_html, get_required_setting
from analytical.utils import disable_html, get_required_setting, is_internal_ip
PORTAL_ID_RE = re.compile(r'^\d+$')
DOMAIN_RE = re.compile(r'^[\w.-]+$')
TRACKING_CODE = """
<script type="text/javascript" language="javascript">
var hs_portalid = %(portal_id)s;
var hs_salog_version = "2.00";
var hs_ppa = "%(domain)s";
document.write(unescape("%%3Cscript src='" + document.location.protocol + "//" + hs_ppa + "/salog.js.aspx' type='text/javascript'%%3E%%3C/script%%3E"));
</script>
"""
<!-- 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()
@ -31,25 +30,23 @@ 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,
and the website domain in ``HUBSPOT_DOMAIN``.
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")
self.domain = get_required_setting('HUBSPOT_DOMAIN',
DOMAIN_RE, "must be an internet domain name")
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,
'domain': self.domain}
html = TRACKING_CODE % {'portal_id': self.portal_id}
if is_internal_ip(context, 'HUBSPOT'):
html = disable_html(html, 'HubSpot')
return html

View file

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

View file

@ -2,21 +2,18 @@
KISSinsights template tags.
"""
from __future__ import absolute_import
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]{3}$')
SITE_CODE_RE = re.compile(r'^[\w]+$')
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>
"""
<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'
@ -30,7 +27,7 @@ def kiss_insights(parser, token):
"""
KISSinsights set-up template tag.
Renders Javascript code to set-up surveys. You must supply
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.
@ -40,13 +37,19 @@ def kiss_insights(parser, token):
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")
'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 = []
@ -54,12 +57,14 @@ class KissInsightsNode(Node):
if identity is not None:
commands.append(IDENTIFY_CODE % identity)
try:
commands.append(SHOW_SURVEY_CODE
% context[SHOW_SURVEY_CONTEXT_KEY])
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)}
html = SETUP_CODE % {
'account_number': self.account_number,
'site_code': self.site_code,
'commands': ' '.join(commands),
}
return html

View file

@ -2,20 +2,21 @@
KISSmetrics template tags.
"""
from __future__ import absolute_import
import json
import re
from django.template import Library, Node, TemplateSyntaxError
from django.utils import simplejson
from analytical.utils import is_internal_ip, disable_html, get_identity, \
get_required_setting
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 type="text/javascript">
<script>
var _kmq = _kmq || [];
%(commands)s
function _kms(u){
@ -34,7 +35,12 @@ TRACKING_CODE = """
"""
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()
@ -44,7 +50,7 @@ def kiss_metrics(parser, token):
"""
KISSinsights tracking template tag.
Renders Javascript code to track page visits. You must supply
Renders JavaScript code to track page visits. You must supply
your KISSmetrics API key in the ``KISS_METRICS_API_KEY``
setting.
"""
@ -53,11 +59,14 @@ def kiss_metrics(parser, token):
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")
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 = []
@ -65,13 +74,36 @@ class KissMetricsNode(Node):
if identity is not None:
commands.append(IDENTIFY_CODE % identity)
try:
name, properties = context[EVENT_CONTEXT_KEY]
commands.append(EVENT_CODE % {'name': name,
'properties': simplejson.dumps(properties)})
properties = context[ALIAS_CONTEXT_KEY]
key, value = properties.popitem()
commands.append(ALIAS_CODE % (key, value))
except KeyError:
pass
html = TRACKING_CODE % {'api_key': self.api_key,
'commands': " ".join(commands)}
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

View file

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

View file

@ -0,0 +1,152 @@
"""
Matomo template tags and filters.
"""
import re
from collections import namedtuple
from itertools import chain
from django.conf import settings
from django.template import Library, Node, TemplateSyntaxError
from analytical.utils import (
disable_html,
get_identity,
get_required_setting,
is_internal_ip,
)
# domain name (characters separated by a dot), optional port, optional URI path, no slash
DOMAINPATH_RE = re.compile(r'^(([^./?#@:]+\.)*[^./?#@:]+)+(:[0-9]+)?(/[^/?#@:]+)*$')
# numeric ID
SITEID_RE = re.compile(r'^\d+$')
TRACKING_CODE = """
<script>
var _paq = window._paq || [];
%(variables)s
%(commands)s
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
(function() {
var u="//%(url)s/";
_paq.push(['setTrackerUrl', u+'matomo.php']);
_paq.push(['setSiteId', %(siteid)s]);
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
g.type='text/javascript'; g.async=true; g.defer=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);
})();
</script>
<noscript><p><img src="//%(url)s/matomo.php?idsite=%(siteid)s" style="border:0;" alt="" /></p></noscript>
""" # noqa
VARIABLE_CODE = (
'_paq.push(["setCustomVariable", %(index)s, "%(name)s", "%(value)s", "%(scope)s"]);' # noqa
)
IDENTITY_CODE = '_paq.push(["setUserId", "%(userid)s"]);'
DISABLE_COOKIES_CODE = "_paq.push(['disableCookies']);"
GIVE_CONSENT_CLASS = 'matomo_give_consent'
REMOVE_CONSENT_CLASS = 'matomo_remove_consent'
ASK_FOR_CONSENT_CODE = """
_paq.push(['requireConsent']);
var elements = document.getElementsByClassName("{}");
for (var i = 0; i < elements.length; i++) {{
elements[i].addEventListener("click",
function () {{
_paq.push(["forgetConsentGiven"]);
}}
);
}}
var elements = document.getElementsByClassName("{}");
for (var i = 0; i < elements.length; i++) {{
elements[i].addEventListener("click",
function () {{
_paq.push(["rememberConsentGiven"]);
}}
);
}}
""".format(REMOVE_CONSENT_CLASS, GIVE_CONSENT_CLASS)
DEFAULT_SCOPE = 'page'
MatomoVar = namedtuple('MatomoVar', ('index', 'name', 'value', 'scope'))
register = Library()
@register.tag
def matomo(parser, token):
"""
Matomo tracking template tag.
Renders JavaScript code to track page visits. You must supply
your Matomo domain (plus optional URI path), and tracked site ID
in the ``MATOMO_DOMAIN_PATH`` and the ``MATOMO_SITE_ID`` setting.
Custom variables can be passed in the ``matomo_vars`` context
variable. It is an iterable of custom variables as tuples like:
``(index, name, value[, scope])`` where scope may be ``'page'``
(default) or ``'visit'``. Index should be an integer and the
other parameters should be strings.
"""
bits = token.split_contents()
if len(bits) > 1:
raise TemplateSyntaxError("'%s' takes no arguments" % bits[0])
return MatomoNode()
class MatomoNode(Node):
def __init__(self):
self.domain_path = get_required_setting(
'MATOMO_DOMAIN_PATH',
DOMAINPATH_RE,
'must be a domain name, optionally followed '
'by an URI path, no trailing slash (e.g. '
'matomo.example.com or my.matomo.server/path)',
)
self.site_id = get_required_setting(
'MATOMO_SITE_ID', SITEID_RE, 'must be a (string containing a) number'
)
def render(self, context):
custom_variables = context.get('matomo_vars', ())
complete_variables = (
var if len(var) >= 4 else var + (DEFAULT_SCOPE,) for var in custom_variables
)
variables_code = (
VARIABLE_CODE % MatomoVar(*var)._asdict() for var in complete_variables
)
commands = []
if getattr(settings, 'MATOMO_DISABLE_COOKIES', False):
commands.append(DISABLE_COOKIES_CODE)
if getattr(settings, 'MATOMO_ASK_FOR_CONSENT', False):
commands.append(ASK_FOR_CONSENT_CODE)
userid = get_identity(context, 'matomo')
if userid is not None:
variables_code = chain(
variables_code, (IDENTITY_CODE % {'userid': userid},)
)
html = TRACKING_CODE % {
'url': self.domain_path,
'siteid': self.site_id,
'variables': '\n '.join(variables_code),
'commands': '\n '.join(commands),
}
if is_internal_ip(context, 'MATOMO'):
html = disable_html(html, 'Matomo')
return html
def contribute_to_analytical(add_node):
MatomoNode() # ensure properly configured
add_node('body_bottom', MatomoNode)

View file

@ -2,32 +2,31 @@
Mixpanel template tags and filters.
"""
from __future__ import absolute_import
import json
import re
from django.template import Library, Node, TemplateSyntaxError
from django.utils import simplejson
from django.utils.safestring import mark_safe
from analytical.utils import is_internal_ip, disable_html, get_identity, \
get_required_setting
from analytical.utils import (
disable_html,
get_identity,
get_required_setting,
is_internal_ip,
)
MIXPANEL_TOKEN_RE = re.compile(r'^[0-9a-f]{32}$')
MIXPANEL_API_TOKEN_RE = re.compile(r'^[0-9a-f]{32}$')
TRACKING_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>(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>
"""
IDENTIFY_CODE = "mpq.push(['identify', '%s']);"
EVENT_CODE = "mpq.push(['track', '%(name)s', %(properties)s]);"
""" # 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()
@ -38,36 +37,54 @@ def mixpanel(parser, token):
"""
Mixpanel tracking template tag.
Renders Javascript code to track page visits. You must supply
your Mixpanel token in the ``MIXPANEL_TOKEN`` setting.
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_TOKEN',
MIXPANEL_TOKEN_RE,
"must be a string containing a 32-digit hexadecimal number")
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:
commands.append(IDENTIFY_CODE % identity)
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': simplejson.dumps(properties)})
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)}
html = TRACKING_CODE % {
'token': self._token,
'commands': ' '.join(commands),
}
if is_internal_ip(context, 'MIXPANEL'):
html = disable_html(html, 'Mixpanel')
return html
return mark_safe(html)
def contribute_to_analytical(add_node):

View file

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

View file

@ -2,16 +2,13 @@
Optimizely template tags and filters.
"""
from __future__ import absolute_import
import re
from django.template import Library, Node, TemplateSyntaxError
from analytical.utils import is_internal_ip, disable_html, get_required_setting
from analytical.utils import disable_html, get_required_setting, is_internal_ip
ACCOUNT_NUMBER_RE = re.compile(r'^\d{7}$')
ACCOUNT_NUMBER_RE = re.compile(r'^\d+$')
SETUP_CODE = """<script src="//cdn.optimizely.com/js/%(account_number)s.js"></script>"""
@ -23,7 +20,7 @@ def optimizely(parser, token):
"""
Optimizely template tag.
Renders Javascript code to set-up A/B testing. You must supply
Renders JavaScript code to set-up A/B testing. You must supply
your Optimizely account number in the ``OPTIMIZELY_ACCOUNT_NUMBER``
setting.
"""
@ -32,11 +29,14 @@ def optimizely(parser, token):
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 containing an seven-digit number")
'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}

View file

@ -2,34 +2,38 @@
Performable template tags and filters.
"""
from __future__ import absolute_import
import re
from django.template import Library, Node, TemplateSyntaxError
from django.utils.safestring import mark_safe
from analytical.utils import is_internal_ip, disable_html, get_identity, \
get_required_setting
from analytical.utils import (
disable_html,
get_identity,
get_required_setting,
is_internal_ip,
)
API_KEY_RE = re.compile(r'^\w{6}$')
SETUP_CODE = """<script src="//d1nu2rn22elx8m.cloudfront.net/performable/pax/%(api_key)s.js" type="text/javascript"></script>"""
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 type="text/javascript">
<script>
var _paq = _paq || [];
_paq.push(["identify", {identity: "%s"}]);
</script>
"""
EMBED_CODE = """
<script type="text/javascript" src="//d1nu2rn22elx8m.cloudfront.net/performable/embed/page.js"></script>
<script type="text/javascript">
<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()
@ -39,7 +43,7 @@ def performable(parser, token):
"""
Performable template tag.
Renders Javascript code to set-up Performable tracking. You must
Renders JavaScript code to set-up Performable tracking. You must
supply your Performable API key in the ``PERFORMABLE_API_KEY``
setting.
"""
@ -48,17 +52,18 @@ def performable(parser, token):
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 containing five alphanumerical characters")
'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)
html = '%s%s' % (IDENTIFY_CODE % identity, html)
if is_internal_ip(context, 'PERFORMABLE'):
html = disable_html(html, 'Performable')
return html
@ -69,7 +74,13 @@ def performable_embed(hostname, page_id):
"""
Include a Performable landing page.
"""
return EMBED_CODE % locals()
return mark_safe(
EMBED_CODE
% {
'hostname': hostname,
'page_id': page_id,
}
)
def contribute_to_analytical(add_node):

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,17 +0,0 @@
"""
Tests for django-analytical.
"""
from analytical.tests.test_geckoboard import *
from analytical.tests.test_tag_analytical import *
from analytical.tests.test_tag_chartbeat import *
from analytical.tests.test_tag_clicky import *
from analytical.tests.test_tag_crazy_egg import *
from analytical.tests.test_tag_google_analytics import *
from analytical.tests.test_tag_hubspot import *
from analytical.tests.test_tag_kiss_insights import *
from analytical.tests.test_tag_kiss_metrics import *
from analytical.tests.test_tag_mixpanel import *
from analytical.tests.test_tag_optimizely import *
from analytical.tests.test_tag_performable import *
from analytical.tests.test_utils import *

View file

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

View file

@ -1,317 +0,0 @@
"""
Tests for the Geckoboard decorators.
"""
from django.http import HttpRequest, HttpResponseForbidden
from django.utils.datastructures import SortedDict
from analytical import geckoboard
from analytical.tests.utils import TestCase
import base64
class WidgetDecoratorTestCase(TestCase):
"""
Tests for the ``widget`` decorator.
"""
def setUp(self):
super(WidgetDecoratorTestCase, self).setUp()
self.settings_manager.delete('GECKOBOARD_API_KEY')
self.xml_request = HttpRequest()
self.xml_request.POST['format'] = '1'
self.json_request = HttpRequest()
self.json_request.POST['format'] = '2'
def test_api_key(self):
self.settings_manager.set(GECKOBOARD_API_KEY='abc')
req = HttpRequest()
req.META['HTTP_AUTHORIZATION'] = "basic %s" % base64.b64encode('abc')
resp = geckoboard.widget(lambda r: "test")(req)
self.assertEqual('<?xml version="1.0" ?><root>test</root>',
resp.content)
def test_missing_api_key(self):
self.settings_manager.set(GECKOBOARD_API_KEY='abc')
req = HttpRequest()
resp = geckoboard.widget(lambda r: "test")(req)
self.assertTrue(isinstance(resp, HttpResponseForbidden), resp)
self.assertEqual('Geckoboard API key incorrect', resp.content)
def test_wrong_api_key(self):
self.settings_manager.set(GECKOBOARD_API_KEY='abc')
req = HttpRequest()
req.META['HTTP_AUTHORIZATION'] = "basic %s" % base64.b64encode('def')
resp = geckoboard.widget(lambda r: "test")(req)
self.assertTrue(isinstance(resp, HttpResponseForbidden), resp)
self.assertEqual('Geckoboard API key incorrect', resp.content)
def test_xml_get(self):
req = HttpRequest()
req.GET['format'] = '1'
resp = geckoboard.widget(lambda r: "test")(req)
self.assertEqual('<?xml version="1.0" ?><root>test</root>',
resp.content)
def test_json_get(self):
req = HttpRequest()
req.GET['format'] = '2'
resp = geckoboard.widget(lambda r: "test")(req)
self.assertEqual('"test"', resp.content)
def test_xml_post(self):
req = HttpRequest()
req.POST['format'] = '1'
resp = geckoboard.widget(lambda r: "test")(req)
self.assertEqual('<?xml version="1.0" ?><root>test</root>',
resp.content)
def test_json_post(self):
req = HttpRequest()
req.POST['format'] = '2'
resp = geckoboard.widget(lambda r: "test")(req)
self.assertEqual('"test"', resp.content)
def test_scalar_xml(self):
resp = geckoboard.widget(lambda r: "test")(self.xml_request)
self.assertEqual('<?xml version="1.0" ?><root>test</root>',
resp.content)
def test_scalar_json(self):
resp = geckoboard.widget(lambda r: "test")(self.json_request)
self.assertEqual('"test"', resp.content)
def test_dict_xml(self):
widget = geckoboard.widget(lambda r: SortedDict([('a', 1), ('b', 2)]))
resp = widget(self.xml_request)
self.assertEqual('<?xml version="1.0" ?><root><a>1</a><b>2</b></root>',
resp.content)
def test_dict_json(self):
widget = geckoboard.widget(lambda r: SortedDict([('a', 1), ('b', 2)]))
resp = widget(self.json_request)
self.assertEqual('{"a": 1, "b": 2}', resp.content)
def test_list_xml(self):
widget = geckoboard.widget(lambda r: {'list': [1, 2, 3]})
resp = widget(self.xml_request)
self.assertEqual('<?xml version="1.0" ?><root><list>1</list>'
'<list>2</list><list>3</list></root>', resp.content)
def test_list_json(self):
widget = geckoboard.widget(lambda r: {'list': [1, 2, 3]})
resp = widget(self.json_request)
self.assertEqual('{"list": [1, 2, 3]}', resp.content)
def test_dict_list_xml(self):
widget = geckoboard.widget(lambda r: {'item': [
{'value': 1, 'text': "test1"}, {'value': 2, 'text': "test2"}]})
resp = widget(self.xml_request)
self.assertEqual('<?xml version="1.0" ?><root>'
'<item><text>test1</text><value>1</value></item>'
'<item><text>test2</text><value>2</value></item></root>',
resp.content)
def test_dict_list_json(self):
widget = geckoboard.widget(lambda r: {'item': [
SortedDict([('value', 1), ('text', "test1")]),
SortedDict([('value', 2), ('text', "test2")])]})
resp = widget(self.json_request)
self.assertEqual('{"item": [{"value": 1, "text": "test1"}, '
'{"value": 2, "text": "test2"}]}', resp.content)
class NumberDecoratorTestCase(TestCase):
"""
Tests for the ``number`` decorator.
"""
def setUp(self):
super(NumberDecoratorTestCase, self).setUp()
self.settings_manager.delete('GECKOBOARD_API_KEY')
self.request = HttpRequest()
self.request.POST['format'] = '2'
def test_scalar(self):
widget = geckoboard.number(lambda r: 10)
resp = widget(self.request)
self.assertEqual('{"item": [{"value": 10}]}', resp.content)
def test_singe_value(self):
widget = geckoboard.number(lambda r: [10])
resp = widget(self.request)
self.assertEqual('{"item": [{"value": 10}]}', resp.content)
def test_two_values(self):
widget = geckoboard.number(lambda r: [10, 9])
resp = widget(self.request)
self.assertEqual('{"item": [{"value": 10}, {"value": 9}]}',
resp.content)
class RAGDecoratorTestCase(TestCase):
"""
Tests for the ``rag`` decorator.
"""
def setUp(self):
super(RAGDecoratorTestCase, self).setUp()
self.settings_manager.delete('GECKOBOARD_API_KEY')
self.request = HttpRequest()
self.request.POST['format'] = '2'
def test_scalars(self):
widget = geckoboard.rag(lambda r: (10, 5, 1))
resp = widget(self.request)
self.assertEqual(
'{"item": [{"value": 10}, {"value": 5}, {"value": 1}]}',
resp.content)
def test_tuples(self):
widget = geckoboard.rag(lambda r: ((10, "ten"), (5, "five"),
(1, "one")))
resp = widget(self.request)
self.assertEqual('{"item": [{"value": 10, "text": "ten"}, '
'{"value": 5, "text": "five"}, {"value": 1, "text": "one"}]}',
resp.content)
class TextDecoratorTestCase(TestCase):
"""
Tests for the ``text`` decorator.
"""
def setUp(self):
super(TextDecoratorTestCase, self).setUp()
self.settings_manager.delete('GECKOBOARD_API_KEY')
self.request = HttpRequest()
self.request.POST['format'] = '2'
def test_string(self):
widget = geckoboard.text(lambda r: "test message")
resp = widget(self.request)
self.assertEqual('{"item": [{"text": "test message", "type": 0}]}',
resp.content)
def test_list(self):
widget = geckoboard.text(lambda r: ["test1", "test2"])
resp = widget(self.request)
self.assertEqual('{"item": [{"text": "test1", "type": 0}, '
'{"text": "test2", "type": 0}]}', resp.content)
def test_list_tuples(self):
widget = geckoboard.text(lambda r: [("test1", geckoboard.TEXT_NONE),
("test2", geckoboard.TEXT_INFO),
("test3", geckoboard.TEXT_WARN)])
resp = widget(self.request)
self.assertEqual('{"item": [{"text": "test1", "type": 0}, '
'{"text": "test2", "type": 2}, '
'{"text": "test3", "type": 1}]}', resp.content)
class PieChartDecoratorTestCase(TestCase):
"""
Tests for the ``pie_chart`` decorator.
"""
def setUp(self):
super(PieChartDecoratorTestCase, self).setUp()
self.settings_manager.delete('GECKOBOARD_API_KEY')
self.request = HttpRequest()
self.request.POST['format'] = '2'
def test_scalars(self):
widget = geckoboard.pie_chart(lambda r: [1, 2, 3])
resp = widget(self.request)
self.assertEqual(
'{"item": [{"value": 1}, {"value": 2}, {"value": 3}]}',
resp.content)
def test_tuples(self):
widget = geckoboard.pie_chart(lambda r: [(1, ), (2, ), (3, )])
resp = widget(self.request)
self.assertEqual(
'{"item": [{"value": 1}, {"value": 2}, {"value": 3}]}',
resp.content)
def test_2tuples(self):
widget = geckoboard.pie_chart(lambda r: [(1, "one"), (2, "two"),
(3, "three")])
resp = widget(self.request)
self.assertEqual('{"item": [{"value": 1, "label": "one"}, '
'{"value": 2, "label": "two"}, '
'{"value": 3, "label": "three"}]}', resp.content)
def test_3tuples(self):
widget = geckoboard.pie_chart(lambda r: [(1, "one", "00112233"),
(2, "two", "44556677"), (3, "three", "8899aabb")])
resp = widget(self.request)
self.assertEqual('{"item": ['
'{"value": 1, "label": "one", "colour": "00112233"}, '
'{"value": 2, "label": "two", "colour": "44556677"}, '
'{"value": 3, "label": "three", "colour": "8899aabb"}]}',
resp.content)
class LineChartDecoratorTestCase(TestCase):
"""
Tests for the ``line_chart`` decorator.
"""
def setUp(self):
super(LineChartDecoratorTestCase, self).setUp()
self.settings_manager.delete('GECKOBOARD_API_KEY')
self.request = HttpRequest()
self.request.POST['format'] = '2'
def test_values(self):
widget = geckoboard.line_chart(lambda r: ([1, 2, 3],))
resp = widget(self.request)
self.assertEqual('{"item": [1, 2, 3], "settings": {}}', resp.content)
def test_x_axis(self):
widget = geckoboard.line_chart(lambda r: ([1, 2, 3],
["first", "last"]))
resp = widget(self.request)
self.assertEqual('{"item": [1, 2, 3], '
'"settings": {"axisx": ["first", "last"]}}', resp.content)
def test_axes(self):
widget = geckoboard.line_chart(lambda r: ([1, 2, 3],
["first", "last"], ["low", "high"]))
resp = widget(self.request)
self.assertEqual('{"item": [1, 2, 3], "settings": '
'{"axisx": ["first", "last"], "axisy": ["low", "high"]}}',
resp.content)
def test_color(self):
widget = geckoboard.line_chart(lambda r: ([1, 2, 3],
["first", "last"], ["low", "high"], "00112233"))
resp = widget(self.request)
self.assertEqual('{"item": [1, 2, 3], "settings": '
'{"axisx": ["first", "last"], "axisy": ["low", "high"], '
'"colour": "00112233"}}', resp.content)
class GeckOMeterDecoratorTestCase(TestCase):
"""
Tests for the ``line_chart`` decorator.
"""
def setUp(self):
super(GeckOMeterDecoratorTestCase, self).setUp()
self.settings_manager.delete('GECKOBOARD_API_KEY')
self.request = HttpRequest()
self.request.POST['format'] = '2'
def test_scalars(self):
widget = geckoboard.geck_o_meter(lambda r: (2, 1, 3))
resp = widget(self.request)
self.assertEqual('{"item": 2, "max": {"value": 3}, '
'"min": {"value": 1}}', resp.content)
def test_tuples(self):
widget = geckoboard.geck_o_meter(lambda r: (2, (1, "min"), (3, "max")))
resp = widget(self.request)
self.assertEqual('{"item": 2, "max": {"value": 3, "text": "max"}, '
'"min": {"value": 1, "text": "min"}}', resp.content)

View file

@ -1,90 +0,0 @@
"""
Tests for the Chartbeat template tags and filters.
"""
import re
from django.conf import settings
from django.contrib.sites.models import Site
from django.http import HttpRequest
from django.template import Context
from analytical.templatetags.chartbeat import ChartbeatTopNode, \
ChartbeatBottomNode
from analytical.tests.utils import TagTestCase
from analytical.utils import AnalyticalException
class ChartbeatTagTestCase(TagTestCase):
"""
Tests for the ``chartbeat`` template tag.
"""
def setUp(self):
super(ChartbeatTagTestCase, self).setUp()
self.settings_manager.set(CHARTBEAT_USER_ID='12345')
def test_top_tag(self):
r = self.render_tag('chartbeat', 'chartbeat_top',
{'chartbeat_domain': "test.com"})
self.assertTrue('var _sf_startpt=(new Date()).getTime()' in r, r)
def test_bottom_tag(self):
r = self.render_tag('chartbeat', 'chartbeat_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_top_node(self):
r = ChartbeatTopNode().render(
Context({'chartbeat_domain': "test.com"}))
self.assertTrue('var _sf_startpt=(new Date()).getTime()' in r, r)
def test_bottom_node(self):
r = ChartbeatBottomNode().render(
Context({'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_no_user_id(self):
self.settings_manager.delete('CHARTBEAT_USER_ID')
self.assertRaises(AnalyticalException, ChartbeatBottomNode)
def test_wrong_user_id(self):
self.settings_manager.set(CHARTBEAT_USER_ID='1234')
self.assertRaises(AnalyticalException, ChartbeatBottomNode)
self.settings_manager.set(CHARTBEAT_USER_ID='123456')
self.assertRaises(AnalyticalException, ChartbeatBottomNode)
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 = ChartbeatBottomNode().render(Context())
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 = ChartbeatBottomNode().render(Context())
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_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'
context = Context({'request': req})
r = ChartbeatBottomNode().render(context)
self.assertTrue(r.startswith(
'<!-- Chartbeat disabled on internal IP address'), r)
self.assertTrue(r.endswith('-->'), r)

View file

@ -1,68 +0,0 @@
"""
Tests for the Clicky template tags and filters.
"""
import re
from django.contrib.auth.models import User
from django.http import HttpRequest
from django.template import Context
from analytical.templatetags.clicky import ClickyNode
from analytical.tests.utils import TagTestCase
from analytical.utils import AnalyticalException
class ClickyTagTestCase(TagTestCase):
"""
Tests for the ``clicky`` template tag.
"""
def setUp(self):
super(ClickyTagTestCase, self).setUp()
self.settings_manager.set(CLICKY_SITE_ID='12345678')
def test_tag(self):
r = self.render_tag('clicky', 'clicky')
self.assertTrue('var clicky_site_id = 12345678;' in r, r)
self.assertTrue('src="http://in.getclicky.com/12345678ns.gif"' in r,
r)
def test_node(self):
r = ClickyNode().render(Context({}))
self.assertTrue('var clicky_site_id = 12345678;' in r, r)
self.assertTrue('src="http://in.getclicky.com/12345678ns.gif"' in r,
r)
def test_no_site_id(self):
self.settings_manager.delete('CLICKY_SITE_ID')
self.assertRaises(AnalyticalException, ClickyNode)
def test_wrong_site_id(self):
self.settings_manager.set(CLICKY_SITE_ID='1234567')
self.assertRaises(AnalyticalException, ClickyNode)
self.settings_manager.set(CLICKY_SITE_ID='123456789')
self.assertRaises(AnalyticalException, ClickyNode)
def test_identify(self):
self.settings_manager.set(ANALYTICAL_AUTO_IDENTIFY=True)
r = ClickyNode().render(Context({'user': User(username='test')}))
self.assertTrue(
'var clicky_custom = {"session": {"username": "test"}};' in r,
r)
def test_custom(self):
r = ClickyNode().render(Context({'clicky_var1': 'val1',
'clicky_var2': 'val2'}))
self.assertTrue(re.search('var clicky_custom = {.*'
'"var1": "val1", "var2": "val2".*};', 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'
context = Context({'request': req})
r = ClickyNode().render(context)
self.assertTrue(r.startswith(
'<!-- Clicky disabled on internal IP address'), r)
self.assertTrue(r.endswith('-->'), r)

View file

@ -1,54 +0,0 @@
"""
Tests for the Crazy Egg template tags and filters.
"""
from django.http import HttpRequest
from django.template import Context
from analytical.templatetags.crazy_egg import CrazyEggNode
from analytical.tests.utils import TagTestCase
from analytical.utils import AnalyticalException
class CrazyEggTagTestCase(TagTestCase):
"""
Tests for the ``crazy_egg`` template tag.
"""
def setUp(self):
super(CrazyEggTagTestCase, self).setUp()
self.settings_manager.set(CRAZY_EGG_ACCOUNT_NUMBER='12345678')
def test_tag(self):
r = self.render_tag('crazy_egg', 'crazy_egg')
self.assertTrue('/1234/5678.js' in r, r)
def test_node(self):
r = CrazyEggNode().render(Context())
self.assertTrue('/1234/5678.js' in r, r)
def test_no_account_number(self):
self.settings_manager.delete('CRAZY_EGG_ACCOUNT_NUMBER')
self.assertRaises(AnalyticalException, CrazyEggNode)
def test_wrong_account_number(self):
self.settings_manager.set(CRAZY_EGG_ACCOUNT_NUMBER='1234567')
self.assertRaises(AnalyticalException, CrazyEggNode)
self.settings_manager.set(CRAZY_EGG_ACCOUNT_NUMBER='123456789')
self.assertRaises(AnalyticalException, CrazyEggNode)
def test_uservars(self):
context = Context({'crazy_egg_var1': 'foo', 'crazy_egg_var2': 'bar'})
r = CrazyEggNode().render(context)
self.assertTrue("CE2.set(1, 'foo');" in r, r)
self.assertTrue("CE2.set(2, 'bar');" 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'
context = Context({'request': req})
r = CrazyEggNode().render(context)
self.assertTrue(r.startswith(
'<!-- Crazy Egg disabled on internal IP address'), r)
self.assertTrue(r.endswith('-->'), r)

View file

@ -1,57 +0,0 @@
"""
Tests for the Google Analytics template tags and filters.
"""
from django.http import HttpRequest
from django.template import Context
from analytical.templatetags.google_analytics import GoogleAnalyticsNode
from analytical.tests.utils import TagTestCase
from analytical.utils import AnalyticalException
class GoogleAnalyticsTagTestCase(TagTestCase):
"""
Tests for the ``google_analytics`` template tag.
"""
def setUp(self):
super(GoogleAnalyticsTagTestCase, self).setUp()
self.settings_manager.set(GOOGLE_ANALYTICS_PROPERTY_ID='UA-123456-7')
def test_tag(self):
r = self.render_tag('google_analytics', 'google_analytics')
self.assertTrue("_gaq.push(['_setAccount', 'UA-123456-7']);" in r, r)
self.assertTrue("_gaq.push(['_trackPageview']);" in r, r)
def test_node(self):
r = GoogleAnalyticsNode().render(Context())
self.assertTrue("_gaq.push(['_setAccount', 'UA-123456-7']);" in r, r)
self.assertTrue("_gaq.push(['_trackPageview']);" in r, r)
def test_no_property_id(self):
self.settings_manager.delete('GOOGLE_ANALYTICS_PROPERTY_ID')
self.assertRaises(AnalyticalException, GoogleAnalyticsNode)
def test_wrong_property_id(self):
self.settings_manager.set(GOOGLE_ANALYTICS_PROPERTY_ID='wrong')
self.assertRaises(AnalyticalException, GoogleAnalyticsNode)
def test_custom_vars(self):
context = Context({'google_analytics_var1': ('test1', 'foo'),
'google_analytics_var5': ('test2', 'bar', 1)})
r = GoogleAnalyticsNode().render(context)
self.assertTrue("_gaq.push(['_setCustomVar', 1, 'test1', 'foo', 3]);"
in r, r)
self.assertTrue("_gaq.push(['_setCustomVar', 5, 'test2', 'bar', 1]);"
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'
context = Context({'request': req})
r = GoogleAnalyticsNode().render(context)
self.assertTrue(r.startswith(
'<!-- Google Analytics disabled on internal IP address'), r)
self.assertTrue(r.endswith('-->'), r)

View file

@ -1,57 +0,0 @@
"""
Tests for the HubSpot template tags and filters.
"""
from django.http import HttpRequest
from django.template import Context
from analytical.templatetags.hubspot import HubSpotNode
from analytical.tests.utils import TagTestCase
from analytical.utils import AnalyticalException
class HubSpotTagTestCase(TagTestCase):
"""
Tests for the ``hubspot`` template tag.
"""
def setUp(self):
super(HubSpotTagTestCase, self).setUp()
self.settings_manager.set(HUBSPOT_PORTAL_ID='1234')
self.settings_manager.set(HUBSPOT_DOMAIN='example.com')
def test_tag(self):
r = self.render_tag('hubspot', 'hubspot')
self.assertTrue('var hs_portalid = 1234;' in r, r)
self.assertTrue('var hs_ppa = "example.com";' in r, r)
def test_node(self):
r = HubSpotNode().render(Context())
self.assertTrue('var hs_portalid = 1234;' in r, r)
self.assertTrue('var hs_ppa = "example.com";' in r, r)
def test_no_portal_id(self):
self.settings_manager.delete('HUBSPOT_PORTAL_ID')
self.assertRaises(AnalyticalException, HubSpotNode)
def test_wrong_portal_id(self):
self.settings_manager.set(HUBSPOT_PORTAL_ID='wrong')
self.assertRaises(AnalyticalException, HubSpotNode)
def test_no_domain(self):
self.settings_manager.delete('HUBSPOT_DOMAIN')
self.assertRaises(AnalyticalException, HubSpotNode)
def test_wrong_domain(self):
self.settings_manager.set(HUBSPOT_DOMAIN='wrong domain')
self.assertRaises(AnalyticalException, HubSpotNode)
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'
context = Context({'request': req})
r = HubSpotNode().render(context)
self.assertTrue(r.startswith(
'<!-- HubSpot disabled on internal IP address'), r)
self.assertTrue(r.endswith('-->'), r)

View file

@ -1,57 +0,0 @@
"""
Tests for the KISSinsights template tags and filters.
"""
from django.contrib.auth.models import User
from django.template import Context
from analytical.templatetags.kiss_insights import KissInsightsNode
from analytical.tests.utils import TagTestCase
from analytical.utils import AnalyticalException
class KissInsightsTagTestCase(TagTestCase):
"""
Tests for the ``kiss_insights`` template tag.
"""
def setUp(self):
super(KissInsightsTagTestCase, self).setUp()
self.settings_manager.set(KISS_INSIGHTS_ACCOUNT_NUMBER='12345')
self.settings_manager.set(KISS_INSIGHTS_SITE_CODE='abc')
def test_tag(self):
r = self.render_tag('kiss_insights', 'kiss_insights')
self.assertTrue("//s3.amazonaws.com/ki.js/12345/abc.js" in r, r)
def test_node(self):
r = KissInsightsNode().render(Context())
self.assertTrue("//s3.amazonaws.com/ki.js/12345/abc.js" in r, r)
def test_no_account_number(self):
self.settings_manager.delete('KISS_INSIGHTS_ACCOUNT_NUMBER')
self.assertRaises(AnalyticalException, KissInsightsNode)
def test_no_site_code(self):
self.settings_manager.delete('KISS_INSIGHTS_SITE_CODE')
self.assertRaises(AnalyticalException, KissInsightsNode)
def test_wrong_account_number(self):
self.settings_manager.set(KISS_INSIGHTS_ACCOUNT_NUMBER='abcde')
self.assertRaises(AnalyticalException, KissInsightsNode)
def test_wrong_site_id(self):
self.settings_manager.set(KISS_INSIGHTS_SITE_CODE='ab')
self.assertRaises(AnalyticalException, KissInsightsNode)
self.settings_manager.set(KISS_INSIGHTS_SITE_CODE='abcd')
self.assertRaises(AnalyticalException, KissInsightsNode)
def test_identify(self):
self.settings_manager.set(ANALYTICAL_AUTO_IDENTIFY=True)
r = KissInsightsNode().render(Context({'user': User(username='test')}))
self.assertTrue("_kiq.push(['identify', 'test']);" in r, r)
def test_show_survey(self):
r = KissInsightsNode().render(
Context({'kiss_insights_show_survey': 1234}))
self.assertTrue("_kiq.push(['showSurvey', 1234]);" in r, r)

View file

@ -1,65 +0,0 @@
"""
Tests for the KISSmetrics tags and filters.
"""
from django.contrib.auth.models import User
from django.http import HttpRequest
from django.template import Context
from analytical.templatetags.kiss_metrics import KissMetricsNode
from analytical.tests.utils import TagTestCase
from analytical.utils import AnalyticalException
class KissMetricsTagTestCase(TagTestCase):
"""
Tests for the ``kiss_metrics`` template tag.
"""
def setUp(self):
super(KissMetricsTagTestCase, self).setUp()
self.settings_manager.set(KISS_METRICS_API_KEY='0123456789abcdef012345'
'6789abcdef01234567')
def test_tag(self):
r = self.render_tag('kiss_metrics', 'kiss_metrics')
self.assertTrue("//doug1izaerwt3.cloudfront.net/0123456789abcdef012345"
"6789abcdef01234567.1.js" in r, r)
def test_node(self):
r = KissMetricsNode().render(Context())
self.assertTrue("//doug1izaerwt3.cloudfront.net/0123456789abcdef012345"
"6789abcdef01234567.1.js" in r, r)
def test_no_api_key(self):
self.settings_manager.delete('KISS_METRICS_API_KEY')
self.assertRaises(AnalyticalException, KissMetricsNode)
def test_wrong_api_key(self):
self.settings_manager.set(KISS_METRICS_API_KEY='0123456789abcdef012345'
'6789abcdef0123456')
self.assertRaises(AnalyticalException, KissMetricsNode)
self.settings_manager.set(KISS_METRICS_API_KEY='0123456789abcdef012345'
'6789abcdef012345678')
self.assertRaises(AnalyticalException, KissMetricsNode)
def test_identify(self):
self.settings_manager.set(ANALYTICAL_AUTO_IDENTIFY=True)
r = KissMetricsNode().render(Context({'user': User(username='test')}))
self.assertTrue("_kmq.push(['identify', 'test']);" in r, r)
def test_event(self):
r = KissMetricsNode().render(Context({'kiss_metrics_event':
('test_event', {'prop1': 'val1', 'prop2': 'val2'})}))
self.assertTrue("_kmq.push(['record', 'test_event', "
'{"prop1": "val1", "prop2": "val2"}]);' 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'
context = Context({'request': req})
r = KissMetricsNode().render(context)
self.assertTrue(r.startswith(
'<!-- KISSmetrics disabled on internal IP address'), r)
self.assertTrue(r.endswith('-->'), r)

View file

@ -1,68 +0,0 @@
"""
Tests for the Mixpanel tags and filters.
"""
from django.contrib.auth.models import User
from django.http import HttpRequest
from django.template import Context
from analytical.templatetags.mixpanel import MixpanelNode
from analytical.tests.utils import TagTestCase
from analytical.utils import AnalyticalException
class MixpanelTagTestCase(TagTestCase):
"""
Tests for the ``mixpanel`` template tag.
"""
def setUp(self):
super(MixpanelTagTestCase, self).setUp()
self.settings_manager.set(OPTIMIZELY_ACCOUNT_NUMBER='1234567')
self.settings_manager.set(
MIXPANEL_TOKEN='0123456789abcdef0123456789abcdef')
def test_tag(self):
r = self.render_tag('mixpanel', 'mixpanel')
self.assertTrue(
"mpq.push(['init', '0123456789abcdef0123456789abcdef']);" in r,
r)
def test_node(self):
r = MixpanelNode().render(Context())
self.assertTrue(
"mpq.push(['init', '0123456789abcdef0123456789abcdef']);" in r,
r)
def test_no_token(self):
self.settings_manager.delete('MIXPANEL_TOKEN')
self.assertRaises(AnalyticalException, MixpanelNode)
def test_wrong_token(self):
self.settings_manager.set(
MIXPANEL_TOKEN='0123456789abcdef0123456789abcde')
self.assertRaises(AnalyticalException, MixpanelNode)
self.settings_manager.set(
MIXPANEL_TOKEN='0123456789abcdef0123456789abcdef0')
self.assertRaises(AnalyticalException, MixpanelNode)
def test_identify(self):
self.settings_manager.set(ANALYTICAL_AUTO_IDENTIFY=True)
r = MixpanelNode().render(Context({'user': User(username='test')}))
self.assertTrue("mpq.push(['identify', 'test']);" in r, r)
def test_event(self):
r = MixpanelNode().render(Context({'mixpanel_event':
('test_event', {'prop1': 'val1', 'prop2': 'val2'})}))
self.assertTrue("mpq.push(['track', 'test_event', "
'{"prop1": "val1", "prop2": "val2"}]);' 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'
context = Context({'request': req})
r = MixpanelNode().render(context)
self.assertTrue(r.startswith(
'<!-- Mixpanel disabled on internal IP address'), r)
self.assertTrue(r.endswith('-->'), r)

View file

@ -1,50 +0,0 @@
"""
Tests for the Optimizely template tags and filters.
"""
from django.http import HttpRequest
from django.template import Context
from analytical.templatetags.optimizely import OptimizelyNode
from analytical.tests.utils import TagTestCase
from analytical.utils import AnalyticalException
class OptimizelyTagTestCase(TagTestCase):
"""
Tests for the ``optimizely`` template tag.
"""
def setUp(self):
super(OptimizelyTagTestCase, self).setUp()
self.settings_manager.set(OPTIMIZELY_ACCOUNT_NUMBER='1234567')
def test_tag(self):
self.assertEqual(
'<script src="//cdn.optimizely.com/js/1234567.js"></script>',
self.render_tag('optimizely', 'optimizely'))
def test_node(self):
self.assertEqual(
'<script src="//cdn.optimizely.com/js/1234567.js"></script>',
OptimizelyNode().render(Context()))
def test_no_account_number(self):
self.settings_manager.delete('OPTIMIZELY_ACCOUNT_NUMBER')
self.assertRaises(AnalyticalException, OptimizelyNode)
def test_wrong_account_number(self):
self.settings_manager.set(OPTIMIZELY_ACCOUNT_NUMBER='123456')
self.assertRaises(AnalyticalException, OptimizelyNode)
self.settings_manager.set(OPTIMIZELY_ACCOUNT_NUMBER='12345678')
self.assertRaises(AnalyticalException, OptimizelyNode)
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'
context = Context({'request': req})
r = OptimizelyNode().render(context)
self.assertTrue(r.startswith(
'<!-- Optimizely disabled on internal IP address'), r)
self.assertTrue(r.endswith('-->'), r)

View file

@ -1,69 +0,0 @@
"""
Tests for the Performable template tags and filters.
"""
from django.http import HttpRequest
from django.template import Context
from analytical.templatetags.performable import PerformableNode
from analytical.tests.utils import TagTestCase
from analytical.utils import AnalyticalException
from django.contrib.auth.models import User
class PerformableTagTestCase(TagTestCase):
"""
Tests for the ``performable`` template tag.
"""
def setUp(self):
super(PerformableTagTestCase, self).setUp()
self.settings_manager.set(PERFORMABLE_API_KEY='123ABC')
def test_tag(self):
r = self.render_tag('performable', 'performable')
self.assertTrue('/performable/pax/123ABC.js' in r, r)
def test_node(self):
r = PerformableNode().render(Context())
self.assertTrue('/performable/pax/123ABC.js' in r, r)
def test_no_api_key(self):
self.settings_manager.delete('PERFORMABLE_API_KEY')
self.assertRaises(AnalyticalException, PerformableNode)
def test_wrong_account_number(self):
self.settings_manager.set(PERFORMABLE_API_KEY='123AB')
self.assertRaises(AnalyticalException, PerformableNode)
self.settings_manager.set(PERFORMABLE_API_KEY='123ABCD')
self.assertRaises(AnalyticalException, PerformableNode)
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'
context = Context({'request': req})
r = PerformableNode().render(context)
self.assertTrue(r.startswith(
'<!-- Performable disabled on internal IP address'), r)
self.assertTrue(r.endswith('-->'), r)
def test_identify(self):
self.settings_manager.set(ANALYTICAL_AUTO_IDENTIFY=True)
r = PerformableNode().render(Context({'user': User(username='test')}))
self.assertTrue('_paq.push(["identify", {identity: "test"}]);' in r, r)
class PerformableEmbedTagTestCase(TagTestCase):
"""
Tests for the ``performable_embed`` template tag.
"""
def test_tag(self):
d = 'example.com'
p = 'test'
r = self.render_tag('performable', 'performable_embed "%s" "%s"'
% (d, p))
self.assertTrue(
"$f.initialize({'host': 'example.com', 'page': 'test'});" in r,
r)

View file

@ -1,51 +0,0 @@
"""
Tests for the analytical.utils module.
"""
from django.http import HttpRequest
from django.template import Context
from analytical.utils import is_internal_ip
from analytical.tests.utils import TestCase
class InternalIpTestCase(TestCase):
def test_render_no_internal_ip(self):
self.settings_manager.set(ANALYTICAL_INTERNAL_IPS=['1.1.1.1'])
context = Context()
self.assertFalse(is_internal_ip(context))
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'
context = Context({'request': req})
self.assertTrue(is_internal_ip(context))
def test_render_prefix_internal_ip(self):
self.settings_manager.set(TEST_INTERNAL_IPS=['1.1.1.1'])
req = HttpRequest()
req.META['REMOTE_ADDR'] = '1.1.1.1'
context = Context({'request': req})
self.assertTrue(is_internal_ip(context, 'TEST'))
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'
context = Context({'request': req})
self.assertTrue(is_internal_ip(context))
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'
context = Context({'request': req})
self.assertTrue(is_internal_ip(context))
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'
context = Context({'request': req})
self.assertFalse(is_internal_ip(context))

View file

@ -1,98 +0,0 @@
"""
Testing utilities.
"""
from django.conf import settings
from django.core.management import call_command
from django.db.models import loading
from django.template import Template, Context, RequestContext
from django.test.simple import run_tests as django_run_tests
from django.test.testcases import TestCase as DjangoTestCase
def run_tests(labels=()):
"""
Use the Django test runner to run the tests.
"""
django_run_tests(labels, verbosity=1, interactive=True)
class TestCase(DjangoTestCase):
"""
Base test case for the django-analytical tests.
Includes the settings manager.
"""
def setUp(self):
self.settings_manager = TestSettingsManager()
def tearDown(self):
self.settings_manager.revert()
class TagTestCase(TestCase):
"""
Tests for a template tag.
Includes the settings manager.
"""
def render_tag(self, library, tag, vars=None, request=None):
if vars is None:
vars = {}
t = Template("{%% load %s %%}{%% %s %%}" % (library, tag))
if request is not None:
context = RequestContext(request, vars)
else:
context = Context(vars)
return t.render(context)
class TestSettingsManager(object):
"""
From: http://www.djangosnippets.org/snippets/1011/
A class which can modify some Django settings temporarily for a
test and then revert them to their original values later.
Automatically handles resyncing the DB if INSTALLED_APPS is
modified.
"""
NO_SETTING = ('!', None)
def __init__(self):
self._original_settings = {}
def set(self, **kwargs):
for k, v in kwargs.iteritems():
self._original_settings.setdefault(k, getattr(settings, k,
self.NO_SETTING))
setattr(settings, k, v)
if 'INSTALLED_APPS' in kwargs:
self.syncdb()
def delete(self, *args):
for k in args:
try:
self._original_settings.setdefault(k, getattr(settings, k,
self.NO_SETTING))
delattr(settings, k)
except AttributeError:
pass # setting did not exist
def syncdb(self):
loading.cache.loaded = False
call_command('syncdb', verbosity=0, interactive=False)
def revert(self):
for k,v in self._original_settings.iteritems():
if v == self.NO_SETTING:
if hasattr(settings, k):
delattr(settings, k)
else:
setattr(settings, k, v)
if 'INSTALLED_APPS' in self._original_settings:
self.syncdb()
self._original_settings = {}

View file

@ -3,25 +3,73 @@ 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-->"
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)
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))
raise AnalyticalException(
"%s setting: %s: '%s'" % (setting, invalid_msg, value)
)
return value
def get_identity(context, prefix=None):
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]
@ -33,19 +81,53 @@ def get_identity(context, prefix=None):
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
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', '')
@ -54,21 +136,26 @@ def is_internal_ip(context, prefix=None):
if not remote_ip:
return False
internal_ips = ''
internal_ips = None
if prefix is not None:
internal_ips = getattr(settings, '%s_INTERNAL_IPS' % prefix, '')
if not internal_ips:
internal_ips = getattr(settings, 'ANALYTICAL_INTERNAL_IPS', '')
if not internal_ips:
internal_ips = getattr(settings, 'INTERNAL_IPS', '')
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
except KeyError, AttributeError:
return remote_ip in (internal_ips or [])
except (KeyError, AttributeError):
return False
def disable_html(html, service):
return HTML_COMMENT % locals()
"""
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):
@ -76,4 +163,5 @@ class AnalyticalException(Exception):
Raised when an exception occurs in any django-analytical code that should
be silenced in templates.
"""
silent_variable_failure = True

View file

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

View file

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

View file

@ -18,7 +18,9 @@ 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::
Example:
.. code-block:: python
ANALYTICAL_INTERNAL_IPS = ['192.168.1.45', '192.168.1.57']
@ -45,7 +47,9 @@ 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::
and is enabled by default. To disable:
.. code-block:: python
ANALYTICAL_AUTO_IDENTIFY = False
@ -64,3 +68,52 @@ and is enabled by default. To disable::
Alternatively, add one of the variables to the context yourself
when you render the template.
Changing the identity
*********************
If you want to override the identity of the logged-in user that the various
providers send you can do it by setting the ``analytical_identity`` context
variable in your view code:
.. code-block:: python
context = RequestContext({'analytical_identity': user.uuid})
return some_template.render(context)
or in the template:
.. code-block:: django
{% with analytical_identity=request.user.uuid|default:None %}
{% analytical_head_top %}
{% endwith %}
or by implementing a context processor, e.g.
.. code-block:: python
# FILE: myproject/context_processors.py
from django.conf import settings
def get_identity(request):
return {
'analytical_identity': 'some-value-here',
}
# FILE: myproject/settings.py
TEMPLATES = [
{
'OPTIONS': {
'context_processors': [
'myproject.context_processors.get_identity',
],
},
},
]
That allows you as a developer to leave your view code untouched and
make sure that the variable is injected for all templates.
If you want to change the identity only for specific provider use the
``*_identity`` context variable, where the ``*`` prefix is the module name
of the specific provider.

View file

@ -10,6 +10,11 @@ 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
@ -18,8 +23,21 @@ increments indicate backwards incompatible changes.
Credits
=======
.. include:: ../AUTHORS.rst
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`_.
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:
@ -27,4 +45,6 @@ Helping out
===========
.. include:: ../README.rst
:start-after: GitHub`_.
:start-after: .. start contribute include
:end-before: .. end contribute include

View file

@ -5,20 +5,20 @@ django-analytical
The django-analytical application integrates analytics services into a
Django_ project.
.. _Django: http://www.djangoproject.com/
.. _Django: https://www.djangoproject.com/
:Package: http://pypi.python.org/pypi/django-analytical/
:Source: http://github.com/jcassee/django-analytical
:Package: https://pypi.org/project/django-analytical/
:Source: https://github.com/jazzband/django-analytical
Overview
========
.. include:: ../README.rst
:start-after: Django_ project.
:end-before: Currently supported services:
:start-after: .. start docs include
:end-before: .. end docs include
To get a feel of how django-analytics works, check out the
To get a feel of how django-analytical works, check out the
:doc:`tutorial`.

View file

@ -12,11 +12,6 @@ configuring the services you use in the project settings.
#. `Adding the template tags to the base template`_
#. `Enabling the services`_
.. note::
The Geckoboard integration differs from that of the other services.
See :doc:`Geckoboard <services/geckoboard>` for installation
instruction if you are not interested in the other services.
.. _installing-the-package:
@ -25,23 +20,29 @@ 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``:
$ easy_install django-analytical
.. code-block:: bash
$ 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:
$ git clone https://github.com/jcassee/django-analytical.git
.. code-block:: bash
.. _PyPI: http://pypi.python.org/pypi/django-analytical/
.. _GitHub: http://github.com/jcassee/django-analytical
$ git clone https://github.com/jazzband/django-analytical.git
Then install the package by running the setup script::
.. _PyPI: https://pypi.org/project/django-analytical/
.. _GitHub: http://github.com/jazzband/django-analytical
$ cd django-analytical
$ python setup.py install
Then install the package by running the setup script:
.. code-block:: bash
$ cd django-analytical
$ python setup.py install
.. _installing-the-application:
@ -51,13 +52,15 @@ Installing the Django application
After you installed 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:
INSTALLED_APPS = [
...
'analytical',
...
]
.. code-block:: python
INSTALLED_APPS = [
...
'analytical',
...
]
.. _adding-the-template-tags:
@ -65,30 +68,32 @@ file of your project::
Adding the template tags to the base template
=============================================
Because every analytics service uses own specific Javascript code that
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::
base template should look like this:
{% load analytical %}
<!DOCTYPE ... >
<html>
<head>
{% analytical_head_top %}
.. code-block:: django
...
{% load analytical %}
<!DOCTYPE ... >
<html>
<head>
{% analytical_head_top %}
{% analytical_head_bottom %}
</head>
<body>
{% analytical_body_top %}
...
...
{% analytical_head_bottom %}
</head>
<body>
{% analytical_body_top %}
{% analytical_body_bottom %}
</body>
</html>
...
{% 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
@ -106,49 +111,101 @@ settings required to enable each service are listed here:
* :doc:`Chartbeat <services/chartbeat>`::
CHARTBEAT_USER_ID = '12345'
CHARTBEAT_USER_ID = '12345'
* :doc:`Clickmap <services/clickmap>`::
CLICKMAP_TRACKER_CODE = '12345678....912'
* :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:`Google Analytics <services/google_analytics>`::
* :doc:`Facebook Pixel <services/facebook_pixel>`::
GOOGLE_ANALYTICS_PROPERTY_ID = 'UA-1234567-8'
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'
* :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'
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'
* :doc:`Mixpanel <services/mixpanel>`::
MIXPANEL_TOKEN = '0123456789abcdef0123456789abcdef'
MIXPANEL_API_TOKEN = '0123456789abcdef0123456789abcdef'
* :doc:`Olark <services/olark>`::
OLARK_SITE_ID = '1234-567-89-0123'
* :doc:`Optimizely <services/optimizely>`::
OPTIMIZELY_ACCOUNT_NUMBER = '1234567'
OPTIMIZELY_ACCOUNT_NUMBER = '1234567'
* :doc:`Performable <services/performable>`::
PERFORMABLE_API_KEY = '123abc'
* :doc:`Rating\@Mail.ru <services/rating_mailru>`::
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-analytics application is now set-up to track visitors. For
information about further configuration and customization, see
:doc:`features`.
The django-analytical application is now set-up to track visitors. For
information about identifying users, further configuration and
customization, see :doc:`features`.

View file

@ -13,13 +13,13 @@ 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/jcassee/django-analytical/issues
.. _`issue tracker`: http://github.com/jazzband/django-analytical/issues
Currently supported services:
.. toctree::
:maxdepth: 1
:glob:
:maxdepth: 1
:glob:
services/*
services/*

View file

@ -70,7 +70,7 @@ contains a line that looks like this::
Here, ``XXXXX`` is your User ID. Set :const:`CHARTBEAT_USER_ID` in the
project :file:`settings.py` file::
CHARTBEAT_SITE_ID = 'XXXXX'
CHARTBEAT_USER_ID = 'XXXXX'
If you do not set a User ID, the tracking code will not be rendered.
@ -96,7 +96,7 @@ important information about detecting the visitor IP address.
Setting the domain
------------------
The Javascript tracking code can send the website domain to Chartbeat.
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`

View file

@ -0,0 +1,78 @@
==================================
Clickmap -- visual click tracking
==================================
`Clickmap`_ is a real-time heatmap tool to track mouse clicks and scroll paths of your website visitors.
Gain intelligence about what's hot and what's not, and optimize your conversion with Clickmap.
.. _`Clickmap`: http://www.clickmap.ch/
.. clickmap-installation:
Installation
============
To start using the Clickmap integration, you must have installed the
django-analytical package and have added the ``analytical`` application
to :const:`INSTALLED_APPS` in your project :file:`settings.py` file.
See :doc:`../install` for details.
Next you need to add the Clickmap template tag to your templates.
This step is only needed if you are not using the generic
:ttag:`analytical.*` tags. If you are, skip to
:ref:`clickmap-configuration`.
The Clickmap JavaScript code is inserted into templates using a template
tag. Load the :mod:`clickmap` template tag library and insert the
:ttag:`clickmap` tag. Because every page that you want to track must
have the tag, it is useful to add it to your base template. Insert
the tag at the bottom of the HTML body::
{% load clickmap %}
...
{% clickmap %}
</body>
</html>
.. _clickmap-configuration:
Configuration
=============
Before you can use the Clickmap integration, you must first set your
Clickmap Tracker ID. If you don't have a Clickmap account yet,
`sign up`_ to get your Tracker ID.
.. _`sign up`: http://www.clickmap.ch/
.. _clickmap-tracker-id:
Setting the Tracker ID
----------------------
Clickmap gives you a unique Tracker ID, and the :ttag:`clickmap`
tag will include it in the rendered JavaScript code. You can find your
Tracker ID clicking the link named "Tracker" in the dashboard
of your Clickmap account. Set :const:`CLICKMAP_TRACKER_ID` in the project
:file:`settings.py` file::
CLICKMAP_TRACKER_ID = 'XXXXXXXX'
If you do not set an Tracker ID, the tracking code will not be
rendered.
.. _clickmap-internal-ips:
Internal IP addresses
---------------------
Usually you do not want to track clicks from your development or
internal IP addresses. By default, if the tags detect that the client
comes from any address in the :const:`ANALYTICAL_INTERNAL_IPS` setting
(which is :const:`INTERNAL_IPS` by default,) the tracking code is
commented out. See :ref:`identifying-visitors` for important information
about detecting the visitor IP address.

View file

@ -20,7 +20,7 @@ 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 tags to your templates. This
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`.
@ -53,7 +53,7 @@ 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.
: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::
@ -84,10 +84,10 @@ 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
: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 your render a template containing the
variables in your view when you render a template containing the
tracking code::
context = RequestContext({'clicky_title': 'A better page title'})
@ -104,7 +104,7 @@ 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 variable listed
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_``.

View file

@ -19,7 +19,7 @@ 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 Crazy Egg template tags to your templates.
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`.
@ -53,7 +53,7 @@ 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
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`

View file

@ -0,0 +1,84 @@
=======================================
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.

93
docs/services/gauges.rst Normal file
View file

@ -0,0 +1,93 @@
=============================
Gaug.es -- Real-time tracking
=============================
Gaug.es_ is an easy way to implement real-time tracking for multiple
websites.
.. _Gaug.es: http://www.gaug.es/
.. gauges-installation:
Installation
============
To start using the Gaug.es integration, you must have installed the
django-analytical package and have added the ``analytical`` application
to :const:`INSTALLED_APPS` in your project :file:`settings.py` file.
See :doc:`../install` for details.
Next you need to add the Gaug.es template tag to your templates.
This step is only needed if you are not using the generic
:ttag:`analytical.*` tags. If you are, skip to
:ref:`gauges-configuration`.
The Gaug.es JavaScript code is inserted into templates using a
template tag. Load the :mod:`gauges` template tag library and
insert the :ttag:`gauges` tag. Because every page that you want to
track must have the tag, it is useful to add it to your base template.
Insert the tag at the top of the HTML head::
{% load gauges %}
<html>
<head>
{% gauges %}
...
.. _gauges-configuration:
Configuration
=============
Before you can use the Gaug.es integration, you must first set your
site id.
.. _gauges-site-id:
Setting the site id
--------------------------
Gaug.es gives you a unique site id, and the :ttag:`gauges`
tag will include it in the rendered JavaScript code. You can find your
site id by clicking the *Tracking Code* link when logged into
the on the gaug.es website. A page will display containing
HTML code looking like this::
<script>
var _gauges = _gauges || [];
(function() {
var t = document.createElement('script');
t.type = 'text/javascript';
t.async = true;
t.id = 'gauges-tracker';
t.setAttribute('data-site-id', 'XXXXXXXXXXXXXXXXXXXXXXX');
t.src = '//secure.gaug.es/track.js';
var s = document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(t, s);
})();
</script>
The code ``XXXXXXXXXXXXXXXXXXXXXXX`` is your site id. Set
:const:`GAUGES_SITE_ID` in the project :file:`settings.py`
file::
GAUGES_SITE_ID = 'XXXXXXXXXXXXXXXXXXXXXXX'
If you do not set an site id, the JavaScript code will not be
rendered.
.. _gauges-internal-ips:
Internal IP addresses
---------------------
Usually you do not want to track clicks from your development or
internal IP addresses. By default, if the tags detect that the client
comes from any address in the :const:`ANALYTICAL_INTERNAL_IPS` setting
(which is :const:`INTERNAL_IPS` by default,) the tracking code is
commented out. See :ref:`identifying-visitors` for important information
about detecting the visitor IP address.

View file

@ -1,242 +0,0 @@
==========================
Geckoboard -- status board
==========================
Geckoboard_ is a hosted, real-time status board serving up indicators
from web analytics, CRM, support, infrastructure, project management,
sales, etc. It can be connected to virtually any source of quantitative
data.
.. _Geckoboard: https://www.geckoboard.com/
.. _geckoboard-installation:
Installation
============
Geckoboard works differently from most other analytics services in that
it pulls measurement data from your website periodically. You will not
have to change anything in your existing templates, and there is no need
to install the ``analytical`` application to use the integration.
Instead you will use custom views to serve the data to Geckoboard custom
widgets.
.. _geckoboard-configuration:
Configuration
=============
If you want to protect the data you send to Geckoboard from access by
others, you can use an API key shared by Geckoboard and your widget
views. Set :const:`GECKOBOARD_API_KEY` in the project
:file:`settings.py` file::
GECKOBOARD_API_KEY = 'XXXXXXXXX'
Provide the API key to the custom widget configuration in Geckoboard.
If you do not set an API key, anyone will be able to view the data by
visiting the widget URL.
Creating custom widgets and charts
==================================
The available custom widgets are described in the Geckoboard support
section, under `Geckoboard API`_. From the perspective of a Django
project, a custom widget is just a view. The django-analytical
application provides view decorators that render the correct response
for the different widgets. When you create a custom widget, enter the
following information:
URL data feed
The view URL.
API key
The content of the :const:`GECKOBOARD_API_KEY` setting, if you have
set it.
Widget type
*Custom*
Feed format
Either *XML* or *JSON*. The view decorators will automatically
detect and output the correct format.
Request type
Either *GET* or *POST*. The view decorators accept both.
Then create a view using one of the decorators from the
:mod:`analytical.geckoboard` module.
.. decorator:: number
Render a *Number & Secondary Stat* widget.
The decorated view must return either a single value, or a list or
tuple with one or two values. The first (or only) value represents
the current value, the second value represents the previous value.
For example, to render a widget that shows the number of active
users and the difference from last week::
from analytical import geckoboard
from datetime import datetime, timedelta
from django.contrib.auth.models import User
@geckoboard.number
def user_count(request):
last_week = datetime.now() - timedelta(weeks=1)
users = User.objects.filter(is_active=True)
last_week_users = users.filter(date_joined__lt=last_week)
return (users.count(), last_week_users.count())
.. decorator:: rag
Render a *RAG Column & Numbers* or *RAG Numbers* widget.
The decorated view must return a tuple or list with three values, or
three tuples (value, text). The values represent numbers shown in
red, amber and green respectively. The text parameter is optional
and will be displayed next to the value in the dashboard. For
example, to render a widget that shows the number of comments that
were approved or deleted by moderators in the last 24 hours::
from analytical import geckoboard
from datetime import datetime, timedelta
from django.contrib.comments.models import Comment, CommentFlag
@geckoboard.rag
def comments(request):
start_time = datetime.now() - timedelta(hours=24)
comments = Comment.objects.filter(submit_date__gt=start_time)
total_count = comments.count()
approved_count = comments.filter(
flags__flag=CommentFlag.MODERATOR_APPROVAL).count()
deleted_count = Comment.objects.filter(
flags__flag=CommentFlag.MODERATOR_DELETION).count()
pending_count = total_count - approved_count - deleted_count
return (
(deleted_count, "Deleted comments"),
(pending_count, "Pending comments"),
(approved_count, "Approved comments"),
)
.. decorator:: text
Render a *Text* widget.
The decorated view must return either a string, a list or tuple of
strings, or a list or tuple of tuples (string, type). The type
parameter tells Geckoboard how to display the text. Use
:const:`TEXT_INFO` for informational messages, :const:`TEXT_WARN`
for warnings and :const:`TEXT_NONE` for plain text (the default).
For example, to render a widget showing the latest Geckoboard
twitter updates::
from analytical import geckoboard
import twitter
@geckoboard.text
def twitter_status(request):
twitter = twitter.Api()
updates = twitter.GetUserTimeline('geckoboard')
return [(u.text, geckoboard.TEXT_NONE) for u in updates]
.. decorator:: pie_chart
Render a *Pie chart* widget.
The decorated view must return a list or tuple of tuples
(value, label, color). The color parameter is a string
``'RRGGBB[TT]'`` representing red, green, blue and optionally
transparency. For example, to render a widget showing the number
of normal, staff and superusers::
from analytical import geckoboard
from django.contrib.auth.models import User
@geckoboard.pie_chart
def user_types(request):
users = User.objects.filter(is_active=True)
total_count = users.count()
superuser_count = users.filter(is_superuser=True).count()
staff_count = users.filter(is_staff=True,
is_superuser=False).count()
normal_count = total_count = superuser_count - staff_count
return [
(normal_count, "Normal users", "ff8800"),
(staff_count, "Staff", "00ff88"),
(superuser_count, "Superusers", "8800ff"),
]
.. decorator:: line_chart
Render a *Line chart* widget.
The decorated view must return a tuple (values, x_axis, y_axis,
color). The value parameter is a tuple or list of data points. The
x-axis parameter is a label string, or a tuple or list of strings,
that will be placed on the X-axis. The y-axis parameter works
similarly for the Y-axis. If there are more axis labels, they are
placed evenly along the axis. The optional color parameter is a
string ``'RRGGBB[TT]'`` representing red, green, blue and optionally
transparency. For example, to render a widget showing the number
of comments per day over the last four weeks (including today)::
from analytical import geckoboard
from datetime import date, timedelta
from django.contrib.comments.models import Comment
@geckoboard.line_chart
def comment_trend(request):
since = date.today() - timedelta(days=29)
days = dict((since + timedelta(days=d), 0)
for d in range(0, 29))
comments = Comment.objects.filter(submit_date=since)
for comment in comments:
days[comment.submit_date.date()] += 1
return (
days.values(),
[days[i] for i in range(0, 29, 7)],
"Comments",
)
.. decorator:: geck_o_meter
Render a *Geck-O-Meter* widget.
The decorated view must return a tuple (value, min, max). The value
parameter represents the current value. The min and max parameters
represent the minimum and maximum value respectively. They are
either a value, or a tuple (value, text). If used, the text
parameter will be displayed next to the minimum or maximum value.
For example, to render a widget showing the number of users that
have logged in in the last 24 hours::
from analytical import geckoboard
from datetime import datetime, timedelta
from django.contrib.auth.models import User
@geckoboard.geck_o_meter
def login_count(request):
since = datetime.now() - timedelta(hours=24)
users = User.objects.filter(is_active=True)
total_count = users.count()
logged_in_count = users.filter(last_login__gt=since).count()
return (logged_in_count, 0, total_count)
.. _`Geckoboard API`: http://geckoboard.zendesk.com/forums/207979-geckoboard-api
----
Thanks go to Geckoboard for their support with the development of this
application.

View file

@ -1,6 +1,6 @@
====================================
Google Analytics -- traffic analysis
====================================
==============================================
Google Analytics (legacy) -- traffic analysis
==============================================
`Google Analytics`_ is the well-known web analytics service from
Google. The product is aimed more at marketers than webmasters or
@ -15,12 +15,12 @@ features.
Installation
============
To start using the Google Analytics integration, you must have installed
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.
Next you need to add the Google Analytics template tags to your
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`.
@ -46,8 +46,9 @@ Configuration
=============
Before you can use the Google Analytics integration, you must first set
your website property ID. You can also add custom segments for Google
Analytics to track.
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:
@ -57,7 +58,7 @@ 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
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::
@ -66,6 +67,66 @@ project :file:`settings.py` file::
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
@ -98,19 +159,19 @@ when your render a template containing the tracking code::
The value of the context variable is a tuple *(name, value, [scope])*.
The scope parameter is one of the
:const:`analytical.google_analytics.SCOPE_*` constants:
:const:`analytical.templatetags.google_analytics.SCOPE_*` constants:
================= ====== =============================================
Constant Value Description
================= ====== =============================================
``SCOPE_VISITOR`` 1 Distinguishes categories of visitors across
multiple sessions.
``SCOPE_SESSION`` 2 Ddistinguishes different visitor experiences
``SCOPE_SESSION`` 2 Distinguishes different visitor experiences
across sessions.
``SCOPE_PAGE`` 3 Defines page-level activity.
================= ====== =============================================
The default scope is :const:`~analytical.google_analytics.SCOPE_PAGE`.
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`::
@ -126,3 +187,83 @@ Just remember that if you set the same context variable in the
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

View file

@ -0,0 +1,168 @@
===============================================
Google Analytics (gtag.js) -- traffic analysis
===============================================
`Google Analytics`_ is the well-known web analytics service from
Google. The product is aimed more at marketers than webmasters or
technologists, supporting integration with AdWords and other e-commence
features. The global site tag (`gtag.js`_) is a JavaScript tagging
framework and API that allows you to send event data to Google Analytics,
Google Ads, and Google Marketing Platform.
.. _`Google Analytics`: http://www.google.com/analytics/
.. _`gtag.js`: https://developers.google.com/analytics/devguides/collection/gtagjs/
.. google-analytics-installation:
Installation
============
To start using the Google Analytics integration, you must have installed
the django-analytical package and have added the ``analytical``
application to :const:`INSTALLED_APPS` in your project
:file:`settings.py` file. See :doc:`../install` for details.
Next you need to add the Google Analytics template tag to your
templates. This step is only needed if you are not using the generic
:ttag:`analytical.*` tags. If you are, skip to
:ref:`google-analytics-configuration-gtag`.
The Google Analytics tracking code is inserted into templates using a
template tag. Load the :mod:`google_analytics_gtag` template tag library and
insert the :ttag:`google_analytics_gtag` tag. Because every page that you
want to track must have the tag, it is useful to add it to your base
template. Insert the tag at the bottom of the HTML head::
{% load google_analytics_gtag %}
<html>
<head>
{% google_analytics_gtag %}
...
</head>
...
.. _google-analytics-configuration-gtag:
Configuration
=============
Before you can use the Google Analytics integration, you must first set
your website property ID. If you track multiple domains with the same
code, you also need to set-up the domain. Finally, you can add custom
segments for Google Analytics to track.
.. _google-analytics-gtag-property-id:
Setting the property ID
-----------------------
Every website you track with Google Analytics gets its own property ID,
and the :ttag:`google_analytics_gtag` tag will include it in the rendered
JavaScript code. You can find the web property ID on the overview page
of your account. Set :const:`GOOGLE_ANALYTICS_GTAG_PROPERTY_ID` in the
project :file:`settings.py` file::
GOOGLE_ANALYTICS_GTAG_PROPERTY_ID = 'UA-XXXXXX-X'
If you do not set a property ID, the tracking code will not be rendered.
Please note that the accepted Property IDs should be one of the following formats:
- 'UA-XXXXXX-Y'
- 'AW-XXXXXXXXXX'
- 'G-XXXXXXXX'
- 'DC-XXXXXXXX'
Internal IP addresses
---------------------
Usually you do not want to track clicks from your development or
internal IP addresses. By default, if the tags detect that the client
comes from any address in the :const:`GOOGLE_ANALYTICS_INTERNAL_IPS`
setting, the tracking code is commented out. It takes the value of
:const:`ANALYTICAL_INTERNAL_IPS` by default (which in turn is
:const:`INTERNAL_IPS` by default). See :ref:`identifying-visitors` for
important information about detecting the visitor IP address.
.. _google-analytics-identify-user:
Identifying authenticated users
-------------------------------
The username of an authenticated user is passed to Google Analytics
automatically as the ``user_id``. See :ref:`identifying-visitors`.
According to `Google Analytics conditions`_ you should avoid
sending Personally Identifiable Information.
Using ``username`` as ``user_id`` might not be the best option.
To avoid that, you can change the identity
by setting ``google_analytics_gtag_identity`` (or ``analytical_identity`` to
affect all providers) context variable:
.. code-block:: python
context = RequestContext({'google_analytics_gtag_identity': user.uuid})
return some_template.render(context)
or in the template:
.. code-block:: django
{% with google_analytics_gtag_identity=request.user.uuid|default:None %}
{% analytical_head_top %}
{% endwith %}
.. _`Google Analytics conditions`: https://developers.google.com/analytics/solutions/crm-integration#user_id
.. _google-analytics-custom-dimensions:
Custom dimensions
----------------
As described in the Google Analytics `custom dimensions`_ documentation
page, you can define custom dimensions which are variables specific to your
business needs. These variables can include both custom event parameters as
well as customer user properties. Using the template context variable
``google_analytics_custom_dimensions``, you can let the :ttag:`google_analytics_gtag`
pass custom dimensions to Google Analytics automatically. The ``google_analytics_custom_dimensions``
variable must be set to a dictionary where the keys are the dimension names
and the values are the dimension values. You can set the context variable in your
view when you render a template containing the tracking code::
.. code-block:: python
context = RequestContext({
'google_analytics_custom_dimensions': {
'gender': 'female',
'country': 'US',
'user_properties': {
'age': 25
}
}
})
return some_template.render(context)
Note that the ``user_properties`` key is used to pass user properties to Google
Analytics. It's not necessary to always use this key, but that'd be the way of
sending user properties to Google Analytics automatically.
You may want to set custom dimensions in a context processor that you add
to the :data:`TEMPLATE_CONTEXT_PROCESSORS` list in :file:`settings.py`::
.. code-block:: python
def google_analytics_segment_language(request):
try:
return {'google_analytics_custom_dimensions': {'language': request.LANGUAGE_CODE}}
except AttributeError:
return {}
Just remember that if you set the same context variable in the
:class:`~django.template.context.RequestContext` constructor and in a
context processor, the latter clobbers the former.
.. _`custom dimensions`: https://support.google.com/analytics/answer/10075209

View file

@ -0,0 +1,238 @@
====================================================
Google Analytics (analytics.js) -- traffic analysis
====================================================
`Google Analytics`_ is the well-known web analytics service from
Google. The product is aimed more at marketers than webmasters or
technologists, supporting integration with AdWords and other e-commence
features. The `analytics.js`_ library (also known as "the Google
Analytics tag") is a JavaScript library for measuring how users interact
with your website.
.. _`Google Analytics`: http://www.google.com/analytics/
.. _`analytics.js`: https://developers.google.com/analytics/devguides/collection/analyticsjs/
.. google-analytics-installation:
Installation
============
To start using the Google Analytics integration, you must have installed
the django-analytical package and have added the ``analytical``
application to :const:`INSTALLED_APPS` in your project
:file:`settings.py` file. See :doc:`../install` for details.
Next you need to add the Google Analytics template tag to your
templates. This step is only needed if you are not using the generic
:ttag:`analytical.*` tags. If you are, skip to
:ref:`google-analytics-configuration-js`.
The Google Analytics tracking code is inserted into templates using a
template tag. Load the :mod:`google_analytics_js` template tag library and
insert the :ttag:`google_analytics_js` tag. Because every page that you
want to track must have the tag, it is useful to add it to your base
template. Insert the tag at the bottom of the HTML head::
{% load google_analytics_js %}
<html>
<head>
...
{% google_analytics_js %}
</head>
...
.. _google-analytics-configuration-js:
Configuration
=============
Before you can use the Google Analytics integration, you must first set
your website property ID. If you track multiple domains with the same
code, you also need to set-up the domain. Finally, you can add custom
segments for Google Analytics to track.
.. _google-analytics-js-property-id:
Setting the property ID
-----------------------
Every website you track with Google Analytics gets its own property ID,
and the :ttag:`google_analytics_js` tag will include it in the rendered
JavaScript code. You can find the web property ID on the overview page
of your account. Set :const:`GOOGLE_ANALYTICS_JS_PROPERTY_ID` in the
project :file:`settings.py` file::
GOOGLE_ANALYTICS_JS_PROPERTY_ID = 'UA-XXXXXXXX-X'
If you do not set a property ID, the tracking code will not be rendered.
Tracking multiple domains
-------------------------
The default code is suitable for tracking a single domain. If you track
multiple domains, set the :const:`GOOGLE_ANALYTICS_TRACKING_STYLE`
setting to one of the :const:`analytical.templatetags.google_analytics_js.TRACK_*`
constants:
============================= ===== =============================================
Constant Value Description
============================= ===== =============================================
``TRACK_SINGLE_DOMAIN`` 1 Track one domain.
``TRACK_MULTIPLE_SUBDOMAINS`` 2 Track multiple subdomains of the same top
domain (e.g. `fr.example.com` and
`nl.example.com`).
``TRACK_MULTIPLE_DOMAINS`` 3 Track multiple top domains (e.g. `example.fr`
and `example.nl`).
============================= ===== =============================================
As noted, the default tracking style is
:const:`~analytical.templatetags.google_analytics_js.TRACK_SINGLE_DOMAIN`.
When you track multiple (sub)domains, django-analytical needs to know
what domain name to pass to Google Analytics. If you use the contrib
sites app, the domain is automatically picked up from the current
:const:`~django.contrib.sites.models.Site` instance. Otherwise, you may
either pass the domain to the template tag through the context variable
:const:`google_analytics_domain` (fallback: :const:`analytical_domain`)
or set it in the project :file:`settings.py` file using
:const:`GOOGLE_ANALYTICS_DOMAIN` (fallback: :const:`ANALYTICAL_DOMAIN`).
Display Advertising
-------------------
Display Advertising allows you to view Demographics and Interests reports,
add Remarketing Lists and support DoubleClick Campain Manager integration.
You can enable `Display Advertising features`_ by setting the
:const:`GOOGLE_ANALYTICS_DISPLAY_ADVERTISING` configuration setting::
GOOGLE_ANALYTICS_DISPLAY_ADVERTISING = True
By default, display advertising features are disabled.
.. _`Display Advertising features`: https://support.google.com/analytics/answer/3450482
.. _google-analytics-js-internal-ips:
Internal IP addresses
---------------------
Usually you do not want to track clicks from your development or
internal IP addresses. By default, if the tags detect that the client
comes from any address in the :const:`GOOGLE_ANALYTICS_INTERNAL_IPS`
setting, the tracking code is commented out. It takes the value of
:const:`ANALYTICAL_INTERNAL_IPS` by default (which in turn is
:const:`INTERNAL_IPS` by default). See :ref:`identifying-visitors` for
important information about detecting the visitor IP address.
.. _google-analytics-js-custom-variables:
Custom variables
----------------
As described in the Google Analytics `custom variables`_ documentation
page, you can define custom segments. Using template context variables
``google_analytics_var1`` through ``google_analytics_var5``, you can let
the :ttag:`google_analytics_js` tag pass custom variables to Google
Analytics automatically. You can set the context variables in your view
when your render a template containing the tracking code::
context = RequestContext({'google_analytics_var1': ('gender', 'female'),
'google_analytics_var2': ('visit', 1)})
return some_template.render(context)
The value of the context variable is a tuple *(name, value)*.
You may want to set custom variables in a context processor that you add
to the :data:`TEMPLATE_CONTEXT_PROCESSORS` list in :file:`settings.py`::
def google_analytics_segment_language(request):
try:
return {'google_analytics_var3': request.LANGUAGE_CODE}
except AttributeError:
return {}
Just remember that if you set the same context variable in the
:class:`~django.template.context.RequestContext` constructor and in a
context processor, the latter clobbers the former.
.. _`custom variables`: https://developers.google.com/analytics/devguides/collection/upgrade/reference/gajs-analyticsjs#custom-vars
.. _google-analytics-js-anonimyze-ips:
Anonymize IPs
-------------
You can enable the `IP anonymization`_ feature by setting the
:const:`GOOGLE_ANALYTICS_ANONYMIZE_IP` configuration setting::
GOOGLE_ANALYTICS_ANONYMIZE_IP = True
This may be mandatory for deployments in countries that have a firm policies
concerning data privacy (e.g. Germany).
By default, IPs are not anonymized.
.. _`IP anonymization`: https://support.google.com/analytics/bin/answer.py?hl=en&answer=2763052
.. _google-analytics-js-sample-rate:
Sample Rate
-----------
You can configure the `Sample Rate`_ feature by setting the
:const:`GOOGLE_ANALYTICS_SAMPLE_RATE` configuration setting::
GOOGLE_ANALYTICS_SAMPLE_RATE = 10
The value is a percentage and can be between 0 and 100 and can be a string or
integer value.
.. _`Sample Rate`: https://developers.google.com/analytics/devguides/collection/analyticsjs/field-reference#sampleRate
.. _google-analytics-js-site-speed-sample-rate:
Site Speed Sample Rate
----------------------
You can configure the `Site Speed Sample Rate`_ feature by setting the
:const:`GOOGLE_ANALYTICS_SITE_SPEED_SAMPLE_RATE` configuration setting::
GOOGLE_ANALYTICS_SITE_SPEED_SAMPLE_RATE = 10
The value is a percentage and can be between 0 and 100 and can be a string or
integer value.
.. _`Site Speed Sample Rate`: https://developers.google.com/analytics/devguides/collection/analyticsjs/field-reference#siteSpeedSampleRate
.. _google-analytics-cookie-expiration:
Cookie Expiration
-----------------
You can configure the `Cookie Expiration`_ feature by setting the
:const:`GOOGLE_ANALYTICS_COOKIE_EXPIRATION` configuration setting::
GOOGLE_ANALYTICS_COOKIE_EXPIRATION = 3600000
The value is the cookie expiration in seconds or 0 to delete the cookie when the browser is closed.
.. _`Cookie Expiration`: https://developers.google.com/analytics/devguides/collection/gajs/methods/gaJSApiBasicConfiguration#_setsessioncookietimeout
Custom JavaScript Source
------------------------
You can configure a custom URL for the javascript file by setting the
:const:`GOOGLE_ANALYTICS_JS_SOURCE` configuration setting::
GOOGLE_ANALYTICS_JS_SOURCE = 'https://www.example.com/analytics.js'

View file

@ -0,0 +1,99 @@
===============================
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.

59
docs/services/heap.rst Normal file
View file

@ -0,0 +1,59 @@
=====================================
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.

73
docs/services/hotjar.rst Normal file
View file

@ -0,0 +1,73 @@
=====================================
Hotjar -- analytics and user feedback
=====================================
`Hotjar`_ is a website analytics and user feedback tool.
.. _`Hotjar`: https://www.hotjar.com/
.. hotjar-installation:
Installation
============
To start using the Hotjar integration, you must have installed the
django-analytical package and have added the ``analytical`` application
to :const:`INSTALLED_APPS` in your project :file:`settings.py` file.
See :doc:`../install` for details.
Next you need to add the Hotjar template tag to your templates.
This step is only needed if you are not using the generic
:ttag:`analytical.*` tags. If you are, skip to
:ref:`hotjar-configuration`.
The Hotjar code is inserted into templates using template tags.
Because every page that you want to track must have the tag,
it is useful to add it to your base template.
At the top of the template, load the :mod:`hotjar` template tag library.
Then insert the :ttag:`hotjar` tag at the bottom of the head section::
{% load hotjar %}
<html>
<head>
...
{% hotjar %}
</head>
...
</html>
.. _hotjar-configuration:
Configuration
=============
Before you can use the Hotjar integration, you must first set your Site ID.
.. _hotjar-id:
Setting the Hotjar Site ID
--------------------------
You can find the Hotjar Site ID in the "Sites & Organizations" section of your Hotjar account.
Set :const:`HOTJAR_SITE_ID` in the project :file:`settings.py` file::
HOTJAR_SITE_ID = 'XXXXXXXXX'
If you do not set a Hotjar Site ID, the code will not be rendered.
.. _hotjar-internal-ips:
Internal IP addresses
---------------------
Usually you do not want to track clicks from your development or
internal IP addresses. By default, if the tags detect that the client
comes from any address in the :const:`HOTJAR_INTERNAL_IPS`
setting, the tracking code is commented out. It takes the value of
:const:`ANALYTICAL_INTERNAL_IPS` by default (which in turn is
:const:`INTERNAL_IPS` by default). See :ref:`identifying-visitors` for
important information about detecting the visitor IP address.

View file

@ -3,7 +3,7 @@ HubSpot -- inbound marketing
============================
HubSpot_ helps you get found by customers. It provides tools for
content creation, convertion and marketing analysis. HubSpot uses
content creation, conversion and marketing analysis. HubSpot uses
tracking on your website to measure effect of your marketing efforts.
.. _HubSpot: http://www.hubspot.com/
@ -19,7 +19,7 @@ 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 tags to your templates. This
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`.
@ -43,29 +43,28 @@ Configuration
=============
Before you can use the HubSpot integration, you must first set your
portal ID and domain.
portal ID, also known as your Hub ID.
.. _hubspot-portal-id:
Setting the portal ID and domain
--------------------------------
Setting the portal ID
---------------------
Your HubSpot account has its own portal ID and primary domain name, and
the :ttag:`hubspot` tag will include them in the rendered Javascript
code. You can find the portal ID and domain by going to the *Domains*
tab in your HubSpot account. The domain you need to use is listed as
*Primary Domain* on that page, and the portal ID can be found in the
footer. Set :const:`HUBSPOT_PORTAL_ID` and :const:`HUBSPOT_DOMAIN` in
the project :file:`settings.py` file::
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'
HUBSPOT_DOMAIN = 'XXXXXXXX.web101.hubspot.com'
If you do not set the portal ID and domain, the tracking code will not
be rendered.
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

166
docs/services/intercom.rst Normal file
View file

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

View file

@ -19,7 +19,7 @@ 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 tags to your templates.
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`.
@ -28,7 +28,7 @@ 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 bottom of the HTML body::
the tag at the top of the HTML body::
{% load kiss_insights %}
...
@ -54,10 +54,10 @@ Setting the account number and 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
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
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

View file

@ -9,7 +9,6 @@ many drop out at each stage.
.. _KISSmetrics: http://www.kissmetrics.com/
.. kiss-metrics-installation:
Installation
@ -20,12 +19,12 @@ 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 tags to your templates.
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
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
@ -54,7 +53,7 @@ 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
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::
@ -108,3 +107,65 @@ a context processor that you add to the
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

View file

@ -0,0 +1,75 @@
==================================================
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.

182
docs/services/matomo.rst Normal file
View file

@ -0,0 +1,182 @@
====================================================
Matomo (formerly Piwik) -- open source web analytics
====================================================
Matomo_ is an open analytics platform currently used by individuals,
companies and governments all over the world.
.. _Matomo: http://matomo.org/
Installation
============
To start using the Matomo integration, you must have installed the
django-analytical package and have added the ``analytical`` application
to :const:`INSTALLED_APPS` in your project :file:`settings.py` file.
See :doc:`../install` for details.
Next you need to add the Matomo template tag to your templates. This
step is only needed if you are not using the generic
:ttag:`analytical.*` tags. If you are, skip to
:ref:`matomo-configuration`.
The Matomo tracking code is inserted into templates using a template
tag. Load the :mod:`matomo` template tag library and insert the
:ttag:`matomo` tag. Because every page that you want to track must
have the tag, it is useful to add it to your base template. Insert
the tag at the bottom of the HTML body as recommended by the
`Matomo best practice for Integration Plugins`_::
{% load matomo %}
...
{% matomo %}
</body>
</html>
.. _`Matomo best practice for Integration Plugins`: http://matomo.org/integrate/how-to/
.. _matomo-configuration:
Configuration
=============
Before you can use the Matomo integration, you must first define
domain name and optional URI path to your Matomo server, as well as
the Matomo ID of the website you're tracking with your Matomo server,
in your project settings.
Setting the domain
------------------
Your Django project needs to know where your Matomo server is located.
Typically, you'll have Matomo installed on a subdomain of its own
(e.g. ``matomo.example.com``), otherwise it runs in a subdirectory of
a website of yours (e.g. ``www.example.com/matomo``). Set
:const:`MATOMO_DOMAIN_PATH` in the project :file:`settings.py` file
accordingly::
MATOMO_DOMAIN_PATH = 'matomo.example.com'
If you do not set a domain the tracking code will not be rendered.
Setting the site ID
-------------------
Your Matomo server can track several websites. Each website has its
site ID (this is the ``idSite`` parameter in the query string of your
browser's address bar when you visit the Matomo Dashboard). Set
:const:`MATOMO_SITE_ID` in the project :file:`settings.py` file to
the value corresponding to the website you're tracking::
MATOMO_SITE_ID = '4'
If you do not set the site ID the tracking code will not be rendered.
.. _matomo-uservars:
User variables
--------------
Matomo supports sending `custom variables`_ along with default statistics. If
you want to set a custom variable, use the context variable ``matomo_vars`` when
you render your template. It should be an iterable of custom variables
represented by tuples like: ``(index, name, value[, scope])``, where scope may
be ``'page'`` (default) or ``'visit'``. ``index`` should be an integer and the
other parameters should be strings. ::
context = Context({
'matomo_vars': [(1, 'foo', 'Sir Lancelot of Camelot'),
(2, 'bar', 'To seek the Holy Grail', 'page'),
(3, 'spam', 'Blue', 'visit')]
})
return some_template.render(context)
Matomo default settings allow up to 5 custom variables for both scope. Variable
mapping between index and name must stay constant, or the latest name
override the previous one.
If you use the same user variables in different views and its value can
be computed from the HTTP request, you can also set them in a context
processor that you add to the :data:`TEMPLATE_CONTEXT_PROCESSORS` list
in :file:`settings.py`.
.. _`custom variables`: http://developer.matomo.org/guides/tracking-javascript-guide#custom-variables
.. _matomo-user-tracking:
User tracking
-------------
If you use the standard Django authentication system, you can allow Matomo to
`track individual users`_ by setting the :data:`ANALYTICAL_AUTO_IDENTIFY`
setting to :const:`True`. This is enabled by default. Matomo will identify
users based on their ``username``.
If you disable this settings, or want to customize what user id to use, you can
set the context variable ``analytical_identity`` (for global configuration) or
``matomo_identity`` (for Matomo specific configuration). Setting one to
:const:`None` will disable the user tracking feature::
# Matomo will identify this user as 'BDFL' if ANALYTICAL_AUTO_IDENTIFY is True or unset
request.user = User(username='BDFL', first_name='Guido', last_name='van Rossum')
# Matomo will identify this user as 'Guido van Rossum'
request.user = User(username='BDFL', first_name='Guido', last_name='van Rossum')
context = Context({
'matomo_identity': request.user.get_full_name()
})
# Matomo will not identify this user (but will still collect statistics)
request.user = User(username='BDFL', first_name='Guido', last_name='van Rossum')
context = Context({
'matomo_identity': None
})
.. _`track individual users`: http://developer.matomo.org/guides/tracking-javascript-guide#user-id
Disabling cookies
-----------------
If you want to `disable cookies`_, set :data:`MATOMO_DISABLE_COOKIES` to
:const:`True`. This is disabled by default.
.. _`disable cookies`: https://matomo.org/faq/general/faq_157/
Ask for consent
---------------
If you do not want to track visitors without permission, you can `ask for consent`_ first.
To enable this, set :data:`MATOMO_ASK_FOR_CONSENT` to :const:`True`.
By default, no consent for tracking is needed (i.e. :const:`False`).
To give and remove consent in your page, create DOM elements with the following classes:
`matomo_give_consent` - class name for element to click when visitors want to **give** consent
`matomo_remove_consent` - class name for element to click when visitors want to **remove** consent
Examples::
# button to allow tracking
<button class="matomo_give_consent">Track me!</button>
# button to remove tracking consent
<button class="matomo_remove_consent">Don't track me anymore!</button>
.. _`asking for consent`: https://developer.matomo.org/guides/tracking-javascript-guide#asking-for-consent
Internal IP addresses
---------------------
Usually, you do not want to track clicks from your development or
internal IP addresses. By default, if the tags detect that the client
comes from any address in the :const:`ANALYTICAL_INTERNAL_IPS` (which
takes the value of :const:`INTERNAL_IPS` by default) the tracking code
is commented out. See :ref:`identifying-visitors` for important
information about detecting the visitor IP address.

View file

@ -19,12 +19,12 @@ 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 tags to your templates. This
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
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
@ -53,11 +53,12 @@ Setting the token
-----------------
Every website you track events for with Mixpanel gets its own token,
and the :ttag:`mixpanel` tag will include it in the rendered Javascript
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_TOKEN` in the project :file:`settings.py` file::
Set :const:`MIXPANEL_API_TOKEN` in the project :file:`settings.py`
file::
MIXPANEL_TOKEN = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
MIXPANEL_API_TOKEN = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
If you do not set a token, the tracking code will not be rendered.
@ -108,23 +109,44 @@ 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
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 `django-celery`_ package.
can use Wes Winham's `mixpanel-celery`_ package.
If you want to track an event in Javascript, use the asynchronous
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
`"Asynchronous Tracking with JavaScript"`_ in the Mixpanel
documentation. For example::
mpq.push(["track", "play-game", {"level": "12", "weapon": "sword", "character": "knight"}]);
.. _django-celery: http://github.com/winhamwr/mixpanel-celery
.. _`"Asynchronous Tracking with Javascript"`: http://mixpanel.com/api/docs/guides/integration/js#async
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

229
docs/services/olark.rst Normal file
View file

@ -0,0 +1,229 @@
=====================
Olark -- visitor chat
=====================
Olark_ is a lightweight tool to chat with visitors to your website using
your existing instant messaging client. Chat with your website visitors
while they browse, using your mobile device or instant messenger. Olark
is fully customizable, supports multiple operators and keeps chat
transcripts.
.. _Olark: http://www.olark.com/
Installation
============
To start using the Olark 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 Olark 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:`olark-configuration`.
The Olark JavaScript code is inserted into templates using a template
tag. Load the :mod:`olark` template tag library and insert the
:ttag:`olark` 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 olark %}
...
{% olark %}
</body>
</html>
.. _olark-configuration:
Configuration
=============
Before you can use the Olark integration, you must first set your site
ID. You can customize the visitor nickname and add information to their
status in the operator buddy list, and customize the text used in the
chat window.
Setting the site ID
-------------------
In order to install the chat code, you need to set your Olark site ID.
The :ttag:`olark` tag will include it in the rendered JavaScript code.
You can find the site ID on `installation page`_ of you Olark account.
Set :const:`OLARK_SITE_ID` in the project :file:`settings.py` file::
OLARK_SITE_ID = 'XXXX-XXX-XX-XXXX'
If you do not set the site ID, the chat window will not be rendered.
.. _`installation page`: https://www.olark.com/install
Setting the visitor nickname
----------------------------
If your website identifies visitors, you can use that to set their
nickname in the operator buddy list. By default, the name and username
of an authenticated user are automatically used to set the nickname.
See :ref:`identifying-visitors`.
You can also set the visitor nickname yourself by adding either the
``olark_nickname`` (alias: ``olark_identity``) or the
``analytical_identity`` variable to the template context. If both
variables are set, the former takes precedence. For example::
context = RequestContext({'olark_nickname': nick})
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 set_olark_nickname(request):
try:
return {'olark_nickname': 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.
See also `api.chat.updateVisitorNickname`_ in the Olark JavaScript API
documentation.
.. _`api.chat.updateVisitorNickname`: http://www.olark.com/documentation/javascript/api.chat.updateVisitorNickname
Adding status information
-------------------------
If you want to send more information about the visitor to the operators,
you can add text snippets to the status field in the buddy list. Set
the ``olark_status`` context variable to a string or a list of strings
and the :ttag:`olark` tag will pass them to Olark as status messages::
context = RequestContext({'olark_status': [
'has %d items in cart' % cart.item_count,
'value of items is $%0.2f' % cart.total_value,
]})
return some_template.render(context)
See also `api.chat.updateVisitorStatus`_ in the Olark JavaScript API
documentation.
.. _`api.chat.updateVisitorStatus`: http://www.olark.com/documentation/javascript/api.chat.updateVisitorStatus
Customizing the chat window messages
------------------------------------
Olark lets you customize the appearance of the Chat window by changing
location, colors and messages text. While you can configure these on
the Olark website, sometimes one set of messages is not enough. For
example, if you want to localize your website, you want to address every
visitor in their own language. Olark allows you to set the messages on
a per-page basis, and the :ttag:`olark` tag supports this feature by way
of the following context variables:
========================================== =============================
Context variable Example message
========================================== =============================
``olark_welcome_title`` Click to Chat
------------------------------------------ -----------------------------
``olark_chatting_title`` Live Help: Now Chatting
------------------------------------------ -----------------------------
``olark_unavailable_title`` Live Help: Offline
------------------------------------------ -----------------------------
``olark_busy_title`` Live Help: Busy
------------------------------------------ -----------------------------
``olark_away_message`` Our live support feature is
currently offline, Please
try again later.
------------------------------------------ -----------------------------
``olark_loading_title`` Loading Olark...
------------------------------------------ -----------------------------
``olark_welcome_message`` Welcome to my website. You
can use this chat window to
chat with me.
------------------------------------------ -----------------------------
``olark_busy_message`` All of our representatives
are with other customers at
this time. We will be with
you shortly.
------------------------------------------ -----------------------------
``olark_chat_input_text`` Type here and hit to chat
------------------------------------------ -----------------------------
``olark_name_input_text`` and type your Name
------------------------------------------ -----------------------------
``olark_email_input_text`` and type your Email
------------------------------------------ -----------------------------
``olark_offline_note_message`` We are offline, send us a
message
------------------------------------------ -----------------------------
``olark_send_button_text`` Send
------------------------------------------ -----------------------------
``olark_offline_note_thankyou_text`` Thank you for your message.
We will get back to you as
soon as we can.
------------------------------------------ -----------------------------
``olark_offline_note_error_text`` You must complete all fields
and specify a valid email
address
------------------------------------------ -----------------------------
``olark_offline_note_sending_text`` Sending...
------------------------------------------ -----------------------------
``olark_operator_is_typing_text`` is typing...
------------------------------------------ -----------------------------
``olark_operator_has_stopped_typing_text`` has stopped typing
------------------------------------------ -----------------------------
``olark_introduction_error_text`` Please leave a name and email
address so we can contact you
in case we get disconnected
------------------------------------------ -----------------------------
``olark_introduction_messages`` Welcome, just fill out some
brief information and click
'Start chat' to talk to us
------------------------------------------ -----------------------------
``olark_introduction_submit_button_text`` Start chat
========================================== =============================
As an example, you could set the texts site-wide base on the current
language using a context processor that you add to the
:data:`TEMPLATE_CONTEXT_PROCESSORS` list in :file:`settings.py`::
OLARK_TEXTS = {
'en': {
'welcome title': "Click for Live Help",
'chatting_title': "Live Help: Now chatting",
...
},
'nl': {
'welcome title': "Klik voor online hulp",
'chatting_title': "Online hulp: in gesprek",
...
},
...
}
def set_olark_texts(request):
lang = request.LANGUAGE_CODE.split('-', 1)[0]
texts = OLARK_TEXTS.get(lang)
if texts is None:
texts = OLARK_TEXTS.get('en')
return dict(('olark_%s' % k, v) for k, v in texts.items())
See also the Olark blog post on `supporting multiple languages`_.
.. _`supporting multiple languages`: http://www.olark.com/blog/2010/olark-in-your-favorite-language/
----
Thanks go to Olark for their support with the development of this
application.

View file

@ -20,12 +20,12 @@ 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 Optimizely template tags to your templates.
Next you need to add the Optimizely 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:`optimizely-configuration`.
The Optimizely Javascript code is inserted into templates using a
The Optimizely JavaScript code is inserted into templates using a
template tag. Load the :mod:`optimizely` template tag library and
insert the :ttag:`optimizely` tag. Because every page that you want to
track must have the tag, it is useful to add it to your base template.
@ -53,7 +53,7 @@ Setting the account number
--------------------------
Optimizely gives you a unique account number, and the :ttag:`optimizely`
tag will include it in the rendered Javascript code. You can find your
tag will include it in the rendered JavaScript code. You can find your
account number by clicking the *Implementation* link in the top
right-hand corner of the Optimizely website. A pop-up window will
appear containing HTML code looking like this::
@ -66,7 +66,7 @@ file::
OPTIMIZELY_ACCOUNT_NUMBER = 'XXXXXXX'
If you do not set an account number, the Javascript code will not be
If you do not set an account number, the JavaScript code will not be
rendered.

View file

@ -20,12 +20,12 @@ 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 Performable template tags to your templates.
Next you need to add the Performable 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:`performable-configuration`.
The Performable Javascript code is inserted into templates using a
The Performable JavaScript code is inserted into templates using a
template tag. Load the :mod:`performable` template tag library and
insert the :ttag:`performable` tag. Because every page that you want to
track must have the tag, it is useful to add it to your base template.
@ -53,14 +53,14 @@ Setting the API key
-------------------
You Performable account has its own API key, which :ttag:`performable`
tag will include it in the rendered Javascript code. You can find your
tag will include it in the rendered JavaScript code. You can find your
API key on the *Account Settings* page (click 'Account Settings' in the
top right-hand corner of your Performable dashboard). Set
:const:`PERFORMABLE_API_KEY` in the project :file:`settings.py` file::
PERFORMABLE_API_KEY = 'XXXXXX'
If you do not set an API key, the Javascript code will not be rendered.
If you do not set an API key, the JavaScript code will not be rendered.
.. _performable-identity-user:
@ -69,8 +69,8 @@ Identifying authenticated users
-------------------------------
If your websites identifies visitors, you can pass this information on
to Performable so that you can track individual users. By default, the
username of an authenticated user is passed to Performable
to Performable so that you can track individual users. By default, the
username of an authenticated user is passed to Performable
automatically. See :ref:`identifying-visitors`.
You can also send the visitor identity yourself by adding either the
@ -116,7 +116,7 @@ Embedding a landing page
========================
You can embed a Performable landing page in your Django website. The
:ttag:`performable_embed` template tag adds the Javascript code to embed
:ttag:`performable_embed` template tag adds the JavaScript code to embed
the page. It takes two arguments: the hostname and the page ID::
{% performable_embed HOSTNAME PAGE_ID %}

View file

@ -0,0 +1,73 @@
===================================
Rating\@Mail.ru -- traffic analysis
===================================
`Rating\@Mail.ru`_ is an analytics tool like as google analytics.
.. _`Rating\@Mail.ru`: http://top.mail.ru/
.. rating-mailru-installation:
Installation
============
To start using the Rating\@Mail.ru 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 Rating\@Mail.ru 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:`rating-mailru-configuration`.
The Rating\@Mail.ru counter code is inserted into templates using a template
tag. Load the :mod:`rating_mailru` template tag library and insert the
:ttag:`rating_mailru` 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 rating_mailru %}
<html>
<head>
...
{% rating_mailru %}
</head>
...
.. _rating-mailru-configuration:
Configuration
=============
Before you can use the Rating\@Mail.ru integration, you must first set
your website counter ID.
.. _rating-mailru-counter-id:
Setting the counter ID
----------------------
Every website you track with Rating\@Mail.ru gets its own counter ID,
and the :ttag:`rating_mailru` tag will include it in the rendered
JavaScript code. You can find the web counter ID on the overview page
of your account. Set :const:`RATING_MAILRU_COUNTER_ID` in the
project :file:`settings.py` file::
RATING_MAILRU_COUNTER_ID = '1234567'
If you do not set a counter ID, the counter 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:`RATING_MAILRU_INTERNAL_IPS` setting,
the tracking code is commented out. It takes the value of
:const:`ANALYTICAL_INTERNAL_IPS` by default (which in turn is
:const:`INTERNAL_IPS` by default). See :ref:`identifying-visitors` for
important information about detecting the visitor IP address.

View file

@ -0,0 +1,161 @@
=======================
SnapEngage -- live chat
=======================
SnapEngage_ is a live chat widget for your site which integrates with your
existing chat client. It integrates with many online applications and even
allows you to make a remote screenshot of the webpage. SnapEngage can be
customized to fit your website look and feel, offers reports and statistics and
is available in many languages.
.. _SnapEngage: http://www.snapengage.com/
Installation
============
To start using the SnapEngage 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 SnapEngage 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:`snapengage-configuration`.
The SnapEngage JavaScript code is inserted into templates using a
template tag. Load the :mod:`SnapEngage` template tag library and
insert the :ttag:`SnapEngage` 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 snapengage %}
...
{% snapengage %}
</body>
</html>
.. _snapengage-configuration:
Configuration
=============
Before you can use the SnapEngage integration, you must first set the
widget ID. You can customize the visitor nickname and add information
to their status in the operator buddy list, and customize the text used
in the chat window.
Setting the widget ID
---------------------
In order to install the chat code, you need to set the ID of the
SnapEngage widget. You can find the site ID on the `Your Widget ID
page`_ of your SnapEngage account. Set :const:`SNAPENGAGE_WIDGET_ID` in
the project :file:`settings.py` file::
SNAPENGAGE_WIDGET_ID = 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX'
If you do not set the widget ID, the chat window will not be rendered.
.. _`Your Widget ID page`: https://secure.snapengage.com/getwidgetid
Customizing the widget
----------------------
The SnapEngage widget can be customized in various ways using either
context variables or settings. More information about controlling the
widget can be found on the `customization FAQ section`_ of the
SnapEngage website.
===================================== ===================================== ==================================================================
Setting Context variable Description
===================================== ===================================== ==================================================================
``SNAPENGAGE_DOMAIN`` ``snapengage_domain`` Manually set the domain name to follow users across subdomains.
``SNAPENGAGE_SECURE_CONNECTION`` ``snapengage_secure_connection`` Force the use of SSL for the chat connection, even on unencrypted
pages. (Default: ``False``)
``SNAPENGAGE_BUTTON_EFFECT`` ``snapengage_button_effect`` An effect applied when the mouse hovers over the button.
(Example: ``"-4px"``)
``SNAPENGAGE_BUTTON_STYLE`` ``snapengage_button_style`` What the chat button should look like. Use any of the
:const:`BUTTON_STYLE_*` constants, or a URL to a custom button
image.
``SNAPENGAGE_BUTTON_LOCATION`` ``snapengage_button_location`` The location of the chat button. Use any of the
:const:`BUTTON_LOCATION_*` constants.
``SNAPENGAGE_BUTTON_LOCATION_OFFSET`` ``snapengage_button_location_offset`` The offset of the button from the top or left side of the page.
(Default: ``"55%"``)
``SNAPENGAGE_FORM_POSITION`` ``snapengage_form_position`` Configure the location of the chat window. Use any of the
:const:`FORM_POSITION_*` constants.
``SNAPENGAGE_FORM_TOP_POSITION`` ``snapengage_form_top_position`` The chat window offset in pixels from the top of the page.
``SNAPENGAGE_READONLY_EMAIL`` ``snapengage_readonly_email`` Whether a preset e-mail address can be changed by the visitor.
(Default: ``False``)
``SNAPENGAGE_SHOW_OFFLINE`` ``snapengage_show_offline`` Whether to show the chat button when all operators are offline.
(Default: ``True``)
``SNAPENGAGE_SCREENSHOTS`` ``snapengage_screenshots`` Whether to allow the user to take a screenshot.
(Default: ``True``)
``SNAPENGAGE_OFFLINE_SCREENSHOTS`` ``snapengage_offline_screenshots`` Whether to allow the user to take a screenshot when all operators
are offline. (Default: ``True``)
``SNAPENGAGE_SOUNDS`` ``snapengage_sounds`` Whether to play chat sound notifications. (Default: ``True``)
===================================== ===================================== ==================================================================
There are also two customizations that can only be used with context
variables.
============================= =========================================
Context variable Description
============================= =========================================
``snapengage_proactive_chat`` Set to ``False`` to disable proactive
chat, for example for users who are
already converted.
``snapengage_email`` Set the e-mail address of the website
visitor. (See :ref:`snapengage-email`)
============================= =========================================
.. _`customization FAQ section`: http://www.snapengage.com/faq#customization
.. _snapengage-email:
Setting the visitor e-mail address
----------------------------------
If your website identifies visitors, you can use that to pass their e-mail
address to the support agent. By default, the e-mail address of an
authenticated user is automatically used. See :ref:`identifying-visitors`.
You can also set the visitor e-mail address yourself by adding either the
``snapengage_email`` (alias: ``snapengage_identity``) or the
``analytical_identity`` variable to the template context. If both
variables are set, the former takes precedence. For example::
context = RequestContext({'snapengage_email': email})
return some_template.render(context)
If you can derive the e-mail address 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`::
from django.core.exceptions import ObjectDoesNotExist
def set_snapengage_email(request):
try:
profile = request.user.get_profile()
return {'snapengage_email': profile.business_email}
except (AttributeError, ObjectDoesNotExist):
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.
If the user should not be able to edit the pre-set e-mail address, you
can set either the ``snapengage_readonly_email`` context variable or the
:setting:`SNAPENGAGE_READONLY_EMAIL` setting to ``True``.
----
Thanks go to SnapEngage for their support with the development of this
application.

View file

@ -0,0 +1,160 @@
=====================================
Spring Metrics -- conversion tracking
=====================================
`Spring Metrics`_ is a conversions analysis tool. It shows you the top
converting sources, search keywords and landing pages. The real-time
dashboard shows you how customers interact with your website and how
to increase conversion.
.. _`Spring Metrics`: http://www.springmetrics.com/
Installation
============
To start using the Spring Metrics 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 Spring Metrics 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:`spring-metrics-configuration`.
The Spring Metrics tracking code is inserted into templates using a
template tag. Load the :mod:`spring_metrics` template tag library and
insert the :ttag:`spring_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 bottom of the HTML head::
{% load spring_metrics %}
<html>
<head>
...
{% spring_metrics %}
</head>
...
.. _spring-metrics-configuration:
Configuration
=============
Before you can use the Spring Metrics integration, you must first set
your website Tracking ID and tag a page for conversion. You can also
customize the data that Spring Metrics tracks.
Setting the Tracking ID
-----------------------
Every website you track with Spring Metrics gets its own Tracking ID,
and the :ttag:`spring_metrics` tag will include it in the rendered
JavaScript code. You can find the Tracking ID in the `Site Settings`_
of your Spring Metrics account. Set :const:`SPRING_METRICS_TRACKING_ID`
in the project :file:`settings.py` file::
SPRING_METRICS_TRACKING_ID = 'XXXXXXXXXX'
If you do not set a Tracking ID, the tracking code will not be rendered.
.. _`manage page`: https://app.springmetrics.com/manage/
.. _`Convertion Tagging`:
Tagging conversion
------------------
In order to make use of Spring Metrics, you must tell it when visitors
become customers. This is called conversion. Usually, it marked by
the client requesting a specific page, such as the "thank you" page
of a webshop checkout. You tag these pages in the `Site Settings`_
of your Spring Metrics account.
Alternatively, you can mark conversion pages using the
:data:`spring_metrics_convert` template context variable::
context = RequestContext({'spring_metrics_convert': 'mailinglist signup'})
return some_template.render(context)
.. _`Site Settings`: https://app.springmetrics.com/manage
Tracking revenue
----------------
Spring Metrics allows you to track the value of conversions. Using the
:data:`spring_metrics_revenue` template context variable, you can let
the :ttag:`spring_metrics` tag pass earned revenue to Spring Metrics.
You can set the context variable in your view when you render a
template containing the tracking code::
context = RequestContext({
'spring_metrics_convert': 'sale',
'spring_metrics_revenue': '30.53',
})
return some_template.render(context)
(You would not need to use the :data:`spring_metrics_convert` variable
if you already tagged the page in Spring Metrics.)
Custom data
-----------
Spring Metrics can also track other data. Interesting examples could be
transaction IDs or the e-mail addresses from logged in users. By
setting any :data:`spring_metrics_X` template context variable, Spring
Metrics will track a variable named :data:`X`. For example::
context = RequestContext({
'spring_metrics_revenue': '30.53',
'spring_metrics_order_id': '15445',
})
return some_template.render(context)
Some variables should be passed on every page and can be computed from
the request object. In such cases you will want to set custom
variables in a context processor that you add to the
:data:`TEMPLATE_CONTEXT_PROCESSORS` list in :file:`settings.py`::
def spring_metrics_global_variables(request):
try:
profile = request.user.get_profile()
return {'spring_metrics_city': profile.address.city}
except (AttributeError, ObjectDoesNotExist):
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.
Identifying authenticated users
-------------------------------
If you have not set the :data:`spring_metrics_email` property
explicitly, the e-mail address of an authenticated user is passed to
Spring Metrics automatically. See :ref:`identifying-visitors`.
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:`SPRING_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.
----
Thanks go to Spring Metrics for their support with the development of
this application.

218
docs/services/uservoice.rst Normal file
View file

@ -0,0 +1,218 @@
..
After updating this file, remember to upload to the UserVoice
knowledge base.
=======================================
UserVoice -- user feedback and helpdesk
=======================================
UserVoice_ makes it simple for your customers to give, discuss, and vote
for feedback. An unobtrusive feedback tab allows visitors to easily
submit and discuss ideas without having to sign up for a new account.
The best ideas are delivered to you based on customer votes.
.. _UserVoice: http://www.uservoice.com/
.. _uservoice-installation:
Installation
============
To start using the UserVoice 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 UserVoice 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:`uservoice-configuration`.
The UserVoice JavaScript code is inserted into templates using a
template tag. Load the :mod:`uservoice` template tag library and insert
the :ttag:`uservoice` tag. Because every page that you want to have
the feedback tab to appear on 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 uservoice %}
...
{% uservoice %}
</body>
</html>
.. _uservoice-configuration:
Configuration
=============
Before you can use the UserVoice integration, you must first set the
widget key.
Setting the widget key
----------------------
In order to use the feedback widget, you need to configure which widget
you want to show. You can find the widget keys in the *Channels* tab on
your UserVoice *Settings* page. Under the *JavaScript Widget* heading,
find the JavaScript embed code of the widget. The widget key is the
alphanumerical string contained in the URL of the script imported by the
embed code::
<script>
UserVoice=window.UserVoice||[];(function(){
var uv=document.createElement('script');uv.type='text/javascript';
uv.async=true;uv.src='//widget.uservoice.com/XXXXXXXXXXXXXXXXXXXX.js';
var s=document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(uv,s)})();
</script>
(The widget key is shown as ``XXXXXXXXXXXXXXXXXXXX``.)
The default widget
..................
Often you will use the same widget throughout your website. The default
widget key is configured by setting :const:`USERVOICE_WIDGET_KEY` in
the project :file:`settings.py` file::
USERVOICE_WIDGET_KEY = 'XXXXXXXXXXXXXXXXXXXX'
If the setting is present but empty, no widget is shown by default. This
is useful if you want to set a widget using a template context variable,
as the setting must be present for the generic :ttag:`analytical.*` tags
to work.
Widget options
..............
You can set :const:`USERVOICE_WIDGET_OPTIONS` to customize your widget
with UserVoice's options.
.. tip::
See the `JS SDK Overview <https://developer.uservoice.com/docs/widgets/overview/>`_ and the `reference <https://developer.uservoice.com/docs/widgets/options/>`_ for the details of available options.
For example, to override the default icon style with a tab and on the left,
you could define:
.. code-block:: python
USERVOICE_WIDGET_OPTIONS = {"trigger_position": "left",
"trigger_style": "tab"}
Per-view widget
...............
The widget configuration can be overriden in a view using
``uservoice_widget_options`` template context variable. For example:
.. code-block:: python
context = RequestContext({'uservoice_widget_options': 'mode': 'satisfaction'})
return some_template.render(context)
It's also possible to set a different widget key for a particular view
with ``uservoice_widget_key``:
.. code-block:: python
context = RequestContext({'uservoice_widget_key': 'XXXXXXXXXXXXXXXXXXXX'})
return some_template.render(context)
These variable passed in the context overrides the default
widget configuration.
.. _uservoice-link:
Using a custom link
-------------------
Instead of showing the default feedback icon or tab, you can make the UserVoice
widget launch when a visitor clicks a link or when some other event
occurs. As the `documentation describe <https://developer.uservoice.com/docs/widgets/methods/#custom-trigger>`_, simply add the ``data-uv-trigger`` HTML attribute to the element. For example::
<a href="mailto:questions@yoursite.com" data-uv-trigger>Contact us</a>
In order to hidden the default trigger, you should disable it putting
``uservoice_add_trigger`` to ``False``::
context = RequestContext({'uservoice_add_trigger': False})
return your_template_with_custom_uservoice_link.render(context)
If you want to disable the automatic trigger globally, set in :file:`settings.py`::
USERVOICE_ADD_TRIGGER = False
Setting the widget key in a context processor
.............................................
You can also set the widget keys in a context processor that you add to
the :data:`TEMPLATE_CONTEXT_PROCESSORS` list in :file:`settings.py`.
For example, to show a specific widget to logged in users::
def uservoice_widget_key(request):
try:
if request.user.is_authenticated():
return {'uservoice_widget_key': 'XXXXXXXXXXXXXXXXXXXX'}
except AttributeError:
pass
return {}
The widget key passed in the context variable overrides both the default
and the per-view widget key.
Identifying users
-----------------
If your websites identifies visitors, you can pass this information on
to Uservoice. By default, the name and email of an authenticated user
is passed to Uservoice automatically. See :ref:`identifying-visitors`.
You can also send the visitor identity yourself by adding either the
``uservoice_identity`` or the ``analytical_identity`` variable to
the template context. (If both are set, the former takes precedence.)
This should be a dictionary with the desired user traits as its keys.
Check the `documentation on identifying users`_ to see valid traits.
For example::
context = RequestContext({'uservoice_identity': {'email': user_email,
'name': username }})
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 {'uservoice_identity': {
email: request.user.username,
name: request.user.get_full_name(),
id: request.user.id,
type: 'vip',
account: {
name: 'Acme, Co.',
monthly_rate: 9.99,
ltv: 1495.00,
plan: 'Enhanced'
}
}
}
except AttributeError:
return {}
.. _`documentation on identifying users`: https://developer.uservoice.com/docs/widgets/identify/
----
Thanks go to UserVoice for their support with the development of this
application.

204
docs/services/woopra.rst Normal file
View file

@ -0,0 +1,204 @@
===========================
Woopra -- website analytics
===========================
Woopra_ generates live detailed statistics about the visitors to your
website. You can watch your visitors navigate live and interact with
them via chat. The service features notifications, campaigns, funnels
and more.
.. _Woopra: http://www.woopra.com/
Installation
============
To start using the Woopra 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 Woopra 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:`woopra-configuration`.
The Woopra tracking code is inserted into templates using a template
tag. Load the :mod:`woopra` template tag library and insert the
:ttag:`woopra` 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 woopra %}
<html>
<head>
...
{% woopra %}
</head>
...
Because JavaScript code is asynchronous, putting the tag in the head
section increases the chances that a page view is going to be tracked
before the visitor leaves the page. See for details the `Asynchronous
JavaScript Developers Guide`_ on the Woopra website.
.. _`Asynchronous JavaScript Developers Guide`: http://www.woopra.com/docs/async/
.. _woopra-configuration:
Configuration
=============
Before you can use the Woopra integration, you must first set the
website domain. You can also customize the data that Woopra tracks and
identify authenticated users.
Setting the domain
------------------
A Woopra account is tied to a website domain. Set
:const:`WOOPRA_DOMAIN` in the project :file:`settings.py` file::
WOOPRA_DOMAIN = 'XXXXXXXX.XXX'
If you do not set a domain, the tracking code will not be rendered.
(In theory, the django-analytical application could get the website
domain from the current ``Site`` or the ``request`` object, but this
setting also works as a sign that the Woopra integration should be
enabled for the :ttag:`analytical.*` template tags.)
Visitor timeout
---------------
The default Woopra visitor timeout -- the time after which Woopra
ignores inactive visitors on a website -- is set at 4 minutes. This
means that if a user opens your Web page and then leaves it open in
another browser window, Woopra will report that the visitor has gone
away after 4 minutes of inactivity on that page (no page scrolling,
clicking or other action).
If you would like to increase or decrease the idle timeout setting you
can set :const:`WOOPRA_IDLE_TIMEOUT` to a time in milliseconds. For
example, to set the default timout to 10 minutes::
WOOPRA_IDLE_TIMEOUT = 10 * 60 * 1000
Keep in mind that increasing this number will not only show you more
visitors on your site at a time, but will also skew your average time on
a page reporting. So its important to keep the number reasonable in
order to accurately make predictions.
Cookie control
--------------
Optional settings that influence the cookie behavior::
WOOPRA_COOKIE_NAME = "wooTracker"
WOOPRA_COOKIE_DOMAIN = ".example.com"
WOOPRA_COOKIE_PATH = "/"
WOOPRA_COOKIE_EXPIRE = "Fri Jan 01 2027 15:00:00 GMT+0000"
See the `Woopra documentation`_ for more specific details.
Tracking control
----------------
Optional settings that influence the tracking as such::
WOOPRA_CLICK_TRACKING = False
WOOPRA_DOWNLOAD_TRACKING = False
WOOPRA_OUTGOING_TRACKING = False
WOOPRA_OUTGOING_IGNORE_SUBDOMAIN = True
WOOPRA_IGNORE_QUERY_URL = True
WOOPRA_HIDE_CAMPAIGN = False
See the `Woopra documentation`_ for more specific details.
.. _`Woopra documentation`:
https://docs.woopra.com/reference/woopraconfig#configuring-your-tracker
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:`WOOPRA_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.
Custom data
-----------
As described in the Woopra documentation on `custom visitor data`_,
the data that is tracked by Woopra can be customized. Using template
context variables, you can let the :ttag:`woopra` tag pass custom data
to Woopra automatically. You can set the context variables in your view
when your render a template containing the tracking code::
context = RequestContext({'woopra_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 woopra_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 {'woopra_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 Woopra live visitor
data are listed in the table below, but you can define any ``woopra_*``
variable you like and have that detail passed from within the visitor
live stream data when viewing Woopra.
==================== ===================================
Context variable Description
==================== ===================================
``woopra_name`` The visitor's full name.
-------------------- -----------------------------------
``woopra_email`` The visitor's email address.
-------------------- -----------------------------------
``woopra_avatar`` A URL link to a visitor avatar.
==================== ===================================
.. _`custom visitor data`: http://www.woopra.com/docs/tracking/custom-visitor-data/
Identifying authenticated users
-------------------------------
If you have not set the ``woopra_name`` or ``woopra_email`` variables
explicitly, the username and email address of an authenticated user are
passed to Woopra automatically. See :ref:`identifying-visitors`.
----
Thanks go to Woopra for their support with the development of this
application.

View file

@ -0,0 +1,84 @@
==================================
Yandex.Metrica -- traffic analysis
==================================
`Yandex.Metrica`_ is an analytics tool like as google analytics.
.. _`Yandex.Metrica`: http://metrica.yandex.com/
.. yandex-metrica-installation:
Installation
============
To start using the Yandex.Metrica 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 Yandex.Metrica 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:`yandex-metrica-configuration`.
The Yandex.Metrica counter code is inserted into templates using a template
tag. Load the :mod:`yandex_metrica` template tag library and insert the
:ttag:`yandex_metrica` 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 yandex_metrica %}
<html>
<head>
...
{% yandex_metrica %}
</head>
...
.. _yandex-metrica-configuration:
Configuration
=============
Before you can use the Yandex.Metrica integration, you must first set
your website counter ID.
.. _yandex-metrica-counter-id:
Setting the counter ID
----------------------
Every website you track with Yandex.Metrica gets its own counter ID,
and the :ttag:`yandex_metrica` tag will include it in the rendered
JavaScript code. You can find the web counter ID on the overview page
of your account. Set :const:`YANDEX_METRICA_COUNTER_ID` in the
project :file:`settings.py` file::
YANDEX_METRICA_COUNTER_ID = '12345678'
If you do not set a counter ID, the counter code will not be rendered.
You can set additional options to tune your counter:
============================ ============= =============================================
Constant Default Value Description
============================ ============= =============================================
``YANDEX_METRICA_WEBVISOR`` False Webvisor, scroll map, form analysis.
``YANDEX_METRICA_TRACKHASH`` False Hash tracking in the browser address bar.
``YANDEX_METRICA_NOINDEX`` False Stop automatic page indexing.
``YANDEX_METRICA_ECOMMERCE`` False Dispatch ecommerce data to Metrica.
============================ ============= =============================================
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:`YANDEX_METRICA_INTERNAL_IPS` setting,
the tracking code is commented out. It takes the value of
:const:`ANALYTICAL_INTERNAL_IPS` by default (which in turn is
:const:`INTERNAL_IPS` by default). See :ref:`identifying-visitors` for
important information about detecting the visitor IP address.

View file

@ -6,7 +6,6 @@ Here's a full list of all available settings, in alphabetical order, and
their default values.
.. data:: ANALYTICAL_AUTO_IDENTIFY
Default: ``True``

View file

@ -4,13 +4,14 @@
Tutorial
========
This tutorial show you how to install and configure django-analytical
for basic tracking, and then briefly touch on two common customization
issues: visitor identification and custom data tracking data.
This tutorial will show you how to install and configure
django-analytical for basic tracking, and then briefly touch on two
common customization issues: visitor identification and custom data
tracking.
*Suppose your Django website provides information about the IPv4 to IPv6
Suppose your Django website provides information about the IPv4 to IPv6
transition. Visitors can discuss their problems and help each other
make the necessary changes to their network infrastructure.* You want to
make the necessary changes to their network infrastructure. You want to
use two different analytics services:
* :doc:`Clicky <services/clicky>` for detailed traffic analysis
@ -28,7 +29,9 @@ Setting up basic tracking
To get started with django-analytical, the package must first be
installed. You can download and install the latest stable package from
the Python Package Index automatically by using ``easy_install``::
the Python Package Index automatically by using ``easy_install``:
.. code-block:: bash
$ easy_install django-analytical
@ -36,7 +39,9 @@ For more ways to install django-analytical, see
:ref:`installing-the-package`.
After you install django-analytical, you need to add it to the list of
installed applications in the ``settings.py`` file of your project::
installed applications in the ``settings.py`` file of your project:
.. code-block:: python
INSTALLED_APPS = [
...
@ -45,7 +50,9 @@ installed applications in the ``settings.py`` file of your project::
]
Then you have to add the general-purpose django-analytical template tags
to your base template::
to your base template:
.. code-block:: django
{% load analytical %}
<!DOCTYPE ... >
@ -68,7 +75,9 @@ to your base template::
Finally, you need to configure the Clicky Site ID and the Crazy Egg
account number. Add the following to your project :file:`settings.py`
file (replacing the ``x``'s with your own codes)::
file (replacing the ``x``'s with your own codes):
.. code-block:: python
CLICKY_SITE_ID = 'xxxxxxxx'
CRAZY_EGG_ACCOUNT_NUMBER = 'xxxxxxxx'
@ -80,10 +89,10 @@ changes, both Clicky and Crazy Egg will be tracking your visitors.
Identifying authenticated users
===============================
*When your visitors post questions on IPv6 or tell about their
experience with the transition, they first log in through the standard
Django authentication system. Clicky can identify and track individual
visitors and you want to use this feature.*
Suppose that when your visitors post questions on IPv6 or tell others
about their experience with the transition, they first log in through
the standard Django authentication system. Clicky can identify and
track individual visitors and you want to use this feature.
If django-analytical template tags detect that the current user is
authenticated, they will automatically include code to send the username
@ -102,13 +111,16 @@ disable or override it, see :ref:`identifying-visitors`.
Adding custom tracking data
===========================
*You think that visitors who already use IPv6 use the website in a
different way from those still on IPv4. You want to test this by
segmenting the Crazy Egg heatmaps based on the IP protocol version.*
Suppose that you think that visitors who already have IPv6 use the
website in a different way from those still on IPv4. You want to test
this hypothesis by segmenting the Crazy Egg heatmaps based on the IP
protocol version.
In order to filter on protocol version in Crazy Egg, you need to
include the visitor IP protocol version in the Crazy Egg tracking code.
The easiest way to do this is by using a context processor::
The easiest way to do this is by using a context processor:
.. code-block:: python
def track_ip_proto(request):
addr = request.META.get('HTTP_X_FORWARDED_FOR', '')

127
pyproject.toml Normal file
View file

@ -0,0 +1,127 @@
[build-system]
build-backend = "setuptools.build_meta"
requires = ["setuptools>=80"]
[project]
name = "django-analytical"
dynamic = ["version"]
description = "Analytics service integration for Django projects"
readme = "README.rst"
license = "MIT"
license-files = ["LICENSE.txt"]
authors = [
{name = "Joost Cassee", email = "joost@cassee.net"},
{name = "Joshua Krall", email = "joshuakrall@pobox.com"},
{name = "Aleck Landgraf", email = "aleck.landgraf@buildingenergy.com"},
{name = "Alexandre Pocquet", email = "apocquet@lecko.fr"},
{name = "Bateau Knowledge", email = "info@bateauknowledge.nl"},
{name = "Bogdan Bodnar", email = "bogdanbodnar@mail.com"},
{name = "Brad Pitcher", email = "bradpitcher@gmail.com"},
{name = "Corentin Mercier", email = "corentin@mercier.link"},
{name = "Craig Bruce", email = "craig@eyesopen.com"},
{name = "Daniel Vitiello", email = "ezdismissal@gmail.com"},
{name = "David Smith", email = "smithdc@gmail.com"},
{name = "Diederik van der Boor", email = "vdboor@edoburu.nl"},
{name = "Eric Amador", email = "eric.amador14@gmail.com"},
{name = "Eric Davis", email = "eric@davislv.com"},
{name = "Eric Wang", email = "gnawrice@gmail.com"},
{name = "Erick Massip", email = "ericmassip1@gmail.com"},
{name = "Garrett Coakley", email = "garrettc@users.noreply.github.com"},
{name = "Garrett Robinson", email = "garrett.f.robinson@gmail.com"},
{name = "GreenKahuna", email = "info@greenkahuna.com"},
{name = "Hugo Osvaldo Barrera", email = "hugo@barrera.io"},
{name = "Ian Ramsay", email = "ianalexr@yahoo.com"},
{name = "Iván Raskovsky", email = "raskovsky+git@gmail.com"},
{name = "James Paden", email = "james@xemion.com"},
{name = "Jannis Leidel", email = "jannis@leidel.info"},
{name = "Julien Grenier", email = "julien.grenier42@gmail.com"},
{name = "Kevin Olbrich", email = "ko@sv01.de"},
{name = "Marc Bourqui", email = "m.bourqui@edsi-tech.com"},
{name = "Martey Dodoo", email = "martey@mobolic.com"},
{name = "Martín Gaitán", email = "gaitan@gmail.com"},
{name = "Matthäus G. Chajdas", email = "dev@anteru.net"},
{name = "Max Arnold", email = "arnold.maxim@gmail.com"},
{name = "Nikolay Korotkiy", email = "sikmir@gmail.com"},
{name = "Paul Oswald", email = "pauloswald@gmail.com"},
{name = "Peter Bittner", email = "django@bittner.it"},
{name = "Petr Dlouhý", email = "petr.dlouhy@email.cz"},
{name = "Philippe O. Wagner", email = "admin@arteria.ch"},
{name = "Pi Delport", email = "pjdelport@gmail.com"},
{name = "Sandra Mau", email = "sandra.mau@gmail.com"},
{name = "Scott Adams", email = "scottadams80@gmail.com"},
{name = "Scott Karlin", email = "gitlab@karlin-online.com"},
{name = "Sean Wallace", email = "sean@lowpro.ca"},
{name = "Sid Mitra", email = "sidmitra.del@gmail.com"},
{name = "Simon Ye", email = "sye737@gmail.com"},
{name = "Steve Schwarz", email = "steve@agilitynerd.com"},
{name = "Steven Skoczen", email = "steven.skoczen@wk.com"},
{name = "Tim Gates", email = "tim.gates@iress.com"},
{name = "Tinnet Coronam", email = "tinnet@coronam.net"},
{name = "Uros Trebec", email = "uros@trebec.org"},
{name = "Walter Renner", email = "walter.renner@me.com"},
{name = "Julian Haluska", email = "mail@julianhaluska.de"},
{name = "Ronard Luna", email = "rlunag@proton.me"},
]
maintainers = [
{name = "Jazzband community", email = "jazzband-bot@users.noreply.github.com"},
{name = "Peter Bittner", email = "django@bittner.it"},
]
keywords=[
"django",
"analytics",
]
classifiers=[
"Development Status :: 5 - Production/Stable",
"Environment :: Web Environment",
"Framework :: Django",
"Framework :: Django :: 4.2",
"Framework :: Django :: 5.1",
"Framework :: Django :: 5.2",
"Intended Audience :: Developers",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Topic :: Internet :: WWW/HTTP",
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
"Topic :: Software Development :: Libraries :: Python Modules",
]
requires-python = ">=3.9"
dependencies = [
"django>=4.2",
]
[project.urls]
Homepage = "https://github.com/jazzband/django-analytical"
Documentation = "https://django-analytical.readthedocs.io/"
[tool.coverage.report]
show_missing = true
skip_covered = true
[tool.coverage.run]
source = ["analytical"]
[tool.pytest.ini_options]
addopts = "--junitxml=tests/unittests-report.xml --color=yes --verbose"
DJANGO_SETTINGS_MODULE = "tests.testproject.settings"
[tool.ruff.format]
quote-style = "single"
[tool.ruff.lint.flake8-quotes]
inline-quotes = "single"
[tool.setuptools]
packages = [
"analytical",
"analytical.templatetags",
]
[tool.setuptools.dynamic]
version = {attr = "analytical.__version__"}

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