Self-hostable website analytics on Django: a straightforward collector API, dashboards, a world map, and PDF reports.
analyticsdjangodockerhandcodedpythonself-hostedsqliteviteweb-analytics
1import json
2
3from django.http import HttpResponse
4from django.views.decorators.csrf import csrf_exempt
5from geoip2.errors import AddressNotFoundError
6from user_agents import parse as ua_parse
7try:
8 from django.contrib.gis.geoip2 import GeoIP2, GeoIP2Exception
9except ImportError: # GeoIP2 dependencies are optional
10 GeoIP2 = None
11
12 class GeoIP2Exception(Exception):
13 """Fallback exception when GeoIP2 isn't available."""
14
15
16from properties.models import Event, Property
17
18
19@csrf_exempt
20def collect(request):
21 """
22 Processes collector events sent to our server, stores them using Event for
23 the relevant Site.
24 """
25 if request.method == 'OPTIONS':
26 response = HttpResponse(status=204)
27 response['Allow'] = 'OPTIONS, POST'
28 response['Access-Control-Allow-Methods'] = 'OPTIONS, POST'
29 response['Access-Control-Allow-Headers'] = request.headers.get('Access-Control-Request-Headers', 'Content-Type')
30 response['Access-Control-Allow-Origin'] = request.headers.get('Origin', '*')
31 return response
32
33 if request.method != 'POST':
34 return HttpResponse(status=405)
35
36 raw_body = request.body
37
38 if not raw_body:
39 return HttpResponse(status=400)
40
41 try:
42 body = json.loads(raw_body)
43 except json.JSONDecodeError:
44 return HttpResponse(status=400)
45
46 collector_id = body.get('collectorId')
47 event_name = body.get('event')
48
49 if collector_id is None or event_name is None:
50 return HttpResponse(status=400)
51
52 try:
53 property_obj = Property.objects.get(id=collector_id)
54 except Property.DoesNotExist:
55 return HttpResponse(status=404)
56
57 event_data = body.get('data', {})
58 if not isinstance(event_data, dict):
59 return HttpResponse(status=400)
60
61 event_obj = Event(
62 property=property_obj,
63 event=event_name,
64 data=event_data,
65 )
66
67 # If we have a data__referrer then strip the url down to just the hostname
68 # ex. "example.com" all lowercase.
69 if 'referrer' in event_obj.data:
70 # Some urls have a query string, some have a fragment, some have more
71 # need to strip everything before the protocol and after the tld
72 # ex. "http://example.com/foo?bar=baz#frag" -> "example.com"
73 event_obj.data['referrer'] = event_obj.data['referrer'].split('://')[-1].split('/')[0].lower().replace('www.', '')
74
75 try:
76 if event_obj.event == 'session_start' and GeoIP2 is not None:
77 # Check HTTP_X_FORWARDED_FOR first item after split , for the client IP
78 # if it exists else use REMOTE_ADDR
79 ip = request.META.get('HTTP_X_FORWARDED_FOR', request.META.get('REMOTE_ADDR')).split(',')[0]
80 if ip != '127.0.0.1':
81 g = GeoIP2()
82 g_data = g.city(ip)
83 if g_data:
84 event_obj.data['country'] = g_data['country_code']
85 # Some MMDB providers (e.g. DB-IP free) don't populate the
86 # ISO subdivision code — only the full name. Prefer the
87 # name so we get a value either way; the world map's
88 # region lookup matches against both forms.
89 event_obj.data['region'] = g_data.get('region_name') or g_data.get('region')
90 event_obj.data['city'] = g_data['city']
91 event_obj.data['loc'] = [g_data['latitude'], g_data['longitude']]
92 except (GeoIP2Exception, AddressNotFoundError):
93 pass
94
95 # If we have a "user_agent" in "data" then parse it and store the results in
96 # data under "platform", "device" and "browser".
97 ua = None
98 if 'user_agent' in event_obj.data:
99 ua = ua_parse(event_obj.data['user_agent'])
100
101 # If we don't have a ua in the event_obj.data lets see if the request has
102 # one to parse.
103 if not ua and request.META.get('HTTP_USER_AGENT'):
104 ua = ua_parse(request.META.get('HTTP_USER_AGENT'))
105
106 if ua:
107 event_obj.data['platform'] = ua.os.family
108 event_obj.data['browser'] = ua.browser.family
109 if ua.is_mobile:
110 event_obj.data['device'] = 'Mobile'
111 elif ua.is_tablet:
112 event_obj.data['device'] = 'Tablet'
113 else:
114 event_obj.data['device'] = 'Desktop'
115 if ua.is_bot:
116 event_obj.data['is_bot'] = True
117 event_obj.data['bot_name'] = ua.browser.family or 'Unknown bot'
118
119 event_obj.save()
120
121 return HttpResponse(status=204)