Add basic stats list
This commit is contained in:
parent
6c3064f3ea
commit
15fa8226e0
1
Pipfile
1
Pipfile
@ -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
16
Pipfile.lock
generated
@ -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",
|
||||||
|
@ -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">
|
|
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)
|
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
|
||||||
|
|
||||||
|
@ -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...")
|
||||||
|
@ -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,
|
||||||
|
}
|
||||||
|
@ -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 %}
|
||||||
|
@ -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 %}
|
||||||
|
@ -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 %}
|
||||||
|
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 = [
|
urlpatterns = [
|
||||||
path("", views.IndexView.as_view(), name="index"),
|
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.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"
|
||||||
|
@ -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",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user