Add basic stats list

This commit is contained in:
R. Miles McCain 2020-04-11 16:42:14 -04:00
parent 6c3064f3ea
commit 15fa8226e0
No known key found for this signature in database
GPG Key ID: 91CB47BDDF2671A5
16 changed files with 230 additions and 38 deletions

View File

@ -15,6 +15,7 @@ django-ipware = "*"
pyyaml = "*" pyyaml = "*"
ua-parser = "*" ua-parser = "*"
user-agents = "*" user-agents = "*"
django-humanize = "*"
[requires] [requires]
python_version = "3.6" python_version = "3.6"

16
Pipfile.lock generated
View File

@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "03f23e0c7409df9eb5a85b3df03d1975a1fe5563496602492cdcff46b8c5c829" "sha256": "13b07e6285b739fe6f4987482aa448f1773d051de2de4806126a0e4bded150cf"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
@ -81,6 +81,13 @@
"index": "pypi", "index": "pypi",
"version": "==0.41.0" "version": "==0.41.0"
}, },
"django-humanize": {
"hashes": [
"sha256:32491bf0209b89a277f7bfdab7fd6d4cc7944bb037f742d62e8e447a575c0028"
],
"index": "pypi",
"version": "==0.1.2"
},
"django-ipware": { "django-ipware": {
"hashes": [ "hashes": [
"sha256:a7c7a8fd019dbdc9c357e6e582f65034e897572fc79a7e467674efa8aef9d00b" "sha256:a7c7a8fd019dbdc9c357e6e582f65034e897572fc79a7e467674efa8aef9d00b"
@ -96,6 +103,13 @@
"index": "pypi", "index": "pypi",
"version": "==3.0.0" "version": "==3.0.0"
}, },
"humanize": {
"hashes": [
"sha256:98b7ac9d1a70ad62175c8e0dd44beebbd92418727fc4e214468dfb2baa8ebfb5",
"sha256:bc2a1ff065977011de2bc36197a4b14730c54bfc46ab12a153376684573a2dab"
],
"version": "==2.3.0"
},
"idna": { "idna": {
"hashes": [ "hashes": [
"sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb", "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb",

View File

@ -1,2 +1,3 @@
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/a17t@0.1.3/dist/a17t.css">
<script async src="https://use.fontawesome.com/releases/v5.3.1/js/all.js"></script>
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/a17t@latest/dist/a17t.css">

View File

@ -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,
),
]

View File

@ -18,7 +18,7 @@ class Session(models.Model):
identifier = models.TextField(blank=True) identifier = models.TextField(blank=True)
# Time # 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) last_seen = models.DateTimeField(auto_now_add=True)
# Core request information # Core request information
@ -40,8 +40,8 @@ class Hit(models.Model):
session = models.ForeignKey(Session, on_delete=models.CASCADE) session = models.ForeignKey(Session, on_delete=models.CASCADE)
# Base request information # Base request information
start = models.DateTimeField(auto_now_add=True) start_time = models.DateTimeField(auto_now_add=True)
duration = models.FloatField(default=0.0) # Seconds spent on page last_seen = models.DateTimeField(auto_now_add=True)
heartbeats = models.IntegerField(default=0) heartbeats = models.IntegerField(default=0)
tracker = models.TextField() # Tracking pixel or JS tracker = models.TextField() # Tracking pixel or JS

View File

@ -46,7 +46,7 @@ def ingress_request(
try: try:
ip_data = _geoip2_lookup(ip) 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}") log.debug(f"Linked to service {service}")
# Create or update session # Create or update session
@ -95,7 +95,7 @@ def ingress_request(
# this is a heartbeat. # this is a heartbeat.
log.debug("Hit is a heartbeat; updating old hit with new data...") log.debug("Hit is a heartbeat; updating old hit with new data...")
hit.heartbeats += 1 hit.heartbeats += 1
hit.duration = (timezone.now() - hit.start).total_seconds() hit.last_seen = timezone.now()
hit.save() hit.save()
if hit is None: if hit is None:
log.debug("Hit is a page load; creating new hit...") log.debug("Hit is a page load; creating new hit...")

View File

@ -2,6 +2,9 @@ import uuid
from django.contrib.auth.models import AbstractUser from django.contrib.auth.models import AbstractUser
from django.db import models from django.db import models
from django.apps import apps
from django.utils import timezone
from django.db.utils import NotSupportedError
def _default_uuid(): def _default_uuid():
@ -35,3 +38,63 @@ class Service(models.Model):
status = models.CharField( status = models.CharField(
max_length=2, choices=SERVICE_STATUSES, default=ACTIVE, db_index=True 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,
}

View File

@ -26,18 +26,18 @@
{{ emailaddress.email }} {{ emailaddress.email }}
{% if emailaddress.verified %} {% if emailaddress.verified %}
<span class="support ~positive">({% trans "Verified" %})</span> <span class="badge ~positive">{% trans "Verified" %}</span>
{% else %} {% else %}
<span class="support ~warning">({% trans "Unverified" %})</span> <span class="badge ~critical">{% trans "Unverified" %}</span>
{% endif %} {% endif %}
{% if emailaddress.primary %}<span class="support ~urge">({% trans "Primary" %})</span>{% endif %} {% if emailaddress.primary %}<span class="badge ~urge">{% trans "Primary" %}</span>{% endif %}
</label> </label>
</div> </div>
{% endfor %} {% endfor %}
<div class="block"> <div class="block mt-4">
<button class="button ~urge mb-1" type="submit" name="action_primary">{% trans 'Make Primary' %}</button> <button class="button ~urge mb-1" type="submit" name="action_primary">{% trans 'Make Primary' %}</button>
<button class="button ~info mb-1" type="submit" name="action_send">{% trans 'Re-send Verification' %}</button> <button class="button ~info mb-1" type="submit" name="action_send">{% trans 'Resend Verification' %}</button>
<button class="button ~critical mb-1" type="submit" name="action_remove">{% trans 'Remove' %}</button> <button class="button ~critical mb-1" type="submit" name="action_remove">{% trans 'Remove' %}</button>
</div> </div>
</fieldset> </fieldset>
@ -57,7 +57,7 @@
<form method="post" action="{% url 'account_email' %}" class="add_email max-w-lg"> <form method="post" action="{% url 'account_email' %}" class="add_email max-w-lg">
{% csrf_token %} {% csrf_token %}
{{ form|a17t }} {{ form|a17t }}
<button name="button ~neutral !high" type="submit">{% trans "Add Email" %}</button> <button name="action_add" class="button ~neutral !high" type="submit">{% trans "Add Email" %}</button>
</form> </form>
{% endblock %} {% endblock %}

View File

@ -8,7 +8,7 @@
{% block content %} {% block content %}
<div class="card ~neutral !low max-w-lg"> <div class="card ~neutral !low max-w-lg">
<h1 class="heading mb-3">{% trans "Sign In" %}</h1> <h1 class="heading mb-3">{% trans "Sign In" %}</h1>
<form class="login" method="POST" action="{% url 'account_login' %} max-w-lg"> <form class="login" method="POST" action="{% url 'account_login' %}">
{% csrf_token %} {% csrf_token %}
{{ form|a17t }} {{ form|a17t }}
{% if redirect_field_value %} {% if redirect_field_value %}

View File

@ -14,32 +14,44 @@
<body class="bg-gray-100 min-h-full"> <body class="bg-gray-100 min-h-full">
{% block body %} {% block body %}
<section class="max-w-4xl mx-auto px-6 md:py-12"> <section class="max-w-screen-lg mx-auto px-6 md:py-12 md:flex">
{% if messages %} <aside class="w-2/12 mr-4 block text-sm">
<div> <a class="icon ~urge ml-6 mb-8 mt-3" href="{% url 'core:dashboard' %}">
{% for message in messages %} <i class="fas fa-low-vision fa-3x text-purple-600"></i>
<article class="card {{message.tags}} !high mb-2 w-full">{{message}}</article> </a>
{% endfor %}
</ul>
</div>
{% endif %}
<div> {% if user.is_authenticated %}
<strong>Menu:</strong>
<ul> {% url 'account_email' as url %}
{% if user.is_authenticated %} {% include 'core/includes/sidebar_portal.html' with label="Emails" url=url %}
<li><a href="{% url 'account_email' %}">Change Email</a></li>
<li><a href="{% url 'account_logout' %}">Sign Out</a></li> {% url 'account_logout' as url %}
{% else %} {% include 'core/includes/sidebar_portal.html' with label="Log Out" url=url %}
<li><a href="{% url 'account_login' %}">Sign In</a></li>
<li><a href="{% url 'account_signup' %}">Sign Up</a></li> {% else %}
{% endif %}
</ul> {% url 'account_login' as url %}
{% include 'core/includes/sidebar_portal.html' with label="Log In" url=url %}
{% url 'account_signup' as url %}
{% include 'core/includes/sidebar_portal.html' with label="Sign Up" url=url %}
{% endif %}
</aside>
<div class="flex-grow">
{% if messages %}
<div>
{% for message in messages %}
<article class="card {{message.tags}} !high mb-2 w-full">{{message}}</article>
{% endfor %}
</ul>
</div>
{% endif %}
<main>
{% block content %}
{% endblock %}
</main>
</div> </div>
<main>
{% block content %}
{% endblock %}
</main>
</section> </section>
{% endblock %} {% endblock %}
{% block extra_body %} {% block extra_body %}

View File

@ -0,0 +1,45 @@
{% load humanizelib %}
<article class="card ~neutral !low service mb-4">
{% with stats=service.get_daily_stats %}
<div class="md:flex justify-between">
<div class="mr-4 md:w-1/4">
<h3 class="heading text-gray-700 text-2xl mr-2 leading-none">
{{service.name}}
</h3>
{% if stats.currently_online > 0 %}
<span class="badge ~positive">
{{stats.currently_online|intcomma}} online
</span>
{% elif stats.online == True %}
<span class="badge ~positive">
Online
</span>
{% elif stats.online == False %}
<span class="badge ~critical">
Offline
</span>
{% endif %}
</div>
<div class="mr-2">
<p class="font-medium text-sm">Sessions</p>
<p class="text-xl text-purple-700 font-medium">{{stats.sessions|intcomma}}</p>
</div>
<div class="mr-2">
<p class="font-medium text-sm">Pages/Session</p>
<p class="text-xl text-purple-700 font-medium">{{stats.avg_hits_per_session|floatformat:"-1"}}</p>
</div>
<div class="mr-2">
<p class="font-medium text-sm">Bounce Rate</p>
<p class="text-xl text-purple-700 font-medium">{{stats.bounce_rate_pct|floatformat:"-1"}}%</p>
</div>
<div class="mr-2">
<p class="font-medium text-sm">Avg. Session</p>
<p class="text-xl text-purple-700 font-medium">{{stats.avg_session_duration|naturaldelta}}</p>
</div>
<div class="mr-2">
<p class="font-medium text-sm">Uptime</p>
<p class="text-xl text-purple-700 font-medium">{{stats.uptime|floatformat:"-1"}}%</p>
</div>
</div>
{% endwith %}
</article>

View File

@ -0,0 +1 @@
<a class="portal !low w-full {% if url == request.get_full_path %}~urge active bg-gray-100{% endif %}" href="{{url}}">{{label}}</a>

View File

@ -0,0 +1,13 @@
{% extends "base.html" %}
{% block content %}
<h4 class="heading text-lg leading-none text-purple-600">Shynet</h4>
<h4 class="heading text-4xl leading-none">Dashboard</h4>
<hr class="sep">
{% for service in user.owning_services.all %}
{% include 'core/includes/service_overview.html' %}
{% empty %}
<p>You don't have any services.</p>
{% endfor %}
{% endblock %}

View File

@ -5,4 +5,5 @@ from . import views
urlpatterns = [ urlpatterns = [
path("", views.IndexView.as_view(), name="index"), path("", views.IndexView.as_view(), name="index"),
path("dash/", views.DashboardView.as_view(), name="dashboard"),
] ]

View File

@ -1,5 +1,10 @@
from django.views.generic import TemplateView from django.views.generic import TemplateView
from django.contrib.auth.mixins import LoginRequiredMixin
class IndexView(TemplateView): class IndexView(TemplateView):
template_name = "core/pages/index.html" template_name = "core/pages/index.html"
class DashboardView(LoginRequiredMixin, TemplateView):
template_name = "core/pages/dashboard.html"

View File

@ -38,6 +38,7 @@ INSTALLED_APPS = [
"django.contrib.messages", "django.contrib.messages",
"django.contrib.staticfiles", "django.contrib.staticfiles",
"django.contrib.sites", "django.contrib.sites",
"django_humanize",
"a17t", "a17t",
"core", "core",
"analytics", "analytics",
@ -158,3 +159,4 @@ MESSAGE_TAGS = {
messages.ERROR: "~critical", messages.ERROR: "~critical",
messages.SUCCESS: "~positive", messages.SUCCESS: "~positive",
} }