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