Analytics - nself-org/nchat GitHub Wiki
nChat v0.5.0 introduces a comprehensive analytics and telemetry system for tracking usage, performance, and engagement metrics. Built on TimescaleDB for efficient time-series data storage and querying.
- Architecture
- Features
- Database Schema
- Configuration
- Usage
- API Endpoints
- Dashboard
- Privacy & Compliance
- Performance
- Troubleshooting
βββββββββββββββββββ
β React Client β ββtrack()βββΆ Analytics Client
βββββββββββββββββββ β
β batch
βΌ
ββββββββββββββββββββββ
β Analytics API β
β /api/analytics/* β
ββββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββ
β PostgreSQL + β
β TimescaleDB β
β (Hypertables) β
ββββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββ
β Hasura GraphQL β
β (Queries/Subs) β
ββββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββ
β Analytics β
β Dashboard β
ββββββββββββββββββββββ
- Database: PostgreSQL 15+ with TimescaleDB extension
- Time-Series Storage: TimescaleDB hypertables with automatic partitioning
- API: Next.js API routes
- GraphQL: Hasura for real-time queries and subscriptions
- Client: React hooks + Zustand for state management
- Visualization: Recharts (lightweight, customizable)
- Export: CSV, JSON, PDF support
-
User Activity
- Daily/Weekly/Monthly Active Users (DAU/WAU/MAU)
- User retention cohorts
- Session duration tracking
- Feature adoption rates
-
Message Analytics
- Message volume by channel/user/time
- Average message length
- Attachments vs text-only
- Thread participation rates
-
Channel Analytics
- Channel growth over time
- Active vs inactive channels
- Engagement rates by channel
- Member participation
-
API Performance
- Response times (p50/p95/p99)
- Error rates by endpoint
- Request volume by endpoint
- Database query performance
-
WebSocket Metrics
- Connection health
- Message latency
- Packet loss rates
- Concurrent connections
-
Reaction Analytics
- Most used reactions
- Reaction diversity
- Top reactors
-
File Analytics
- Upload volume and size
- File type breakdown
- Storage usage per user
-
Search Analytics
- Popular search queries
- No-results searches
- Click-through rates
- User growth rate
- Storage usage trends
- Feature usage by tier
- API call volume
Primary time-series table for all analytics events.
CREATE TABLE nchat_analytics_events (
id UUID PRIMARY KEY,
timestamp TIMESTAMPTZ NOT NULL,
event_name VARCHAR(100) NOT NULL,
event_category VARCHAR(50) NOT NULL,
user_id UUID REFERENCES nchat_users(id),
session_id VARCHAR(100) NOT NULL,
platform VARCHAR(20),
properties JSONB DEFAULT '{}'
);
-- Convert to hypertable (1 day chunks)
SELECT create_hypertable('nchat_analytics_events', 'timestamp',
chunk_time_interval => INTERVAL '1 day');
-- Retention: 90 days
SELECT add_retention_policy('nchat_analytics_events', INTERVAL '90 days');Aggregated hourly user activity metrics.
CREATE TABLE nchat_analytics_user_activity (
timestamp TIMESTAMPTZ NOT NULL,
user_id UUID NOT NULL,
messages_sent INTEGER DEFAULT 0,
reactions_given INTEGER DEFAULT 0,
files_uploaded INTEGER DEFAULT 0,
engagement_score FLOAT DEFAULT 0,
session_duration_seconds INTEGER DEFAULT 0,
UNIQUE(user_id, timestamp)
);
SELECT create_hypertable('nchat_analytics_user_activity', 'timestamp',
chunk_time_interval => INTERVAL '7 days');Aggregated hourly channel activity metrics.
CREATE TABLE nchat_analytics_channel_activity (
timestamp TIMESTAMPTZ NOT NULL,
channel_id UUID NOT NULL,
messages_count INTEGER DEFAULT 0,
active_users_count INTEGER DEFAULT 0,
reactions_count INTEGER DEFAULT 0,
average_response_time_seconds FLOAT DEFAULT 0,
UNIQUE(channel_id, timestamp)
);Automatically maintained aggregated views for fast queries:
-- Daily user summary
CREATE MATERIALIZED VIEW nchat_analytics_daily_user_summary
WITH (timescaledb.continuous) AS
SELECT
time_bucket('1 day', timestamp) AS day,
user_id,
SUM(messages_sent) AS total_messages,
AVG(engagement_score) AS avg_engagement_score
FROM nchat_analytics_user_activity
GROUP BY day, user_id;
-- Refresh every hour
SELECT add_continuous_aggregate_policy(
'nchat_analytics_daily_user_summary',
start_offset => INTERVAL '7 days',
end_offset => INTERVAL '1 hour',
schedule_interval => INTERVAL '1 hour'
);-- Get Daily Active Users
CREATE FUNCTION nchat_analytics_get_dau(start_date DATE, end_date DATE)
RETURNS TABLE (date DATE, active_users BIGINT);
-- Get Monthly Active Users
CREATE FUNCTION nchat_analytics_get_mau(start_date DATE)
RETURNS BIGINT;
-- Calculate Engagement Score
CREATE FUNCTION nchat_analytics_calculate_engagement_score(
p_messages INT, p_reactions INT, p_files INT, p_threads INT
)
RETURNS FLOAT;Add to src/config/app-config.ts:
analytics: {
enabled: boolean // Master toggle
trackingMode: 'full' | 'privacy' | 'minimal'
retentionDays: number // Data retention (default: 90)
enableRealtime: boolean // Real-time dashboard updates
enableExports: boolean // Allow CSV/PDF exports
anonymizeUserData: boolean // Anonymize PII
excludeTestUsers: boolean // Exclude test users from stats
trackPerformance: boolean // Track API performance
trackWebSocket: boolean // Track WebSocket metrics
trackFeatureUsage: boolean // Track feature adoption
trackErrors: boolean // Track errors (Sentry integration)
allowUserOptOut: boolean // Allow users to opt-out
}# Required
NEXT_PUBLIC_ANALYTICS_ENABLED=true
# Optional
ANALYTICS_RETENTION_DAYS=90
TIMESCALEDB_ENABLED=true
ANALYTICS_BATCH_SIZE=10
ANALYTICS_FLUSH_INTERVAL=30000import { useAnalytics } from '@/hooks/use-analytics';
function MyComponent() {
const { track } = useAnalytics();
const handleClick = () => {
track('button_clicked', {
buttonName: 'Submit',
location: 'chat-input',
});
};
return <button onClick={handleClick}>Submit</button>;
}import { useAnalytics } from '@/hooks/use-analytics'
function AuthComponent() {
const { identify } = useAnalytics()
useEffect(() => {
if (user) {
identify(user.id, {
email: user.email,
displayName: user.displayName,
role: user.role,
plan: 'free',
})
}
}, [user])
}import { useAnalytics } from '@/hooks/use-analytics'
function Page() {
const { page } = useAnalytics()
useEffect(() => {
page('/chat', 'Chat - nChat')
}, [])
}const {
trackMessageSent,
trackMessageEdited,
trackReactionAdded,
trackFileUploaded,
trackSearchPerformed,
trackFeatureUsed,
trackError,
} = useAnalytics()
// Track message sent
trackMessageSent(channelId, messageLength, hasAttachments)
// Track reaction
trackReactionAdded(messageId, 'π')
// Track file upload
trackFileUploaded(fileId, fileName, fileSize, mimeType)
// Track search
trackSearchPerformed(query, resultCount)
// Track feature usage
trackFeatureUsed('voice-call', { duration: 120 })
// Track error
trackError('api_error', 'Failed to send message', { code: 500 })import { useAnalyticsDashboard } from '@/hooks/use-analytics-dashboard';
function AnalyticsDashboard() {
const {
summary,
messageVolume,
userActivity,
isLoading,
error,
setDateRange,
refresh,
} = useAnalyticsDashboard();
if (isLoading) return <Spinner />;
if (error) return <Error message={error} />;
return (
<div>
<h1>Analytics Dashboard</h1>
<MetricCard value={summary.messages.total.value} label="Total Messages" />
<MetricCard value={summary.users.activeUsers.value} label="Active Users" />
<Chart data={messageVolume} />
</div>
);
}// Message analytics
const { volume, topMessages, stats } = useMessageAnalytics()
// User analytics
const { activity, growth, activeUsers } = useUserAnalytics()
// Channel analytics
const { activity, stats } = useChannelAnalytics()
// Date range filtering
const { dateRange, setPreset, setRange } = useDateRangeFilter()
setPreset('last30days')
setRange(new Date('2025-01-01'), new Date('2025-01-31'))
// Real-time updates (auto-refresh every 30 seconds)
useRealtimeAnalytics(30000)POST /api/analytics/track
Content-Type: application/json
[
{
"name": "message_sent",
"category": "messaging",
"properties": {
"channelId": "uuid",
"messageLength": 42
}
}
]
Response: { success: true, tracked: 1 }
GET /api/analytics/dashboard?start=2025-01-01&end=2025-01-31&granularity=day
Response: {
success: true,
data: {
summary: { ... },
messageVolume: [...],
userActivity: [...],
...
}
}
GET /api/analytics/messages
GET /api/analytics/users
GET /api/analytics/channels
GET /api/analytics/reactions
POST /api/analytics/export
Content-Type: application/json
{
"format": "csv",
"sections": ["summary", "messages", "users"],
"dateRange": { "start": "2025-01-01", "end": "2025-01-31" }
}
Response: {
success: true,
downloadUrl: "/api/analytics/download/export-123.csv"
}
Navigate to /admin/analytics (admin/owner only).
- Overview - High-level metrics and trends
- Messages - Message volume, top messages, patterns
- Users - User activity, growth, retention
- Channels - Channel engagement, top channels
- Reactions - Reaction analytics, top emojis
- Files - File uploads, storage usage
- Search - Search queries, no-results searches
- Reports - Scheduled reports, export history
- Date Range: Today, Yesterday, Last 7/30/90 days, Custom
- Granularity: Hour, Day, Week, Month, Year
- Channels: Filter by specific channels
- Users: Filter by specific users
- Include Bots: Toggle bot inclusion
Supported formats:
- CSV: Spreadsheet-friendly
- JSON: API-friendly
- PDF: Report-ready (with charts)
- XLSX: Excel-compatible
- Data Minimization: Only collect necessary data
- Anonymization: Option to anonymize user data
- Right to Access: Users can export their data
- Right to Erasure: Users can request deletion
- Consent Management: Opt-in/opt-out support
- Raw Events: 90 days (configurable)
- Aggregated Data: 365 days
- Continuous Aggregates: Permanent (summary only)
- Automatic Cleanup: TimescaleDB retention policies
// Allow users to opt-out
const { reset } = useAnalytics()
reset() // Clears user identity and stops tracking- Client-Side Batching: Events batched and sent every 30 seconds
- Server-Side Caching: Dashboard data cached for 5 minutes
- Continuous Aggregates: Pre-computed summaries for fast queries
- TimescaleDB Compression: Automatic compression of old data
- Index Optimization: Strategic indexes on high-traffic queries
- DAU/MAU queries: <100ms
- Dashboard load: <500ms
- Event insertion: <10ms (batched)
- Export generation: <5s for 30 days
- 10M+ events/day: Tested and optimized
- 100k+ concurrent users: Supported
- 10TB+ data: TimescaleDB handles efficiently
Solution:
- Check
analytics.enabledin AppConfig - Verify
NEXT_PUBLIC_ANALYTICS_ENABLED=true - Check browser console for errors
- Verify analytics client initialized
Solution:
- Reduce date range
- Increase cache TTL
- Check TimescaleDB compression status
- Verify continuous aggregates are refreshing
Solution:
- Enable continuous aggregates
- Increase retention policy frequency
- Check for missing indexes
- Enable TimescaleDB compression
Solution:
- Check retention policies (data may be expired)
- Verify hypertables are created
- Check Hasura permissions
- Verify cron jobs running (continuous aggregates)
-
Enable TimescaleDB:
cd .backend nself db exec postgres "CREATE EXTENSION IF NOT EXISTS timescaledb CASCADE;"
-
Run Analytics Migration:
nself db migrate up # Applies analytics schema from migrations/ -
Update AppConfig:
analytics: { enabled: true, trackingMode: 'full', retentionDays: 90, // ... }
-
Initialize Analytics Client:
// Add to _app.tsx or root layout import { getAnalyticsClient } from '@/lib/analytics/analytics-client' useEffect(() => { getAnalyticsClient().initialize() }, [])
- Track Meaningful Events: Don't over-track; focus on business-critical events
- Use Batching: Batch events for better performance
- Respect Privacy: Anonymize PII, honor opt-outs
- Monitor Performance: Watch for slow queries
- Regular Exports: Export data regularly for backup
- Dashboard Access: Restrict to admins only
- Test Tracking: Use test users excluded from stats
- Enhanced PDF exports with charts
- Email scheduled reports
- Custom dashboard widgets
- Machine learning insights
- Anomaly detection
- Predictive analytics
For issues or questions:
- GitHub Issues: https://github.com/nself/nchat/issues
- Documentation: https://docs.nchat.io/analytics
- Email: [email protected]
Version: v0.5.0 Last Updated: 2026-01-30 Contributors: nself team