Add basic stats list
This commit is contained in:
parent
6c3064f3ea
commit
15fa8226e0
1
Pipfile
1
Pipfile
@ -15,6 +15,7 @@ django-ipware = "*"
|
||||
pyyaml = "*"
|
||||
ua-parser = "*"
|
||||
user-agents = "*"
|
||||
django-humanize = "*"
|
||||
|
||||
[requires]
|
||||
python_version = "3.6"
|
||||
|
16
Pipfile.lock
generated
16
Pipfile.lock
generated
@ -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",
|
||||
|
@ -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 rel="stylesheet" href="https://cdn.jsdelivr.net/npm/a17t@latest/dist/a17t.css">
|
34
shynet/analytics/migrations/0004_auto_20200411_1541.py
Normal file
34
shynet/analytics/migrations/0004_auto_20200411_1541.py
Normal 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,
|
||||
),
|
||||
]
|
@ -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
|
||||
|
||||
|
@ -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...")
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -26,18 +26,18 @@
|
||||
|
||||
{{ emailaddress.email }}
|
||||
{% if emailaddress.verified %}
|
||||
<span class="support ~positive">({% trans "Verified" %})</span>
|
||||
<span class="badge ~positive">{% trans "Verified" %}</span>
|
||||
{% else %}
|
||||
<span class="support ~warning">({% trans "Unverified" %})</span>
|
||||
<span class="badge ~critical">{% trans "Unverified" %}</span>
|
||||
{% endif %}
|
||||
{% if emailaddress.primary %}<span class="support ~urge">({% trans "Primary" %})</span>{% endif %}
|
||||
{% if emailaddress.primary %}<span class="badge ~urge">{% trans "Primary" %}</span>{% endif %}
|
||||
</label>
|
||||
</div>
|
||||
{% 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 ~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>
|
||||
</div>
|
||||
</fieldset>
|
||||
@ -57,7 +57,7 @@
|
||||
<form method="post" action="{% url 'account_email' %}" class="add_email max-w-lg">
|
||||
{% csrf_token %}
|
||||
{{ 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>
|
||||
|
||||
{% endblock %}
|
||||
|
@ -8,7 +8,7 @@
|
||||
{% block content %}
|
||||
<div class="card ~neutral !low max-w-lg">
|
||||
<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 %}
|
||||
{{ form|a17t }}
|
||||
{% if redirect_field_value %}
|
||||
|
@ -14,32 +14,44 @@
|
||||
<body class="bg-gray-100 min-h-full">
|
||||
{% block body %}
|
||||
|
||||
<section class="max-w-4xl mx-auto px-6 md:py-12">
|
||||
{% if messages %}
|
||||
<div>
|
||||
{% for message in messages %}
|
||||
<article class="card {{message.tags}} !high mb-2 w-full">{{message}}</article>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
<section class="max-w-screen-lg mx-auto px-6 md:py-12 md:flex">
|
||||
<aside class="w-2/12 mr-4 block text-sm">
|
||||
<a class="icon ~urge ml-6 mb-8 mt-3" href="{% url 'core:dashboard' %}">
|
||||
<i class="fas fa-low-vision fa-3x text-purple-600"></i>
|
||||
</a>
|
||||
|
||||
<div>
|
||||
<strong>Menu:</strong>
|
||||
<ul>
|
||||
{% if user.is_authenticated %}
|
||||
<li><a href="{% url 'account_email' %}">Change Email</a></li>
|
||||
<li><a href="{% url 'account_logout' %}">Sign Out</a></li>
|
||||
{% else %}
|
||||
<li><a href="{% url 'account_login' %}">Sign In</a></li>
|
||||
<li><a href="{% url 'account_signup' %}">Sign Up</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% if user.is_authenticated %}
|
||||
|
||||
{% url 'account_email' as url %}
|
||||
{% include 'core/includes/sidebar_portal.html' with label="Emails" url=url %}
|
||||
|
||||
{% url 'account_logout' as url %}
|
||||
{% include 'core/includes/sidebar_portal.html' with label="Log Out" url=url %}
|
||||
|
||||
{% else %}
|
||||
|
||||
{% 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>
|
||||
<main>
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
</main>
|
||||
</section>
|
||||
{% endblock %}
|
||||
{% block extra_body %}
|
||||
|
45
shynet/core/templates/core/includes/service_overview.html
Normal file
45
shynet/core/templates/core/includes/service_overview.html
Normal 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>
|
1
shynet/core/templates/core/includes/sidebar_portal.html
Normal file
1
shynet/core/templates/core/includes/sidebar_portal.html
Normal 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>
|
13
shynet/core/templates/core/pages/dashboard.html
Normal file
13
shynet/core/templates/core/pages/dashboard.html
Normal 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 %}
|
@ -5,4 +5,5 @@ from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path("", views.IndexView.as_view(), name="index"),
|
||||
path("dash/", views.DashboardView.as_view(), name="dashboard"),
|
||||
]
|
||||
|
@ -1,5 +1,10 @@
|
||||
from django.views.generic import TemplateView
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
|
||||
|
||||
class IndexView(TemplateView):
|
||||
template_name = "core/pages/index.html"
|
||||
|
||||
|
||||
class DashboardView(LoginRequiredMixin, TemplateView):
|
||||
template_name = "core/pages/dashboard.html"
|
||||
|
@ -38,6 +38,7 @@ INSTALLED_APPS = [
|
||||
"django.contrib.messages",
|
||||
"django.contrib.staticfiles",
|
||||
"django.contrib.sites",
|
||||
"django_humanize",
|
||||
"a17t",
|
||||
"core",
|
||||
"analytics",
|
||||
@ -158,3 +159,4 @@ MESSAGE_TAGS = {
|
||||
messages.ERROR: "~critical",
|
||||
messages.SUCCESS: "~positive",
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user