Skip to main content

Analytics Dashboard

The admin panel exposes granular analytics queries for platform stats, user growth, engagement, revenue, content, and system health. There is no single composite "dashboard" query — each metric is its own GraphQL field. All of these (except adminGetContentStats, adminGetSystemHealth, and adminExportAnalytics) are now wired to the /dashboard page.

A note on this session's fixes: every method below except adminGetPlatformStats and adminGetUserStats (both fixed in an earlier pass) returned an object shape that didn't match its GraphQL type at all - missing non-null fields, differently-named fields (canceled_at vs the model's cancelledAt, growth_rate vs growthRate), or called data-access-service methods that don't exist (findByDateRange, getTopHashtags, findTopByFollowers, getUserPostCounts, and others - none of these were real methods anywhere in the codebase). Each was rewritten against the real Sequelize models and the real GraphQL schema. Some new data-access-service methods were added along the way (post.access-service.js's findByDateRange/countMediaByDateRange, message.access-service.js's countByDateRange, user-moderation-log.access-service.js's findRecent) since the methods the old code assumed existed simply didn't.

Implementation checklist

  • Platform overview stats (adminGetPlatformStats - wired to the dashboard page)
  • User growth over time (adminGetUserGrowth - wired to the dashboard page. retentionRate now prefers real signup-cohort data, fixed this session: user_cohort_snapshots (written daily by services/analytics-snapshot.service.js#runCohortSnapshot, see below) gives a real Day-N retention read - sum(active_count) / sum(cohort_size) across the cohorts whose signup day falls in a given bucket - instead of always using the old proxy. Graceful bootstrap: a bucket with no matching cohort snapshot yet (fresh install, or the cron hasn't run long enough to cover it - roughly the first ~30 days after this shipped) falls back to the original live proxy unchanged: the fraction of users who existed before the bucket that were active again during it (via createdAt/lastActiveAt), which is still not true cohort retention and is null only when there's no pre-existing user base to measure against.)
  • Engagement stats (adminGetEngagementStats - fixed this session, wired to the dashboard page)
  • Content stats (adminGetContentStats - resolver in admin/admin-dashboard.resolver.js gated on VIEW_ANALYTICS → adminDashboardManager.getContentStats, which returns real post/media/hashtag counts matching the ContentStats type)
  • Content creation stats by period (adminGetContentCreationStats - wired to the dashboard page. Fixed this session: period/startDate/endDate are now actually honored - totalPosts/totalMedia/totalHashtags/trendingHashtags are scoped to the resulting [startDate, endDate] window (explicit dates win, otherwise a trailing window sized by period), via postAccessService.findByDateRange() and countMediaByDateRange(), both added this session since neither existed before. postsToday/mediaToday/postsWeek intentionally keep their literal "today"/"last 7 days from now" meaning regardless of the requested window, matching what those field names mean per the schema - they're not part of the period scoping. Previously these args were accepted but silently ignored, and the query always returned the same all-time-ish snapshot as adminGetContentStats regardless of what was requested.)
  • Revenue stats (adminGetRevenueStats - fixed this session, wired to the dashboard page. Revenue is completed PaymentTransaction rows only, matching adminGetPlatformStats' definition - coin purchases and tips aren't included.)
  • Moderation queue stats (adminGetModerationQueueStats - fixed this session, wired to the dashboard page. flaggedContent/reportedContent both reuse the same pending-report count (see the flagged-content note in Content Moderation). autoFlagged now reflects a real signal, also fixed this session: it previously returned a hardcoded 0 behind a stale comment claiming no auto-moderation system existed - that's no longer true (see the Auto-moderation rules section), so it now counts content_moderation_logs rows written with action: 'flag' and moderator_id: 'system' - the trail left whenever services/auto-moderation.service.js matches a rule and calls contentModerationManager.flagContent() with the 'system' actor. Like the rest of this method's counts, it's an all-time queue snapshot, not scoped to a time window.)
  • Top content by metric (adminGetTopContent - fixed this session, wired to the dashboard page. Its contentType: String arg now also honors COMMENT, not just posts: previously the method never destructured contentType at all, so every call returned posts regardless of what was asked for. PostComment only tracks likesCount/repliesCount (no views/shares counters exist on comments), so LIKES maps to likesCount, COMMENTS (most-replied-to) maps to repliesCount, and anything else falls back to likesCount. There's still no equivalent query path here for other content types (blasts, tales, articles, etc).)
  • Top users by metric (adminGetTopUsers - fixed this session, wired to the dashboard page. Its ENGAGEMENT branch had a second bug found in a follow-up review: the raw SQL literal referenced quoted camelCase column names ("likesCount" etc.) that don't exist as DB columns - Post uses underscored: true with explicit field: 'likes_count'-style mappings, so the literal needed the real snake_case names. Not currently reachable from the dashboard page, which only requests the FOLLOWERS metric.)
  • Subscription stats (adminGetSubscriptionStats - fixed this session, wired to the dashboard page)
  • Hashtag/trending stats (adminGetHashtagStats - wired to the dashboard page. growthRate is now real, fixed this session: it compares a hashtag's current usageCount against the oldest available row in hashtag_usage_snapshots (written daily by services/analytics-snapshot.service.js#runHashtagSnapshot, see below), a baseline that naturally approaches ~7+ days old as daily snapshots accumulate. trendingScore now actually uses that growth: usageCount * (1 + growthRate / 100), floored at 0 so a collapsing hashtag can't go negative. Graceful bootstrap: a hashtag with no snapshot yet keeps growthRate: 0 exactly as before, which also keeps trendingScore identical to the old usage-count-only fallback - behavior is unchanged until history accumulates.)
  • Historical snapshot backfill (services/analytics-snapshot.service.js - new this session, not itself a GraphQL query. Two once-daily cron jobs - hashtag usage at 02:00, signup-cohort retention at 02:30, both server time - write into two new analytics-warehouse tables that the growthRate/retentionRate fixes above read from: hashtag_usage_snapshots (one row per hashtag per day) and user_cohort_snapshots (one row per signup-day cohort per measurement day; see services/analytics/migrations/006_create_hashtag_usage_snapshots_table.sql and 007_create_user_cohort_snapshots_table.sql). Each job is best-effort per item - one hashtag or cohort day failing to write doesn't abort the rest of the run - and both no-op cleanly when the analytics sink isn't connected.)
  • Admin activity log (adminGetActivityLog - fixed this session, wired to the dashboard page. Merges two sources: the user_moderation_logs table and the content_moderation_logs table (via content-moderation-log.access-service.js, which content-moderation.manager.js#logContentModerationAction now persists through). There's still no dedicated admin-activity-log table - this is a merge of the two existing moderation logs, sorted by date. Entries whose actor can't be resolved to a real AdminUser are dropped since the schema's admin field is non-null.)
  • System health metrics (adminGetSystemHealth) — returns the flat SystemHealthMetrics shape with REAL values: DB round-trip + Apollo-plugin avg response time (services/metrics.service), Redis/storage status, OS memory %, CPU load %, disk via statfs, active DB-pool connections, and error rate. The metrics plugin is wired in api/server.js. Wired to the /system page's status pills and stat cards.
  • Analytics export (adminExportAnalytics) — builds real rows from the analytics methods (users/content/engagement/revenue/moderation), serializes to CSV or JSON, uploads to S3 (or writes a local file fallback), and returns a real downloadUrl + fileSize. Wired to the /system page's export form (report type + format picker).

The separate /system/environment page (which integrations are configured, per-group connectivity tests) is a distinct feature — see Environment Status.

Technical reference

See Admin Panel → Granular analytics queries and Analytics export for the full GraphQL API.