Merge branch 'master' of github.com:jcassee/django-analytical

This commit is contained in:
Joost Cassee 2013-03-25 00:50:15 +01:00
commit 782c8d90cf
17 changed files with 117 additions and 30 deletions

View file

@ -1,6 +1,6 @@
The django-analytical package was written by `Joost Cassee`_, with
contributions from `Eric Davis`_, `Paul Oswald`_, `Uros Trebec`_,
`Steven Skoczen`_ and others.
`Steven Skoczen`_, `Piet Delport`_, `Sandra Mau`_, `Simon Ye`_ and others.
Included Javascript code snippets for integration of the analytics
services were written by the respective service providers.
@ -14,6 +14,9 @@ The work on Crazy Egg was made possible by `Bateau Knowledge`_.
.. _`Eric Davis`: https://github.com/edavis
.. _`Paul Oswald`: https://github.com/poswald
.. _`Uros Trebec`: https://github.com/failedguidedog
.. _`Steven Skoczen`: https://github.com/https://github.com/skoczen
.. _`Steven Skoczen`: https://github.com/skoczen
.. _`Piet Delport`: https://github.com/pjdelport
.. _`Sandra Mau`: https://github.com/xthepoet
.. _`Simon Ye`: https://github.com/yesimon
.. _Analytical: https://github.com/jkrall/analytical
.. _`Bateau Knowledge`: http://www.bateauknowledge.nl/

View file

@ -1,3 +1,12 @@
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 (Piet Delport)
Version 0.12.0
--------------
* Add support for the UserVoice service.

View file

@ -10,6 +10,6 @@ Django_ project. See the ``docs`` directory for more information.
__author__ = "Joost Cassee"
__email__ = "joost@cassee.net"
__version__ = "0.12.1"
__copyright__ = "Copyright (C) 2011 Joost Cassee and others"
__version__ = "0.14.0"
__copyright__ = "Copyright (C) 2011-2012 Joost Cassee and others"
__license__ = "MIT License"

View file

@ -50,6 +50,7 @@ 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']);"
register = Library()
@ -120,6 +121,8 @@ class GoogleAnalyticsNode(Node):
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)
return commands
def contribute_to_analytical(add_node):

View file

@ -35,9 +35,11 @@ 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()
@ -67,6 +69,12 @@ class KissMetricsNode(Node):
identity = get_identity(context, 'kiss_metrics')
if identity is not None:
commands.append(IDENTIFY_CODE % identity)
try:
properties = context[ALIAS_CONTEXT_KEY]
key, value = properties.popitem()
commands.append(ALIAS_CODE % (key,value))
except KeyError:
pass
try:
name, properties = context[EVENT_CONTEXT_KEY]
commands.append(EVENT_CODE % {'name': name,

View file

@ -16,14 +16,14 @@ from analytical.utils import is_internal_ip, disable_html, get_identity, \
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 b,a,e,d,c;b=document.createElement("script");b.type="text/javascript";b.async=true;b.src=(document.location.protocol==="https:"?"https:":"http:")+"//api.mixpanel.com/site_media/js/api/mixpanel.js";a=document.getElementsByTagName("script")[0];a.parentNode.insertBefore(b,a);e=function(f){return function(){mpq.push([f].concat(Array.prototype.slice.call(arguments,0)))}};d=["init","track","track_links","track_forms","register","register_once","identify","name_tag","set_config"];for(c=0;c<d.length;c++){mpq[d[c]]=e(d[c])}})();
(function(c,a){window.mixpanel=a;var b,d,h,e;b=c.createElement("script");b.type="text/javascript";b.async=!0;b.src=("https:"===c.location.protocol?"https:":"http:")+'//cdn.mxpnl.com/libs/mixpanel-2.1.min.js';d=c.getElementsByTagName("script")[0];d.parentNode.insertBefore(b,d);a._i=[];a.init=function(b,c,f){function d(a,b){var c=b.split(".");2==c.length&&(a=a[c[0]],b=c[1]);a[b]=function(){a.push([b].concat(Array.prototype.slice.call(arguments,0)))}}var g=a;"undefined"!==typeof f?
g=a[f]=[]:f="mixpanel";g.people=g.people||[];h="disable track track_pageview track_links track_forms register register_once unregister identify name_tag set_config people.identify people.set people.increment".split(" ");for(e=0;e<h.length;e++)d(g,h[e]);a._i.push([b,c,f])};a.__SV=1.1})(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]);"
IDENTIFY_CODE = "mixpanel.register_once({distinct_id: '%s'});"
EVENT_CODE = "mixpanel.track('%(name)s', %(properties)s);"
EVENT_CONTEXT_KEY = 'mixpanel_event'
register = Library()

View file

@ -12,3 +12,5 @@ DATABASES = {
INSTALLED_APPS = [
'analytical',
]
SECRET_KEY = 'testing'

View file

@ -86,6 +86,15 @@ class GoogleAnalyticsTagTestCase(TagTestCase):
'<!-- Google Analytics disabled on internal IP address'), r)
self.assertTrue(r.endswith('-->'), r)
@override_settings(GOOGLE_ANALYTICS_ANONYMIZE_IP=True)
def test_anonymize_ip(self):
r = GoogleAnalyticsNode().render(Context())
self.assertTrue("_gaq.push (['_gat._anonymizeIp']);" in r, r)
@override_settings(GOOGLE_ANALYTICS_ANONYMIZE_IP=False)
def test_anonymize_ip_not_present(self):
r = GoogleAnalyticsNode().render(Context())
self.assertFalse("_gaq.push (['_gat._anonymizeIp']);" in r, r)
@without_apps('django.contrib.sites')
@override_settings(GOOGLE_ANALYTICS_PROPERTY_ID='UA-123456-7',

View file

@ -64,6 +64,12 @@ class KissMetricsTagTestCase(TagTestCase):
self.assertTrue("_kmq.push(['set', "
'{"prop1": "val1", "prop2": "val2"}]);' in r, r)
def test_alias(self):
r = KissMetricsNode().render(Context({'kiss_metrics_alias':
{'test': 'test_alias'}}))
self.assertTrue("_kmq.push(['alias', 'test', 'test_alias']);" in r,r)
@override_settings(ANALYTICAL_INTERNAL_IPS=['1.1.1.1'])
def test_render_internal_ip(self):
req = HttpRequest()

View file

@ -20,13 +20,13 @@ class MixpanelTagTestCase(TagTestCase):
def test_tag(self):
r = self.render_tag('mixpanel', 'mixpanel')
self.assertTrue(
"mpq.push(['init', '0123456789abcdef0123456789abcdef']);" in r,
"mixpanel.init('0123456789abcdef0123456789abcdef');" in r,
r)
def test_node(self):
r = MixpanelNode().render(Context())
self.assertTrue(
"mpq.push(['init', '0123456789abcdef0123456789abcdef']);" in r,
"mixpanel.init('0123456789abcdef0123456789abcdef');" in r,
r)
@override_settings(MIXPANEL_API_TOKEN=SETTING_DELETED)
@ -44,18 +44,18 @@ class MixpanelTagTestCase(TagTestCase):
@override_settings(ANALYTICAL_AUTO_IDENTIFY=True)
def test_identify(self):
r = MixpanelNode().render(Context({'user': User(username='test')}))
self.assertTrue("mpq.push(['identify', 'test']);" in r, r)
self.assertTrue("mixpanel.register_once({distinct_id: 'test'});" in r, r)
@override_settings(ANALYTICAL_AUTO_IDENTIFY=True)
def test_identify_anonymous_user(self):
r = MixpanelNode().render(Context({'user': AnonymousUser()}))
self.assertFalse("mpq.push(['identify', " in r, r)
self.assertFalse("mixpanel.register_once({distinct_id:" 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)
('test_event', {'prop1': 'val1', 'prop2': 'val2'})}))
self.assertTrue("mixpanel.track('test_event', "
'{"prop1": "val1", "prop2": "val2"});' in r, r)
@override_settings(ANALYTICAL_INTERNAL_IPS=['1.1.1.1'])
def test_render_internal_ip(self):

View file

@ -29,7 +29,7 @@ pygments_style = 'sphinx'
intersphinx_mapping = {
'http://docs.python.org/2.6': None,
'http://docs.djangoproject.com/en/1.3': 'http://docs.djangoproject.com/en/1.3/_objects/',
'http://docs.djangoproject.com/en/1.3': 'http://docs.djangoproject.com/en/1.4/_objects/',
}

View file

@ -18,7 +18,7 @@ Overview
:start-after: Django_ project.
:end-before: Currently supported services:
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

@ -160,6 +160,6 @@ settings required to enable each service are listed here:
----
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

@ -151,7 +151,7 @@ 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.
================= ====== =============================================
@ -172,3 +172,21 @@ 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

View file

@ -108,7 +108,29 @@ 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-event:
.. _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
----------------

View file

@ -124,8 +124,7 @@ notation, as described in the section titled
`"Asynchronous Tracking with Javascript"`_ in the Mixpanel
documentation. For example::
mpq.push(["track", "play-game", {"level": "12", "weapon": "sword", "character": "knight"}]);
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

16
tox.ini
View file

@ -1,7 +1,7 @@
[tox]
envlist =
py2.6-django1.2,py2.6-django1.3,
py2.7-django1.2,py2.7-django1.3
py2.6-django1.2,py2.6-django1.3,py2.6-django1.4,
py2.7-django1.2,py2.7-django1.3,py2.7-django1.4,
[testenv]
commands = python -Wall setup.py test
@ -12,7 +12,11 @@ deps = Django>=1.2,<1.3
[testenv:py2.6-django1.3]
basepython = python2.6
deps = Django>=1.3
deps = Django>=1.3,<1.4
[testenv:py2.6-django1.4]
basepython = python2.6
deps = Django>=1.4,<1.5
[testenv:py2.7-django1.2]
basepython = python2.7
@ -20,4 +24,8 @@ deps = Django>=1.2,<1.3
[testenv:py2.7-django1.3]
basepython = python2.7
deps = Django>=1.3
deps = Django>=1.3,<1.4
[testenv:py2.7-django1.4]
basepython = python2.7
deps = Django>=1.4,<1.5