This commit is contained in:
2026-08-31 15:11:52 +03:30
parent 236002a87c
commit 463b812e3e
63 changed files with 3254 additions and 1476 deletions
View File
+31
View File
@@ -0,0 +1,31 @@
from rest_framework import serializers
class TelemetryEventSerializer(serializers.Serializer):
"""یک رویدادِ خام از اپ — نوع‌اش تعیین می‌کند کدام فیلدها معنی دارند.
عمداً یک سریالایزرِ آزاد (نه یکی برای هر مدل) چون این اندپوینت دسته‌ای
(batch) کار می‌کند و ترکیبِ چند نوع رویداد را با هم می‌گیرد."""
type = serializers.ChoiceField(
choices=["session_start", "session_end", "page_view", "click", "error"]
)
at = serializers.DateTimeField()
screen = serializers.CharField(max_length=150, required=False, allow_blank=True, default="")
duration_seconds = serializers.FloatField(required=False, allow_null=True, default=None)
element = serializers.CharField(max_length=150, required=False, allow_blank=True, default="")
x = serializers.FloatField(required=False, allow_null=True, default=None)
y = serializers.FloatField(required=False, allow_null=True, default=None)
message = serializers.CharField(required=False, allow_blank=True, default="")
stack = serializers.CharField(required=False, allow_blank=True, default="")
class TelemetryBatchSerializer(serializers.Serializer):
session_id = serializers.CharField(max_length=36)
platform = serializers.CharField(max_length=30, required=False, allow_blank=True, default="")
app_version = serializers.CharField(max_length=30, required=False, allow_blank=True, default="")
events = TelemetryEventSerializer(many=True)
def validate_events(self, value):
if not value:
raise serializers.ValidationError("حداقل یک رویداد لازم است.")
return value
+60
View File
@@ -0,0 +1,60 @@
from django.db import transaction
from apps.account.models import User
from apps.telemetry.models import AppSession, ClickEvent, ClientErrorLog, PageView
class TelemetryIngestService:
@staticmethod
@transaction.atomic
def ingest(user: User, ip_address: str | None, data: dict) -> None:
session, _created = AppSession.objects.get_or_create(
id=data["session_id"],
defaults={
"user": user,
"platform": data.get("platform", ""),
"app_version": data.get("app_version", ""),
"ip_address": ip_address,
"started_at": data["events"][0]["at"],
},
)
# اگر آی‌پی از قبل ثبت نشده یا عوض شده، آخرین مقدار را نگه می‌داریم —
# همان چیزی که «لیست آی‌پی‌هایی که کاربر باهاش اومده» را می‌سازد (از
# طریق تجمیعِ همه‌ی session های همان کاربر، نه فقط این یکی).
if ip_address and session.ip_address != ip_address:
session.ip_address = ip_address
session.save(update_fields=["ip_address"])
for event in data["events"]:
event_type = event["type"]
if event_type == "session_start":
if session.started_at != event["at"]:
session.started_at = min(session.started_at, event["at"])
session.save(update_fields=["started_at"])
elif event_type == "session_end":
session.ended_at = event["at"]
session.save(update_fields=["ended_at"])
elif event_type == "page_view":
PageView.objects.create(
session=session,
screen_name=event.get("screen", ""),
entered_at=event["at"],
duration_seconds=event.get("duration_seconds"),
)
elif event_type == "click":
ClickEvent.objects.create(
session=session,
screen_name=event.get("screen", ""),
element=event.get("element", ""),
x=event.get("x"),
y=event.get("y"),
created_at=event["at"],
)
elif event_type == "error":
ClientErrorLog.objects.create(
session=session,
screen_name=event.get("screen", ""),
message=event.get("message", ""),
stack_trace=event.get("stack", ""),
created_at=event["at"],
)
+24
View File
@@ -0,0 +1,24 @@
from rest_framework import status
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from .serializers import TelemetryBatchSerializer
from .services import TelemetryIngestService
class TelemetryEventsView(APIView):
"""اپ فلاتر دوره‌ای (و موقعِ بستن) یک دسته رویداد اینجا می‌فرستد —
صفحه‌بازدید، کلیک، خطا، شروع/پایانِ جلسه. لاگین لازم است چون این
مانیتورینگ «به‌ازای کاربر» است، نه ترافیکِ ناشناس."""
permission_classes = [IsAuthenticated]
def post(self, request):
serializer = TelemetryBatchSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
ip = request.META.get("HTTP_X_FORWARDED_FOR", "").split(",")[0].strip() or request.META.get(
"REMOTE_ADDR"
)
TelemetryIngestService.ingest(request.user, ip, serializer.validated_data)
return Response({"success": True}, status=status.HTTP_201_CREATED)