Dashboard de analítica
El panel de administración expone consultas de analítica granular para estadísticas de plataforma, crecimiento de usuarios, engagement, ingresos, contenido y salud del sistema. No existe una única query compuesta de "dashboard" — cada métrica es su propio campo de GraphQL. Todas estas (excepto adminGetContentStats, adminGetSystemHealth, y adminExportAnalytics) ya están conectadas a la página /dashboard.
Una nota sobre las correcciones de esta sesión: cada método a continuación excepto adminGetPlatformStats y adminGetUserStats (ambos corregidos en un pase anterior) devolvía una forma de objeto que no coincidía en absoluto con su tipo de GraphQL - campos no-nulos faltantes, campos con nombres distintos (canceled_at vs el cancelledAt del modelo, growth_rate vs growthRate), o llamaba a métodos de data-access-service que no existen (findByDateRange, getTopHashtags, findTopByFollowers, getUserPostCounts, y otros - ninguno de estos era un método real en ningún lugar del código). Cada uno fue reescrito contra los modelos reales de Sequelize y el esquema real de GraphQL. En el camino se agregaron algunos métodos nuevos de data-access-service (findByDateRange/countMediaByDateRange de post.access-service.js, countByDateRange de message.access-service.js, findRecent de user-moderation-log.access-service.js) ya que los métodos que el código anterior asumía que existían simplemente no existían.
Checklist de implementación
- Estadísticas generales de la plataforma (
adminGetPlatformStats- conectado a la página del dashboard) - Crecimiento de usuarios a lo largo del tiempo (
adminGetUserGrowth- conectado a la página del dashboard.retentionRateahora prefiere datos reales de cohortes de registro, corregido en esta sesión:user_cohort_snapshots(escrita a diario porservices/analytics-snapshot.service.js#runCohortSnapshot, ver más abajo) entrega una lectura real de retención Day-N -sum(active_count) / sum(cohort_size)entre las cohortes cuyo día de registro cae dentro de un bucket determinado - en lugar de usar siempre el proxy anterior. Bootstrap con degradación elegante: un bucket sin instantánea de cohorte que lo cubra todavía (instalación nueva, o el cron aún no lleva suficiente tiempo corriendo para cubrirlo - aproximadamente los primeros ~30 días tras este lanzamiento) recae en el proxy en vivo original sin cambios: la fracción de usuarios que existían antes del bucket y que volvieron a estar activos durante este (mediantecreatedAt/lastActiveAt), que sigue sin ser una retención real por cohortes y esnullúnicamente cuando no hay una base de usuarios preexistente contra la cual medir.) - Estadísticas de engagement (
adminGetEngagementStats- corregido en esta sesión, conectado a la página del dashboard) - Estadísticas de contenido (
adminGetContentStats- resolver enadmin/admin-dashboard.resolver.jsprotegido por el permiso VIEW_ANALYTICS →adminDashboardManager.getContentStats, que devuelve conteos reales de posts/medios/hashtags que coinciden con el tipoContentStats) - Estadísticas de creación de contenido por período (
adminGetContentCreationStats- conectado a la página del dashboard. Corregido en esta sesión:period/startDate/endDateahora sí se respetan -totalPosts/totalMedia/totalHashtags/trendingHashtagsquedan delimitados a la ventana[startDate, endDate]resultante (las fechas explícitas tienen prioridad, si no una ventana retroactiva dimensionada segúnperiod), víapostAccessService.findByDateRange()ycountMediaByDateRange(), ambos añadidos en esta sesión ya que ninguno existía antes.postsToday/mediaToday/postsWeekmantienen intencionalmente su significado literal de "hoy"/"últimos 7 días desde ahora" sin importar la ventana solicitada, coincidiendo con lo que esos nombres de campo significan según el esquema - no forman parte de la delimitación por período. Antes estos argumentos se aceptaban pero se ignoraban silenciosamente, y la consulta siempre devolvía la misma instantánea de todo-el-tiempo queadminGetContentStats, sin importar lo que se pidiera.) - Estadísticas de ingresos (
adminGetRevenueStats- corregido en esta sesión, conectado a la página del dashboard. Los ingresos son solo filas completadas dePaymentTransaction, coincidiendo con la definición deadminGetPlatformStats- las compras de monedas y las propinas no están incluidas.) - Estadísticas de la cola de moderación (
adminGetModerationQueueStats- corregido en esta sesión, conectado a la página del dashboard.flaggedContent/reportedContentreutilizan ambos el mismo conteo de reportes pendientes (ver la nota sobre contenido marcado en Moderación de contenido).autoFlaggedahora refleja una señal real, también corregido en esta sesión: antes devolvía un0fijo detrás de un comentario obsoleto que afirmaba que no existía ningún sistema de auto-moderación - eso ya no es cierto (ver la sección Reglas de auto-moderación), así que ahora cuenta las filas decontent_moderation_logsescritas conaction: 'flag'ymoderator_id: 'system'- el rastro que dejaservices/auto-moderation.service.jscada vez que una regla coincide y llama acontentModerationManager.flagContent()con el actor'system'. Igual que el resto de los conteos de este método, es una instantánea de la cola de todo-el-tiempo, no delimitada a una ventana temporal.) - Mejor contenido por métrica (
adminGetTopContent- corregido en esta sesión, conectado a la página del dashboard. Su argumentocontentType: Stringahora también respetaCOMMENT, no solo posts: antes el método nunca desestructurabacontentTypeen absoluto, así que toda llamada devolvía posts sin importar lo que se pidiera.PostCommentsolo registralikesCount/repliesCount(no existen contadores de vistas/compartidos para comentarios), así queLIKESse mapea alikesCount,COMMENTS(más respondidos) se mapea arepliesCount, y cualquier otro valor recae enlikesCount. Todavía no hay una ruta de consulta equivalente aquí para otros tipos de contenido (blasts, tales, articles, etc).) - Mejores usuarios por métrica (
adminGetTopUsers- corregido en esta sesión, conectado a la página del dashboard. Su ramaENGAGEMENTtenía un segundo bug encontrado en una revisión posterior: el literal SQL crudo referenciaba nombres de columna camelCase entre comillas ("likesCount", etc.) que no existen como columnas de base de datos -Postusaunderscored: truecon mapeos explícitos estilofield: 'likes_count', así que el literal necesitaba los nombres reales en snake_case. Actualmente no es alcanzable desde la página del dashboard, que solo solicita la métricaFOLLOWERS.) - Estadísticas de suscripciones (
adminGetSubscriptionStats- corregido en esta sesión, conectado a la página del dashboard) - Estadísticas de hashtags/tendencias (
adminGetHashtagStats- conectado a la página del dashboard.growthRateahora es real, corregido en esta sesión: compara elusageCountactual de un hashtag contra la fila más antigua disponible enhashtag_usage_snapshots(escrita a diario porservices/analytics-snapshot.service.js#runHashtagSnapshot, ver más abajo), una línea base que naturalmente se acerca a ~7+ días de antigüedad a medida que se acumulan instantáneas diarias.trendingScoreahora sí usa ese crecimiento:usageCount * (1 + growthRate / 100), con un piso de 0 para que un hashtag en declive no pueda quedar negativo. Bootstrap con degradación elegante: un hashtag sin instantánea todavía mantienegrowthRate: 0tal como antes, lo cual también mantienetrendingScoreidéntico al respaldo anterior basado solo en el conteo de uso - el comportamiento no cambia hasta que se acumule historial.) - Relleno histórico de instantáneas (
services/analytics-snapshot.service.js- nuevo en esta sesión, no es en sí una query de GraphQL. Dos tareas cron diarias - uso de hashtags a las 02:00, retención por cohortes de registro a las 02:30, ambas hora del servidor - escriben en dos tablas nuevas del almacén de analítica de las cuales leen las correcciones degrowthRate/retentionRateanteriores:hashtag_usage_snapshots(una fila por hashtag por día) yuser_cohort_snapshots(una fila por cohorte de día de registro por día de medición; verservices/analytics/migrations/006_create_hashtag_usage_snapshots_table.sqly007_create_user_cohort_snapshots_table.sql). Cada tarea es best-effort por elemento - que un hashtag o un día de cohorte falle al escribirse no aborta el resto de la corrida - y ambas se degradan sin error cuando el sumidero de analítica no está conectado.) - Registro de actividad de administradores (
adminGetActivityLog- corregido en esta sesión, conectado a la página del dashboard. Combina dos fuentes: la tablauser_moderation_logsy la tablacontent_moderation_logs(a través decontent-moderation-log.access-service.js, por la cualcontent-moderation.manager.js#logContentModerationActionahora persiste). Todavía no existe una tabla dedicada de registro de actividad de administradores - esto es una combinación de los dos registros de moderación existentes, ordenados por fecha. Las entradas cuyo actor no se puede resolver a unAdminUserreal se descartan, ya que el campoadmindel esquema es no-nulo.) - Métricas de salud del sistema (
adminGetSystemHealth) — devuelve la forma planaSystemHealthMetricscon valores REALES: tiempo de ida y vuelta a la DB + tiempo de respuesta promedio del plugin de Apollo (services/metrics.service), estado de Redis/almacenamiento, % de memoria del SO, % de carga de CPU, disco mediantestatfs, conexiones activas del pool de DB, y tasa de error. El plugin de métricas está conectado enapi/server.js. Conectado a las pastillas de estado y las tarjetas de estadísticas de la página/system. - Exportación de analítica (
adminExportAnalytics) — construye filas reales a partir de los métodos de analítica (usuarios/contenido/engagement/ingresos/moderación), las serializa a CSV o JSON, las sube a S3 (o escribe un archivo local como respaldo), y devuelve undownloadUrl+fileSizereales. Conectado al formulario de exportación de la página/system(selector de tipo de reporte + formato).
La página /system/environment (qué integraciones están configuradas, pruebas de conectividad por grupo) es una funcionalidad distinta — ver Estado del entorno.
Referencia técnica
Ver Panel de administración → Consultas de analítica granular y Exportación de analítica para la API completa de GraphQL.