Add simple_ga app to push backend event info to Google Analytics

This commit is contained in:
Michael Hall 2018-06-22 14:27:26 -04:00
parent aed1dc2b14
commit 0690e3241e
10 changed files with 199 additions and 0 deletions

View file

@ -55,6 +55,7 @@ INSTALLED_APPS = [
'events',
'accounts',
'resume',
'simple_ga',
]
LOGIN_URL = 'login'
@ -84,6 +85,7 @@ MIDDLEWARE = [
'social_django.middleware.SocialAuthExceptionMiddleware',
'resume.middleware.ResumeMiddleware',
'simple_ga.middleware.GAEventMiddleware',
]
ROOT_URLCONF = 'get_together.urls'
@ -102,6 +104,7 @@ TEMPLATES = [
'social_django.context_processors.backends',
'social_django.context_processors.login_redirect',
'django_settings_export.settings_export',
'simple_ga.context_processors.events',
],
},
},

View file

@ -21,6 +21,11 @@
gtag('js', new Date());
gtag('config', '{{settings.GOOGLE_ANALYTICS_ID}}');
// GA Events
{% for event in ga_events %}
{{ event.gtag }}
{% endfor %}
{% block extra_google_analytics %}{% endblock %}
</script>
{% endif %}

View file

@ -15,6 +15,8 @@ from events import location
from accounts.decorators import setup_wanted
from django.conf import settings
import simple_ga as ga
import datetime
import simplejson
import geocoder
@ -45,6 +47,8 @@ def home(request, *args, **kwards):
near_distance = int(request.GET.get("distance", DEFAULT_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
ll = None
@ -53,6 +57,7 @@ def home(request, *args, **kwards):
city = City.objects.get(id=request.GET.get("city"))
context['city'] = city
ll = [city.latitude, city.longitude]
ga.add_event(request, 'homepage_search', category='search', label='city', value=city.short_name)
else :
context['city_search'] = False
try:

1
simple_ga/__init__.py Normal file
View file

@ -0,0 +1 @@
from simple_ga.api import *

36
simple_ga/api.py Normal file
View 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
View file

@ -0,0 +1,5 @@
from django.apps import AppConfig
class SimpleGaConfig(AppConfig):
name = 'simple_ga'

View 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
View 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

View file

3
simple_ga/tests.py Normal file
View file

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.