diff --git a/Pipfile b/Pipfile index 9d35a69..b2422b8 100644 --- a/Pipfile +++ b/Pipfile @@ -15,6 +15,7 @@ django-ipware = "*" pyyaml = "*" ua-parser = "*" user-agents = "*" +django-humanize = "*" [requires] python_version = "3.6" diff --git a/Pipfile.lock b/Pipfile.lock index bac52fe..4cca2e3 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "03f23e0c7409df9eb5a85b3df03d1975a1fe5563496602492cdcff46b8c5c829" + "sha256": "13b07e6285b739fe6f4987482aa448f1773d051de2de4806126a0e4bded150cf" }, "pipfile-spec": 6, "requires": { @@ -81,6 +81,13 @@ "index": "pypi", "version": "==0.41.0" }, + "django-humanize": { + "hashes": [ + "sha256:32491bf0209b89a277f7bfdab7fd6d4cc7944bb037f742d62e8e447a575c0028" + ], + "index": "pypi", + "version": "==0.1.2" + }, "django-ipware": { "hashes": [ "sha256:a7c7a8fd019dbdc9c357e6e582f65034e897572fc79a7e467674efa8aef9d00b" @@ -96,6 +103,13 @@ "index": "pypi", "version": "==3.0.0" }, + "humanize": { + "hashes": [ + "sha256:98b7ac9d1a70ad62175c8e0dd44beebbd92418727fc4e214468dfb2baa8ebfb5", + "sha256:bc2a1ff065977011de2bc36197a4b14730c54bfc46ab12a153376684573a2dab" + ], + "version": "==2.3.0" + }, "idna": { "hashes": [ "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb", diff --git a/shynet/a17t/templates/a17t/head.html b/shynet/a17t/templates/a17t/head.html index 690893f..8957004 100644 --- a/shynet/a17t/templates/a17t/head.html +++ b/shynet/a17t/templates/a17t/head.html @@ -1,2 +1,3 @@ + + - \ No newline at end of file diff --git a/shynet/analytics/migrations/0004_auto_20200411_1541.py b/shynet/analytics/migrations/0004_auto_20200411_1541.py new file mode 100644 index 0000000..444f27d --- /dev/null +++ b/shynet/analytics/migrations/0004_auto_20200411_1541.py @@ -0,0 +1,34 @@ +# Generated by Django 3.0.5 on 2020-04-11 19:41 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('analytics', '0003_auto_20200410_1325'), + ] + + operations = [ + migrations.RenameField( + model_name='hit', + old_name='start', + new_name='start_time', + ), + migrations.RenameField( + model_name='session', + old_name='first_seen', + new_name='start_time', + ), + migrations.RemoveField( + model_name='hit', + name='duration', + ), + migrations.AddField( + model_name='hit', + name='last_seen', + field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), + preserve_default=False, + ), + ] diff --git a/shynet/analytics/models.py b/shynet/analytics/models.py index 2f3218e..1248a7f 100644 --- a/shynet/analytics/models.py +++ b/shynet/analytics/models.py @@ -18,7 +18,7 @@ class Session(models.Model): identifier = models.TextField(blank=True) # Time - first_seen = models.DateTimeField(auto_now_add=True) + start_time = models.DateTimeField(auto_now_add=True) last_seen = models.DateTimeField(auto_now_add=True) # Core request information @@ -40,8 +40,8 @@ class Hit(models.Model): session = models.ForeignKey(Session, on_delete=models.CASCADE) # Base request information - start = models.DateTimeField(auto_now_add=True) - duration = models.FloatField(default=0.0) # Seconds spent on page + start_time = models.DateTimeField(auto_now_add=True) + last_seen = models.DateTimeField(auto_now_add=True) heartbeats = models.IntegerField(default=0) tracker = models.TextField() # Tracking pixel or JS diff --git a/shynet/analytics/tasks.py b/shynet/analytics/tasks.py index 1cf910e..a210900 100644 --- a/shynet/analytics/tasks.py +++ b/shynet/analytics/tasks.py @@ -46,7 +46,7 @@ def ingress_request( try: ip_data = _geoip2_lookup(ip) - service = Service.objects.get(uuid=service_uuid) + service = Service.objects.get(uuid=service_uuid, status=Service.ACTIVE) log.debug(f"Linked to service {service}") # Create or update session @@ -95,7 +95,7 @@ def ingress_request( # this is a heartbeat. log.debug("Hit is a heartbeat; updating old hit with new data...") hit.heartbeats += 1 - hit.duration = (timezone.now() - hit.start).total_seconds() + hit.last_seen = timezone.now() hit.save() if hit is None: log.debug("Hit is a page load; creating new hit...") diff --git a/shynet/core/models.py b/shynet/core/models.py index 7faa66b..5e5105e 100644 --- a/shynet/core/models.py +++ b/shynet/core/models.py @@ -2,6 +2,9 @@ import uuid from django.contrib.auth.models import AbstractUser from django.db import models +from django.apps import apps +from django.utils import timezone +from django.db.utils import NotSupportedError def _default_uuid(): @@ -35,3 +38,63 @@ class Service(models.Model): status = models.CharField( max_length=2, choices=SERVICE_STATUSES, default=ACTIVE, db_index=True ) + + def __str__(self): + return self.name + + def get_daily_stats(self): + return self.get_core_stats( + start_time=timezone.now() - timezone.timedelta(days=1) + ) + + def get_core_stats(self, start_time=None, end_time=None): + if start_time is None: + start_time = timezone.now() - timezone.timedelta(days=30) + if end_time is None: + end_time = timezone.now() + + Session = apps.get_model("analytics", "Session") + Hit = apps.get_model("analytics", "Hit") + + currently_online = Session.objects.filter( + service=self, start_time__gt=timezone.now() - timezone.timedelta(seconds=10) + ).count() + + sessions = Session.objects.filter( + service=self, start_time__gt=start_time, start_time__lt=end_time + ) + session_count = sessions.count() + + hits = Hit.objects.filter( + session__service=self, start_time__lt=end_time, start_time__gt=start_time + ) + hit_count = hits.count() + + bounces = sessions.annotate(hit_count=models.Count("hit")).filter(hit_count=1) + bounce_count = bounces.count() + + try: + avg_session_duration = sessions.annotate( + duration=models.F("last_seen") - models.F("start_time") + ).aggregate(duration=models.Avg("duration"))["duration"] + except NotSupportedError: + avg_session_duration = ( + sum( + [ + (session.last_seen - session.start_time).total_seconds() + for session in sessions + ] + ) + / session_count + ) + + return { + "currently_online": currently_online, + "sessions": session_count, + "hits": hit_count, + "avg_hits_per_session": hit_count / (max(session_count, 1)), + "bounce_rate_pct": bounce_count * 100 / session_count, + "avg_session_duration": avg_session_duration, + "uptime": 99.9, + "online": True, + } diff --git a/shynet/core/templates/account/email.html b/shynet/core/templates/account/email.html index fa8ce30..df69203 100644 --- a/shynet/core/templates/account/email.html +++ b/shynet/core/templates/account/email.html @@ -26,18 +26,18 @@ {{ emailaddress.email }} {% if emailaddress.verified %} - ({% trans "Verified" %}) + {% trans "Verified" %} {% else %} - ({% trans "Unverified" %}) + {% trans "Unverified" %} {% endif %} - {% if emailaddress.primary %}({% trans "Primary" %}){% endif %} + {% if emailaddress.primary %}{% trans "Primary" %}{% endif %} {% endfor %} -
+
- +
@@ -57,7 +57,7 @@
{% csrf_token %} {{ form|a17t }} - +
{% endblock %} diff --git a/shynet/core/templates/account/login.html b/shynet/core/templates/account/login.html index e6b29da..02c3749 100644 --- a/shynet/core/templates/account/login.html +++ b/shynet/core/templates/account/login.html @@ -8,7 +8,7 @@ {% block content %}

{% trans "Sign In" %}

-