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.retentionRatenow prefers real signup-cohort data, fixed this session:user_cohort_snapshots(written daily byservices/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 (viacreatedAt/lastActiveAt), which is still not true cohort retention and isnullonly 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 inadmin/admin-dashboard.resolver.jsgated on VIEW_ANALYTICS →adminDashboardManager.getContentStats, which returns real post/media/hashtag counts matching theContentStatstype) - Content creation stats by period (
adminGetContentCreationStats- wired to the dashboard page. Fixed this session:period/startDate/endDateare now actually honored -totalPosts/totalMedia/totalHashtags/trendingHashtagsare scoped to the resulting[startDate, endDate]window (explicit dates win, otherwise a trailing window sized byperiod), viapostAccessService.findByDateRange()andcountMediaByDateRange(), both added this session since neither existed before.postsToday/mediaToday/postsWeekintentionally 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 asadminGetContentStatsregardless of what was requested.) - Revenue stats (
adminGetRevenueStats- fixed this session, wired to the dashboard page. Revenue is completedPaymentTransactionrows only, matchingadminGetPlatformStats' definition - coin purchases and tips aren't included.) - Moderation queue stats (
adminGetModerationQueueStats- fixed this session, wired to the dashboard page.flaggedContent/reportedContentboth reuse the same pending-report count (see the flagged-content note in Content Moderation).autoFlaggednow reflects a real signal, also fixed this session: it previously returned a hardcoded0behind a stale comment claiming no auto-moderation system existed - that's no longer true (see the Auto-moderation rules section), so it now countscontent_moderation_logsrows written withaction: 'flag'andmoderator_id: 'system'- the trail left wheneverservices/auto-moderation.service.jsmatches a rule and callscontentModerationManager.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. ItscontentType: Stringarg now also honorsCOMMENT, not just posts: previously the method never destructuredcontentTypeat all, so every call returned posts regardless of what was asked for.PostCommentonly trackslikesCount/repliesCount(no views/shares counters exist on comments), soLIKESmaps tolikesCount,COMMENTS(most-replied-to) maps torepliesCount, and anything else falls back tolikesCount. 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. ItsENGAGEMENTbranch 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 -Postusesunderscored: truewith explicitfield: 'likes_count'-style mappings, so the literal needed the real snake_case names. Not currently reachable from the dashboard page, which only requests theFOLLOWERSmetric.) - Subscription stats (
adminGetSubscriptionStats- fixed this session, wired to the dashboard page) - Hashtag/trending stats (
adminGetHashtagStats- wired to the dashboard page.growthRateis now real, fixed this session: it compares a hashtag's currentusageCountagainst the oldest available row inhashtag_usage_snapshots(written daily byservices/analytics-snapshot.service.js#runHashtagSnapshot, see below), a baseline that naturally approaches ~7+ days old as daily snapshots accumulate.trendingScorenow 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 keepsgrowthRate: 0exactly as before, which also keepstrendingScoreidentical 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 thegrowthRate/retentionRatefixes above read from:hashtag_usage_snapshots(one row per hashtag per day) anduser_cohort_snapshots(one row per signup-day cohort per measurement day; seeservices/analytics/migrations/006_create_hashtag_usage_snapshots_table.sqland007_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: theuser_moderation_logstable and thecontent_moderation_logstable (viacontent-moderation-log.access-service.js, whichcontent-moderation.manager.js#logContentModerationActionnow 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 realAdminUserare dropped since the schema'sadminfield is non-null.) - System health metrics (
adminGetSystemHealth) — returns the flatSystemHealthMetricsshape with REAL values: DB round-trip + Apollo-plugin avg response time (services/metrics.service), Redis/storage status, OS memory %, CPU load %, disk viastatfs, active DB-pool connections, and error rate. The metrics plugin is wired inapi/server.js. Wired to the/systempage'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 realdownloadUrl+fileSize. Wired to the/systempage'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.