184 lines
7.9 KiB
Python
184 lines
7.9 KiB
Python
|
|
from datetime import timedelta
|
||
|
|
|
||
|
|
from django.contrib import admin
|
||
|
|
from django.db.models import Avg, Count, DurationField, ExpressionWrapper, F, Max, Q, Sum
|
||
|
|
from django.template.response import TemplateResponse
|
||
|
|
from django.utils import timezone
|
||
|
|
from unfold.admin import ModelAdmin
|
||
|
|
|
||
|
|
from apps.account.models import User
|
||
|
|
|
||
|
|
from .models import ApiCallLog, AppSession, ClickEvent, ClientErrorLog, PageView
|
||
|
|
|
||
|
|
# مانیتورینگِ کلیِ اپ توسط پنل ادمین: صفحهی changelist مدلِ AppSession
|
||
|
|
# جایگزینِ یک داشبوردِ تجمیعی میشود (نه لیستِ خامِ ردیفها) — برای دیدنِ
|
||
|
|
# لیستِ خام هر رکورد، لینکِ «نمای خام» بالای همان صفحه با ?raw=1 به همان
|
||
|
|
# changelist معمولیِ جنگو میرود.
|
||
|
|
|
||
|
|
_WINDOW_DAYS = 30
|
||
|
|
|
||
|
|
|
||
|
|
@admin.register(AppSession)
|
||
|
|
class AppSessionAdmin(ModelAdmin):
|
||
|
|
list_display = ("user", "platform", "started_at", "ended_at", "ip_address")
|
||
|
|
list_filter = ("platform",)
|
||
|
|
search_fields = ("user__phone", "ip_address")
|
||
|
|
date_hierarchy = "started_at"
|
||
|
|
|
||
|
|
def changelist_view(self, request, extra_context=None):
|
||
|
|
if request.GET.get("raw") == "1":
|
||
|
|
return super().changelist_view(request, extra_context)
|
||
|
|
return self._dashboard_view(request)
|
||
|
|
|
||
|
|
def _dashboard_view(self, request):
|
||
|
|
context = {
|
||
|
|
**self.admin_site.each_context(request),
|
||
|
|
"title": "مانیتورینگ اپ",
|
||
|
|
"opts": self.model._meta,
|
||
|
|
**self._build_dashboard_context(),
|
||
|
|
}
|
||
|
|
return TemplateResponse(request, "admin/telemetry/dashboard.html", context)
|
||
|
|
|
||
|
|
def _build_dashboard_context(self):
|
||
|
|
now = timezone.now()
|
||
|
|
cutoff = now - timedelta(days=_WINDOW_DAYS)
|
||
|
|
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||
|
|
|
||
|
|
sessions_qs = AppSession.objects.filter(started_at__gte=cutoff)
|
||
|
|
api_qs = ApiCallLog.objects.filter(created_at__gte=cutoff)
|
||
|
|
pageviews_qs = PageView.objects.filter(entered_at__gte=cutoff)
|
||
|
|
errors_qs = ClientErrorLog.objects.filter(created_at__gte=cutoff)
|
||
|
|
|
||
|
|
overview = {
|
||
|
|
"active_users_today": sessions_qs.filter(started_at__gte=today_start)
|
||
|
|
.values("user")
|
||
|
|
.distinct()
|
||
|
|
.count(),
|
||
|
|
"sessions_today": sessions_qs.filter(started_at__gte=today_start).count(),
|
||
|
|
"sessions_window": sessions_qs.count(),
|
||
|
|
"page_views_today": pageviews_qs.filter(entered_at__gte=today_start).count(),
|
||
|
|
"api_calls_today": api_qs.filter(created_at__gte=today_start).count(),
|
||
|
|
"errors_today": errors_qs.filter(created_at__gte=today_start).count(),
|
||
|
|
"avg_response_ms": round(api_qs.aggregate(v=Avg("duration_ms"))["v"] or 0, 1),
|
||
|
|
}
|
||
|
|
|
||
|
|
ended = sessions_qs.exclude(ended_at__isnull=True).annotate(
|
||
|
|
dur=ExpressionWrapper(F("ended_at") - F("started_at"), output_field=DurationField())
|
||
|
|
)
|
||
|
|
avg_duration = ended.aggregate(v=Avg("dur"))["v"]
|
||
|
|
overview["avg_session_minutes"] = round(avg_duration.total_seconds() / 60, 1) if avg_duration else 0
|
||
|
|
|
||
|
|
top_screens = list(
|
||
|
|
pageviews_qs.values("screen_name")
|
||
|
|
.annotate(count=Count("id"), avg_duration=Avg("duration_seconds"))
|
||
|
|
.order_by("-count")[:10]
|
||
|
|
)
|
||
|
|
max_screen_count = max((s["count"] for s in top_screens), default=1) or 1
|
||
|
|
for s in top_screens:
|
||
|
|
s["bar_percent"] = round(100 * s["count"] / max_screen_count)
|
||
|
|
s["avg_duration"] = round(s["avg_duration"], 1) if s["avg_duration"] else None
|
||
|
|
|
||
|
|
endpoints = list(
|
||
|
|
api_qs.values("path", "method")
|
||
|
|
.annotate(
|
||
|
|
count=Count("id"),
|
||
|
|
avg_ms=Avg("duration_ms"),
|
||
|
|
max_ms=Max("duration_ms"),
|
||
|
|
error_count=Count("id", filter=Q(status_code__gte=400)),
|
||
|
|
)
|
||
|
|
.order_by("-count")[:20]
|
||
|
|
)
|
||
|
|
for e in endpoints:
|
||
|
|
e["avg_ms"] = round(e["avg_ms"], 1) if e["avg_ms"] else 0
|
||
|
|
|
||
|
|
recent_errors = list(errors_qs.select_related("session__user")[:20])
|
||
|
|
|
||
|
|
active_user_ids = sessions_qs.values_list("user_id", flat=True).distinct()
|
||
|
|
user_rows = []
|
||
|
|
for user in User.objects.filter(id__in=active_user_ids):
|
||
|
|
user_sessions = sessions_qs.filter(user=user)
|
||
|
|
ended_user = user_sessions.exclude(ended_at__isnull=True).annotate(
|
||
|
|
dur=ExpressionWrapper(F("ended_at") - F("started_at"), output_field=DurationField())
|
||
|
|
)
|
||
|
|
total_dur = ended_user.aggregate(v=Sum("dur"))["v"]
|
||
|
|
ips = list(
|
||
|
|
user_sessions.exclude(ip_address__isnull=True)
|
||
|
|
.values_list("ip_address", flat=True)
|
||
|
|
.distinct()
|
||
|
|
)
|
||
|
|
top_screen = (
|
||
|
|
pageviews_qs.filter(session__user=user)
|
||
|
|
.values("screen_name")
|
||
|
|
.annotate(c=Count("id"))
|
||
|
|
.order_by("-c")
|
||
|
|
.first()
|
||
|
|
)
|
||
|
|
last_session = user_sessions.order_by("-started_at").first()
|
||
|
|
user_rows.append(
|
||
|
|
{
|
||
|
|
"user": user,
|
||
|
|
"session_count": user_sessions.count(),
|
||
|
|
"total_minutes": round(total_dur.total_seconds() / 60, 1) if total_dur else 0,
|
||
|
|
"page_view_count": pageviews_qs.filter(session__user=user).count(),
|
||
|
|
"api_call_count": api_qs.filter(user=user).count(),
|
||
|
|
"error_count": errors_qs.filter(session__user=user).count(),
|
||
|
|
"ips": ips,
|
||
|
|
"referral_count": User.objects.filter(
|
||
|
|
referred_by=user, referral_bonus_granted=True
|
||
|
|
).count(),
|
||
|
|
"top_screen": top_screen["screen_name"] if top_screen else "—",
|
||
|
|
"last_seen": last_session.started_at if last_session else None,
|
||
|
|
}
|
||
|
|
)
|
||
|
|
user_rows.sort(key=lambda r: r["total_minutes"], reverse=True)
|
||
|
|
|
||
|
|
overview_cards = [
|
||
|
|
("کاربران فعال امروز", overview["active_users_today"]),
|
||
|
|
("جلسههای امروز", overview["sessions_today"]),
|
||
|
|
(f"جلسهها ({_WINDOW_DAYS} روز اخیر)", overview["sessions_window"]),
|
||
|
|
("میانگین طول جلسه (دقیقه)", overview["avg_session_minutes"]),
|
||
|
|
("صفحهبازدید امروز", overview["page_views_today"]),
|
||
|
|
("فراخوانی API امروز", overview["api_calls_today"]),
|
||
|
|
("میانگین زمان پاسخ (ms)", overview["avg_response_ms"]),
|
||
|
|
("خطای امروز", overview["errors_today"]),
|
||
|
|
]
|
||
|
|
|
||
|
|
return {
|
||
|
|
"overview": overview,
|
||
|
|
"overview_cards": overview_cards,
|
||
|
|
"top_screens": top_screens,
|
||
|
|
"endpoints": endpoints,
|
||
|
|
"recent_errors": recent_errors,
|
||
|
|
"user_rows": user_rows[:50],
|
||
|
|
"window_days": _WINDOW_DAYS,
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
@admin.register(ApiCallLog)
|
||
|
|
class ApiCallLogAdmin(ModelAdmin):
|
||
|
|
list_display = ("method", "path", "status_code", "duration_ms", "ip_address", "user", "created_at")
|
||
|
|
list_filter = ("method", "status_code")
|
||
|
|
search_fields = ("path", "user__phone", "ip_address")
|
||
|
|
date_hierarchy = "created_at"
|
||
|
|
|
||
|
|
|
||
|
|
@admin.register(PageView)
|
||
|
|
class PageViewAdmin(ModelAdmin):
|
||
|
|
list_display = ("screen_name", "session", "entered_at", "duration_seconds")
|
||
|
|
list_filter = ("screen_name",)
|
||
|
|
search_fields = ("screen_name",)
|
||
|
|
|
||
|
|
|
||
|
|
@admin.register(ClickEvent)
|
||
|
|
class ClickEventAdmin(ModelAdmin):
|
||
|
|
list_display = ("screen_name", "element", "session", "created_at")
|
||
|
|
list_filter = ("screen_name",)
|
||
|
|
search_fields = ("screen_name", "element")
|
||
|
|
|
||
|
|
|
||
|
|
@admin.register(ClientErrorLog)
|
||
|
|
class ClientErrorLogAdmin(ModelAdmin):
|
||
|
|
list_display = ("screen_name", "message", "session", "created_at")
|
||
|
|
list_filter = ("screen_name",)
|
||
|
|
search_fields = ("screen_name", "message")
|