Arag / app /models /visitor.py
AuthorBot
feat: proper visitor analytics system - New Visitor model (visitor_uid + fingerprint fallback, first_seen/last_seen/page_views, geo, device) - VisitorRepository with upsert + analytics queries - visitor_tracker.py: record_visitor() upserts on every session/init - session/init accepts visitor_uid in request body (HF-Spaces iframe-safe, no cookies) - 4 new analytics endpoints: /visitors /geo /devices /sessions/stats - geo.py: region extraction + X-Real-IP support - DB migration: visitor_uid column on chat_sessions - ARCHITECTURE.md updated
3ae7860
Raw
History Blame Contribute Delete
2.52 kB
"""Author RAG Chatbot SaaS β€” Visitor Model.
One row per unique visitor per author.
A visitor is identified by visitor_uid (from widget localStorage) with a
server-side SHA-256 fingerprint as fallback.
Privacy rules:
- Raw IP is NEVER stored β€” only country/city derived from it.
- visitor_uid is a random UUID generated server-side (not derived from IP).
- fingerprint is a one-way hash β€” not reversible.
"""
import datetime
from sqlalchemy import DateTime, Index, Integer, String, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column
from app.models.base import Base, generate_uuid
class Visitor(Base):
"""One unique visitor per author.
Created on first session/init. Updated (lastSeen, pageViews) on every
subsequent session/init from the same browser.
"""
__tablename__ = "visitors"
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=generate_uuid)
author_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True)
# Identity β€” primary (localStorage UUID) and fallback (hash)
visitor_uid: Mapped[str] = mapped_column(String(36), nullable=False, index=True)
fingerprint: Mapped[str] = mapped_column(String(64), nullable=False, default="")
# Lifecycle
first_seen: Mapped[datetime.datetime] = mapped_column(DateTime, nullable=False)
last_seen: Mapped[datetime.datetime] = mapped_column(DateTime, nullable=False)
page_views: Mapped[int] = mapped_column(Integer, default=1, nullable=False)
# Geography β€” derived from IP at visit time, never the raw IP
country_code: Mapped[str | None] = mapped_column(String(2), nullable=True)
country_name: Mapped[str | None] = mapped_column(String(100), nullable=True)
region: Mapped[str | None] = mapped_column(String(100), nullable=True)
city: Mapped[str | None] = mapped_column(String(100), nullable=True)
# Device
device_type: Mapped[str | None] = mapped_column(String(20), nullable=True)
browser: Mapped[str | None] = mapped_column(String(100), nullable=True)
os: Mapped[str | None] = mapped_column(String(100), nullable=True)
__table_args__ = (
# Each visitor_uid is unique per author (one row per browser per author)
UniqueConstraint("author_id", "visitor_uid", name="uq_visitor_author_uid"),
Index("ix_visitors_author_fp", "author_id", "fingerprint"),
)
def __repr__(self) -> str:
return f"<Visitor uid={self.visitor_uid[:8]}... author={self.author_id[:8]}...>"