Visitantes del Perfil — Referencia Técnica
← Volver a Visitantes del Perfil
Actualizado — la conexión faltante del lado de escritura descrita a continuación ha sido corregida: PublicProfilePage.tsx ahora llama a recordProfileVisit al cargar. La funcionalidad está completamente operativa de extremo a extremo a partir de este cambio.
Dónde vive esto
Backend
apps/backend/graphql/types/user-profile-view.type.js— esquema paramyProfileViewers,myProfileViewerCount,recordProfileVisit,clearMyProfileViewersapps/backend/graphql/resolvers/user-profile-view.resolver.js— los cuatro resolvers, todos requieren autenticaciónapps/backend/managers/user-managers/profile-view.manager.js—recordVisit,getMyViewers,getViewerCount,clearMyViewersapps/backend/data-access-services/user/user-profile-view.access-service.js— acceso a la base de datos, incluyendo una ventana de debounce de 60 segundos (findRecent/DEBOUNCE_WINDOW_MS) para evitar contar dos veces visitas repetidas rápidas- Tabla dedicada, no montada sobre nada más:
user_profile_view(modeloapps/backend/database/models/UserProfileView.js, migración20260717010000-create-user-profile-view.js) — columnasviewer_id,viewed_user_id,viewed_at,created_at, con un índice compuesto en(viewed_user_id, viewed_at DESC).
Frontend
apps/frontend-nextjs/src/page-components/settings/ProfileViewersPage.tsx— consultamyProfileViewers(limit: 50, offset: 0)ymyProfileViewerCountal montar (network-only), renderiza la lista + el conteo, y un botón Limpiar que llama aclearMyProfileViewers.limit: 50está fijado de forma estática — no hay interfaz de paginación pese a que el resolver aceptaoffset.- Ruta:
apps/frontend-nextjs/src/app/settings/profile-viewers/page.tsx - Puntos de entrada:
SettingsPage.tsx(el elemento de configuración "Visitantes del perfil") yProfilePage.tsx(un elemento de menú en tu propio perfil), ambos enlazando a/settings/profile-viewers. recordProfileVisitahora se llama desdePublicProfilePage.tsx(corregido en este cambio) — unuseEffectse dispara una vez por carga de página, reutilizando la verificación existenteisOwnProfilede la página para omitir las auto-visitas, y solo se dispara para visitantes autenticados. La mutación absorbe sus propios errores del lado del cliente (.catch(() => {})) para que una llamada fallida nunca rompa la página de perfil. La protección propia del backend de no-op en auto-visitas y el debounce de 60 segundos siguen aplicándose como una segunda capa de protección.
Checklist de implementación técnica
-
myProfileViewers— query real, deduplica a una fila por visitante distinto (visita más reciente) en el código de la aplicación (profile-view.manager.js) -
myProfileViewerCount— query real -
clearMyProfileViewers— elimina todas las filas del que llama -
recordProfileVisit(backend) — completamente implementado: requiere autenticación, no hace nada en auto-visitas, debounce de 60s, verifica el flaghide_profile_visits(ver abajo), que es real y está conectado. Corrección: el argumento se llamatargetUserId, noviewedUserIdcomo asumía una versión anterior de este documento — verifica contra el archivo de tipos en lugar de este documento si tienes dudas. -
recordProfileVisit(punto de llamada del frontend) — corregido en este cambio.PublicProfilePage.tsxahora lo llama en unuseEffectal cargar, protegido para omitir auto-visitas y visitantes no autenticados. - flag de privacidad
hide_profile_visits— real y conectado de extremo a extremo: por defecto esfalseen el objeto de configuración predeterminada deprivacy-settings.manager.js, incluido en la lista blanca booleana devalidatePrivacySettings, expuesto comohideProfileVisitsen el tipo GraphQLPrivacySettingsy su input, y tiene un interruptor enAccountPrivacyPage.tsx. Se aplica en la verificación de control deprofile-view.manager.js— un visitante que lo tenga activado no registra ninguna visita para ningún perfil que navegue. - Sin gating premium/monedas — gratis para todos los usuarios autenticados.
- Sin trabajo de retención/expiración — el historial es ilimitado hasta que el usuario llama a
clearMyProfileViewers. - No existe implementación en iOS (confirmado mediante grep en
apps/ios).
API GraphQL
query MyProfileViewers($limit: Int, $offset: Int) {
myProfileViewers(limit: $limit, offset: $offset) {
viewer { id username profilePicture isVerified }
viewedAt
}
}
query MyProfileViewerCount { myProfileViewerCount }
# Now called from PublicProfilePage.tsx on load (fixed this pass)
mutation RecordProfileVisit($targetUserId: ID!) {
recordProfileVisit(targetUserId: $targetUserId) # returns Boolean!, not an object
}
mutation ClearMyProfileViewers { clearMyProfileViewers } # also returns Boolean!, not an object