adding
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
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")
|
||||
Reference in New Issue
Block a user