repos
/ analytics-django master

analytics-django

mirror archived upstream

Self-hostable website analytics on Django: a straightforward collector API, dashboards, a world map, and PDF reports.

analyticsdjangodockerhandcodedpythonself-hostedsqliteviteweb-analytics

4.4 KB · 122 lines · Python Raw History
  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)