Add simple_ga app to push backend event info to Google Analytics
This commit is contained in:
parent
aed1dc2b14
commit
0690e3241e
10 changed files with 199 additions and 0 deletions
|
@ -55,6 +55,7 @@ INSTALLED_APPS = [
|
||||||
'events',
|
'events',
|
||||||
'accounts',
|
'accounts',
|
||||||
'resume',
|
'resume',
|
||||||
|
'simple_ga',
|
||||||
]
|
]
|
||||||
|
|
||||||
LOGIN_URL = 'login'
|
LOGIN_URL = 'login'
|
||||||
|
@ -84,6 +85,7 @@ MIDDLEWARE = [
|
||||||
|
|
||||||
'social_django.middleware.SocialAuthExceptionMiddleware',
|
'social_django.middleware.SocialAuthExceptionMiddleware',
|
||||||
'resume.middleware.ResumeMiddleware',
|
'resume.middleware.ResumeMiddleware',
|
||||||
|
'simple_ga.middleware.GAEventMiddleware',
|
||||||
]
|
]
|
||||||
|
|
||||||
ROOT_URLCONF = 'get_together.urls'
|
ROOT_URLCONF = 'get_together.urls'
|
||||||
|
@ -102,6 +104,7 @@ TEMPLATES = [
|
||||||
'social_django.context_processors.backends',
|
'social_django.context_processors.backends',
|
||||||
'social_django.context_processors.login_redirect',
|
'social_django.context_processors.login_redirect',
|
||||||
'django_settings_export.settings_export',
|
'django_settings_export.settings_export',
|
||||||
|
'simple_ga.context_processors.events',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -21,6 +21,11 @@
|
||||||
gtag('js', new Date());
|
gtag('js', new Date());
|
||||||
|
|
||||||
gtag('config', '{{settings.GOOGLE_ANALYTICS_ID}}');
|
gtag('config', '{{settings.GOOGLE_ANALYTICS_ID}}');
|
||||||
|
|
||||||
|
// GA Events
|
||||||
|
{% for event in ga_events %}
|
||||||
|
{{ event.gtag }}
|
||||||
|
{% endfor %}
|
||||||
{% block extra_google_analytics %}{% endblock %}
|
{% block extra_google_analytics %}{% endblock %}
|
||||||
</script>
|
</script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -15,6 +15,8 @@ from events import location
|
||||||
from accounts.decorators import setup_wanted
|
from accounts.decorators import setup_wanted
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
|
import simple_ga as ga
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
import simplejson
|
import simplejson
|
||||||
import geocoder
|
import geocoder
|
||||||
|
@ -45,6 +47,8 @@ def home(request, *args, **kwards):
|
||||||
|
|
||||||
near_distance = int(request.GET.get("distance", DEFAULT_NEAR_DISTANCE))
|
near_distance = int(request.GET.get("distance", DEFAULT_NEAR_DISTANCE))
|
||||||
context['distance'] = near_distance
|
context['distance'] = near_distance
|
||||||
|
if "distance" in request.GET and request.GET.get("distance"):
|
||||||
|
ga.add_event(request, 'homepage_search', category='search', label='distance', value=near_distance)
|
||||||
|
|
||||||
city=None
|
city=None
|
||||||
ll = None
|
ll = None
|
||||||
|
@ -53,6 +57,7 @@ def home(request, *args, **kwards):
|
||||||
city = City.objects.get(id=request.GET.get("city"))
|
city = City.objects.get(id=request.GET.get("city"))
|
||||||
context['city'] = city
|
context['city'] = city
|
||||||
ll = [city.latitude, city.longitude]
|
ll = [city.latitude, city.longitude]
|
||||||
|
ga.add_event(request, 'homepage_search', category='search', label='city', value=city.short_name)
|
||||||
else :
|
else :
|
||||||
context['city_search'] = False
|
context['city_search'] = False
|
||||||
try:
|
try:
|
||||||
|
|
1
simple_ga/__init__.py
Normal file
1
simple_ga/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
from simple_ga.api import *
|
36
simple_ga/api.py
Normal file
36
simple_ga/api.py
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
|
||||||
|
class GAFailure(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def add_event(request, action, category=None, label=None, value=None, fail_silently=False):
|
||||||
|
"""
|
||||||
|
Attempt to add a message to the request using the 'messages' app.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
events = request._ga_events
|
||||||
|
except AttributeError:
|
||||||
|
if not hasattr(request, 'META'):
|
||||||
|
raise TypeError(
|
||||||
|
"add_message() argument must be an HttpRequest object, not "
|
||||||
|
"'%s'." % request.__class__.__name__
|
||||||
|
)
|
||||||
|
if not fail_silently:
|
||||||
|
raise GAFailure(
|
||||||
|
'You cannot add messages without installing '
|
||||||
|
'django.contrib.messages.middleware.MessageMiddleware'
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return events.add(action, category, label, value)
|
||||||
|
|
||||||
|
|
||||||
|
def get_events(request):
|
||||||
|
"""
|
||||||
|
Return the message storage on the request if it exists, otherwise return
|
||||||
|
an empty list.
|
||||||
|
"""
|
||||||
|
if not hasattr(request, '_ga_events'):
|
||||||
|
return []
|
||||||
|
next_event = request._ga_events.pop()
|
||||||
|
while next_event is not None:
|
||||||
|
yield next_event
|
||||||
|
next_event = request._ga_events.pop();
|
5
simple_ga/apps.py
Normal file
5
simple_ga/apps.py
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class SimpleGaConfig(AppConfig):
|
||||||
|
name = 'simple_ga'
|
11
simple_ga/context_processors.py
Normal file
11
simple_ga/context_processors.py
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
from simple_ga.api import get_events
|
||||||
|
|
||||||
|
|
||||||
|
def events(request):
|
||||||
|
"""
|
||||||
|
Return a lazy 'messages' context variable as well as
|
||||||
|
'DEFAULT_MESSAGE_LEVELS'.
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
'ga_events': get_events(request),
|
||||||
|
}
|
130
simple_ga/middleware.py
Normal file
130
simple_ga/middleware.py
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
import json
|
||||||
|
from django.utils.deprecation import MiddlewareMixin
|
||||||
|
from django.utils.safestring import SafeData, mark_safe
|
||||||
|
|
||||||
|
class GAEvent:
|
||||||
|
def __init__(self, action, category=None, label=None, value=None):
|
||||||
|
self.action = action
|
||||||
|
self.category = category
|
||||||
|
self.label = label
|
||||||
|
self.value = value
|
||||||
|
|
||||||
|
def gtag(self):
|
||||||
|
return mark_safe(
|
||||||
|
"gtag('event', '%(action)s', {'event_category' : '%(category)s', 'event_label' : '%(label)s' }, 'event_value' : '%(value)s' });" % {
|
||||||
|
'action': self.action,
|
||||||
|
'category': self.category,
|
||||||
|
'label': self.label,
|
||||||
|
'value': self.value,
|
||||||
|
})
|
||||||
|
|
||||||
|
class EventEncoder(json.JSONEncoder):
|
||||||
|
"""
|
||||||
|
Compactly serialize instances of the ``Message`` class as JSON.
|
||||||
|
"""
|
||||||
|
message_key = '__json_message'
|
||||||
|
|
||||||
|
def default(self, obj):
|
||||||
|
if isinstance(obj, GAEvent):
|
||||||
|
event = {
|
||||||
|
'type': 'GAEvent',
|
||||||
|
'action': obj.action,
|
||||||
|
'category': obj.category,
|
||||||
|
'label': obj.label,
|
||||||
|
'value': obj.value,
|
||||||
|
}
|
||||||
|
return event
|
||||||
|
return super().default(obj)
|
||||||
|
|
||||||
|
|
||||||
|
class EventDecoder(json.JSONDecoder):
|
||||||
|
"""
|
||||||
|
Decode JSON that includes serialized ``Message`` instances.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def process_events(self, obj):
|
||||||
|
if isinstance(obj, list) and obj:
|
||||||
|
return [self.process_events(item) for item in obj]
|
||||||
|
if isinstance(obj, dict):
|
||||||
|
if getattr(obj, 'type', None) == 'GAEvent':
|
||||||
|
return GAEvent(**obj)
|
||||||
|
return {key: self.process_events(value)
|
||||||
|
for key, value in obj.items()}
|
||||||
|
return obj
|
||||||
|
|
||||||
|
def decode(self, s, **kwargs):
|
||||||
|
decoded = super().decode(s, **kwargs)
|
||||||
|
return self.process_events(decoded)
|
||||||
|
|
||||||
|
class EventStorage:
|
||||||
|
session_key = '_ga_events'
|
||||||
|
|
||||||
|
def __init__(self, request):
|
||||||
|
self.request = request
|
||||||
|
self._ga_events = self.load()
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return len(self._ga_events)
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return iter(self._ga_events)
|
||||||
|
|
||||||
|
def __contains__(self, item):
|
||||||
|
return item in self._ga_events
|
||||||
|
|
||||||
|
def load(self):
|
||||||
|
"""
|
||||||
|
Retrieve a list of resume points from the request's session.
|
||||||
|
"""
|
||||||
|
if self.session_key not in self.request.session:
|
||||||
|
return []
|
||||||
|
return self.deserialize_events(self.request.session.get(self.session_key))
|
||||||
|
|
||||||
|
def store(self):
|
||||||
|
"""
|
||||||
|
Store a list of resume points to the request's session.
|
||||||
|
"""
|
||||||
|
if self._ga_events:
|
||||||
|
self.request.session[self.session_key] = self.serialize_events(self._ga_events)
|
||||||
|
else:
|
||||||
|
self.request.session.pop(self.session_key, None)
|
||||||
|
return []
|
||||||
|
|
||||||
|
def serialize_events(self, events):
|
||||||
|
encoder = EventEncoder(separators=(',', ':'))
|
||||||
|
return encoder.encode(events)
|
||||||
|
|
||||||
|
def deserialize_events(self, data):
|
||||||
|
if data and isinstance(data, str):
|
||||||
|
return json.loads(data, cls=EventDecoder)
|
||||||
|
return data
|
||||||
|
|
||||||
|
def add(self, action, category=None, label=None, value=None):
|
||||||
|
self._ga_events.append(GAEvent(action, category, label, value))
|
||||||
|
|
||||||
|
def pop(self):
|
||||||
|
if len(self._ga_events) > 0:
|
||||||
|
return self._ga_events.pop()
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class GAEventMiddleware(MiddlewareMixin):
|
||||||
|
"""
|
||||||
|
Middleware that handles setting resume points in a user flow.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def process_request(self, request):
|
||||||
|
request._ga_events = EventStorage(request)
|
||||||
|
|
||||||
|
def process_response(self, request, response):
|
||||||
|
"""
|
||||||
|
Update the storage backend (i.e., save the resume points).
|
||||||
|
|
||||||
|
Raise ValueError if not all resume points could be stored and DEBUG is True.
|
||||||
|
"""
|
||||||
|
# A higher middleware layer may return a request which does not contain
|
||||||
|
# resume storage, so make no assumption that it will be there.
|
||||||
|
if hasattr(request, '_ga_events'):
|
||||||
|
request._ga_events.store()
|
||||||
|
return response
|
0
simple_ga/migrations/__init__.py
Normal file
0
simple_ga/migrations/__init__.py
Normal file
3
simple_ga/tests.py
Normal file
3
simple_ga/tests.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
Loading…
Reference in a new issue