Skip to main content
Back to Examples

Custom Analytics Dashboard

Build production-ready analytics dashboards with real-time metrics, historical trends, and engagement tracking using React and Chart.js.

Advanced3 hoursReal-time Data

What You'll Learn

  • Build complete React analytics dashboard component
  • Integrate real-time WebSocket updates for live metrics
  • Visualize data with Chart.js for trends and heatmaps
  • Track engagement metrics and viewer behavior
  • Export analytics data to CSV and PDF formats

Step 1: Project Setup & Dependencies

Install Dependencies

# Install WAVE SDK and analytics dependencies
npm install @wave/sdk @wave/analytics-sdk

# Install charting library
npm install chart.js react-chartjs-2

# Install date utilities
npm install date-fns

# Install export utilities
npm install jspdf jspdf-autotable html2canvas

# TypeScript types
npm install -D @types/chart.js

TypeScript Type Definitions

// types/analytics.ts
export interface StreamAnalytics {
  streamId: string;
  status: 'live' | 'ended' | 'scheduled';
  startTime: string;
  endTime?: string;
  duration: number; // seconds

  // Real-time metrics
  currentViewers: number;
  peakConcurrentViewers: number;
  totalViews: number;

  // Engagement metrics
  averageWatchDuration: number; // seconds
  engagementRate: number; // percentage
  chatMessages: number;
  reactions: number;
  shares: number;

  // Quality metrics
  averageBitrate: number; // kbps
  bufferRatio: number; // percentage
  qualityDistribution: {
    '1080p': number;
    '720p': number;
    '480p': number;
    '360p': number;
  };

  // Geographic data
  viewersByCountry: Record<string, number>;
  viewersByCity: Record<string, number>;

  // Device breakdown
  deviceTypes: {
    desktop: number;
    mobile: number;
    tablet: number;
    smartTV: number;
  };

  // Traffic sources
  trafficSources: {
    direct: number;
    social: number;
    search: number;
    embedded: number;
  };
}

export interface HistoricalData {
  timestamp: string;
  viewers: number;
  bitrate: number;
  bufferEvents: number;
}

export interface RevenueMetrics {
  totalRevenue: number;
  currency: string;
  breakdown: {
    subscriptions: number;
    donations: number;
    advertising: number;
    sponsorships: number;
  };
  topDonors: Array<{
    username: string;
    amount: number;
  }>;
}

export interface AlertConfig {
  type: 'viewers' | 'bitrate' | 'buffer' | 'engagement';
  threshold: number;
  comparison: 'above' | 'below';
  enabled: boolean;
}

Step 2: Main Analytics Dashboard Component

Complete Dashboard Component

Production-ready React component with real-time updates

// components/StreamAnalyticsDashboard.tsx
'use client';

import React, { useEffect, useState, useCallback } from 'react';
import { WaveAnalyticsClient } from '@wave/analytics-sdk';
import { Line, Doughnut, Bar } from 'react-chartjs-2';
import {
  Chart as ChartJS,
  CategoryScale,
  LinearScale,
  PointElement,
  LineElement,
  BarElement,
  ArcElement,
  Title,
  Tooltip,
  Legend,
  Filler
} from 'chart.js';
import { format, subHours, subDays } from 'date-fns';
import type { StreamAnalytics, HistoricalData } from '@/types/analytics';

// Register Chart.js components
ChartJS.register(
  CategoryScale,
  LinearScale,
  PointElement,
  LineElement,
  BarElement,
  ArcElement,
  Title,
  Tooltip,
  Legend,
  Filler
);

interface StreamAnalyticsDashboardProps {
  streamId: string;
  apiKey: string;
  refreshInterval?: number; // milliseconds
  enableRealtime?: boolean;
}

export const StreamAnalyticsDashboard: React.FC<StreamAnalyticsDashboardProps> = ({
  streamId,
  apiKey,
  refreshInterval = 5000,
  enableRealtime = true
}) => {
  // State management
  const [analytics, setAnalytics] = useState<StreamAnalytics | null>(null);
  const [historicalData, setHistoricalData] = useState<HistoricalData[]>([]);
  const [timeRange, setTimeRange] = useState<'1h' | '24h' | '7d' | '30d'>('24h');
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
  const [wsConnected, setWsConnected] = useState(false);

  // Initialize analytics client
  const [analyticsClient] = useState(() =>
    new WaveAnalyticsClient({ apiKey })
  );

  // Fetch initial analytics data
  const fetchAnalytics = useCallback(async () => {
    try {
      const data = await analyticsClient.getStreamAnalytics(streamId);
      setAnalytics(data);
      setError(null);
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Failed to fetch analytics');
    } finally {
      setIsLoading(false);
    }
  }, [analyticsClient, streamId]);

  // Fetch historical data
  const fetchHistoricalData = useCallback(async () => {
    try {
      const now = new Date();
      const startTime = {
        '1h': subHours(now, 1),
        '24h': subDays(now, 1),
        '7d': subDays(now, 7),
        '30d': subDays(now, 30)
      }[timeRange];

      const data = await analyticsClient.getHistoricalData(streamId, {
        startTime: startTime.toISOString(),
        endTime: now.toISOString(),
        granularity: timeRange === '1h' ? '1m' : timeRange === '24h' ? '5m' : '1h'
      });

      setHistoricalData(data);
    } catch (err) {
      console.error('Failed to fetch historical data:', err);
    }
  }, [analyticsClient, streamId, timeRange]);

  // Setup real-time WebSocket connection
  useEffect(() => {
    if (!enableRealtime) return;

    const ws = analyticsClient.subscribeToRealtimeUpdates(streamId);

    ws.on('connect', () => {
      setWsConnected(true);
    });

    ws.on('disconnect', () => {
      setWsConnected(false);
    });

    ws.on('analytics', (update: Partial<StreamAnalytics>) => {
      setAnalytics(prev => prev ? { ...prev, ...update } : null);
    });

    ws.on('historical', (dataPoint: HistoricalData) => {
      setHistoricalData(prev => [...prev.slice(-99), dataPoint]);
    });

    return () => {
      ws.disconnect();
    };
  }, [analyticsClient, streamId, enableRealtime]);

  // Periodic refresh for non-realtime mode
  useEffect(() => {
    if (enableRealtime) return;

    const interval = setInterval(fetchAnalytics, refreshInterval);
    return () => clearInterval(interval);
  }, [enableRealtime, fetchAnalytics, refreshInterval]);

  // Initial data fetch
  useEffect(() => {
    fetchAnalytics();
    fetchHistoricalData();
  }, [fetchAnalytics, fetchHistoricalData]);

  // Loading state
  if (isLoading) {
    return (
      <div className="flex items-center justify-center p-12">
        <div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-primary-500"></div>
      </div>
    );
  }

  // Error state
  if (error) {
    return (
      <div className="p-6 bg-error-50 border border-error-200 rounded-lg">
        <h3 className={combineTokens(DesignTokens.typography.body, 'font-semibold text-error-700 mb-2')}>Error Loading Analytics</h3>
        <p className="text-sm text-error-600">{error}</p>
      </div>
    );
  }

  if (!analytics) return null;

  // Chart data configurations
  const viewersTrendData = {
    labels: historicalData.map(d => format(new Date(d.timestamp), 'HH:mm')),
    datasets: [
      {
        label: 'Concurrent Viewers',
        data: historicalData.map(d => d.viewers),
        borderColor: 'rgb(59, 130, 246)',
        backgroundColor: 'rgba(59, 130, 246, 0.1)',
        fill: true,
        tension: 0.4
      }
    ]
  };

  const qualityDistributionData = {
    labels: Object.keys(analytics.qualityDistribution),
    datasets: [
      {
        data: Object.values(analytics.qualityDistribution),
        backgroundColor: [
          'rgba(59, 130, 246, 0.8)',
          'rgba(16, 185, 129, 0.8)',
          'rgba(245, 158, 11, 0.8)',
          'rgba(239, 68, 68, 0.8)'
        ],
        borderWidth: 0
      }
    ]
  };

  const deviceBreakdownData = {
    labels: Object.keys(analytics.deviceTypes).map(k =>
      k.charAt(0).toUpperCase() + k.slice(1)
    ),
    datasets: [
      {
        data: Object.values(analytics.deviceTypes),
        backgroundColor: [
          'rgba(139, 92, 246, 0.8)',
          'rgba(236, 72, 153, 0.8)',
          'rgba(251, 146, 60, 0.8)',
          'rgba(34, 197, 94, 0.8)'
        ]
      }
    ]
  };

  return (
    <div className="space-y-6">
      {/* Header with Real-time Status */}
      <div className="flex items-center justify-between">
        <div>
          <h2 className={`${DesignTokens.typography.h3} text-text-primary`}>Stream Analytics</h2>
          <p className="text-sm text-text-secondary mt-1">
            Stream ID: {streamId}
          </p>
        </div>

        <div className="flex items-center gap-4">
          {/* Real-time indicator */}
          <div className="flex items-center gap-2">
            <div className={`w-2 h-2 rounded-full ${wsConnected ? 'bg-success-500 animate-pulse' : 'bg-error-500'}`} />
            <span className="text-sm text-text-secondary">
              {wsConnected ? 'Live' : 'Offline'}
            </span>
          </div>

          {/* Time range selector */}
          <select
            value={timeRange}
            onChange={(e) => setTimeRange(e.target.value as any)}
            className="px-3 py-2 border border-border-primary rounded-md text-sm bg-white"
          >
            <option value="1h">Last Hour</option>
            <option value="24h">Last 24 Hours</option>
            <option value="7d">Last 7 Days</option>
            <option value="30d">Last 30 Days</option>
          </select>
        </div>
      </div>

      {/* Key Metrics Grid */}
      <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
        <MetricCard
          title="Current Viewers"
          value={analytics.currentViewers.toLocaleString()}
          change={calculateChange(analytics.currentViewers, analytics.peakConcurrentViewers)}
          icon={
            <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
              <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
              <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
            </svg>
          }
        />

        <MetricCard
          title="Total Views"
          value={analytics.totalViews.toLocaleString()}
          subtitle={`Peak: ${analytics.peakConcurrentViewers.toLocaleString()}`}
          icon={
            <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
              <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
            </svg>
          }
        />

        <MetricCard
          title="Avg Watch Time"
          value={formatDuration(analytics.averageWatchDuration)}
          subtitle={`Engagement: ${analytics.engagementRate.toFixed(1)}%`}
          icon={
            <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
              <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
            </svg>
          }
        />

        <MetricCard
          title="Chat Messages"
          value={analytics.chatMessages.toLocaleString()}
          subtitle={`${analytics.reactions.toLocaleString()} reactions`}
          icon={
            <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
              <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
            </svg>
          }
        />
      </div>

      {/* Charts Row 1 */}
      <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
        {/* Viewers Trend Chart */}
        <div className="bg-white p-6 rounded-lg border border-border-primary">
          <h3 className={combineTokens(DesignTokens.typography.body, " font-semibold text-text-primary mb-4")}>
            Viewer Trends
          </h3>
          <Line
            data={viewersTrendData}
            options={{
              responsive: true,
              plugins: {
                legend: { display: false },
                tooltip: {
                  callbacks: {
                    label: (context) => `${context.parsed.y.toLocaleString()} viewers`
                  }
                }
              },
              scales: {
                y: {
                  beginAtZero: true,
                  ticks: {
                    callback: (value) => value.toLocaleString()
                  }
                }
              }
            }}
          />
        </div>

        {/* Quality Distribution Chart */}
        <div className="bg-white p-6 rounded-lg border border-border-primary">
          <h3 className={combineTokens(DesignTokens.typography.body, " font-semibold text-text-primary mb-4")}>
            Quality Distribution
          </h3>
          <Doughnut
            data={qualityDistributionData}
            options={{
              responsive: true,
              plugins: {
                legend: { position: 'bottom' },
                tooltip: {
                  callbacks: {
                    label: (context) => {
                      const label = context.label || '';
                      const value = context.parsed || 0;
                      const total = context.dataset.data.reduce((a: number, b: number) => a + b, 0);
                      const percentage = ((value / total) * 100).toFixed(1);
                      return `${label}: ${percentage}%`;
                    }
                  }
                }
              }
            }}
          />
        </div>
      </div>

      {/* Charts Row 2 */}
      <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
        {/* Device Breakdown */}
        <div className="bg-white p-6 rounded-lg border border-border-primary">
          <h3 className={combineTokens(DesignTokens.typography.body, " font-semibold text-text-primary mb-4")}>
            Device Types
          </h3>
          <Doughnut
            data={deviceBreakdownData}
            options={{
              responsive: true,
              plugins: {
                legend: { position: 'bottom' }
              }
            }}
          />
        </div>

        {/* Geographic Distribution */}
        <div className="bg-white p-6 rounded-lg border border-border-primary col-span-2">
          <h3 className={combineTokens(DesignTokens.typography.body, " font-semibold text-text-primary mb-4")}>
            Top Locations
          </h3>
          <div className="space-y-3">
            {Object.entries(analytics.viewersByCountry)
              .sort(([, a], [, b]) => b - a)
              .slice(0, 5)
              .map(([country, viewers]) => {
                const percentage = (viewers / analytics.totalViews) * 100;
                return (
                  <div key={country}>
                    <div className="flex justify-between text-sm mb-1">
                      <span className="text-text-primary font-medium">{country}</span>
                      <span className="text-text-secondary">
                        {viewers.toLocaleString()} ({percentage.toFixed(1)}%)
                      </span>
                    </div>
                    <div className="w-full bg-background-secondary rounded-full h-2">
                      <div
                        className="bg-primary-500 h-2 rounded-full transition-all duration-300"
                        style={{ width: `${percentage}%` }}
                      />
                    </div>
                  </div>
                );
              })}
          </div>
        </div>
      </div>

      {/* Export Actions */}
      <div className="flex justify-end gap-3">
        <button
          onClick={() => exportToCSV(analytics, historicalData)}
          className="px-4 py-2 bg-background-secondary text-text-primary rounded-md hover:bg-background-tertiary transition-colors flex items-center gap-2"
        >
          <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
            <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
          </svg>
          Export CSV
        </button>

        <button
          onClick={() => exportToPDF(analytics)}
          className="px-4 py-2 bg-primary-500 text-white rounded-md hover:bg-primary-600 transition-colors flex items-center gap-2"
        >
          <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
            <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
          </svg>
          Export PDF
        </button>
      </div>
    </div>
  );
};

// Helper Components
interface MetricCardProps {
  title: string;
  value: string;
  subtitle?: string;
  change?: number;
  icon: React.ReactNode;
}

const MetricCard: React.FC<MetricCardProps> = ({ title, value, subtitle, change, icon }) => (
  <div className="bg-white p-6 rounded-lg border border-border-primary">
    <div className="flex items-start justify-between">
      <div>
        <p className="text-sm text-text-secondary mb-1">{title}</p>
        <p className={`${DesignTokens.typography.h4} text-text-primary`}>{value}</p>
        {subtitle && (
          <p className="text-xs text-text-secondary mt-1">{subtitle}</p>
        )}
      </div>
      <div className="p-3 bg-primary-50 rounded-lg text-primary-600">
        {icon}
      </div>
    </div>
    {change !== undefined && (
      <div className={`mt-3 flex items-center gap-1 text-sm ${change >= 0 ? 'text-success-600' : 'text-error-600'}`}>
        <svg className={`w-4 h-4 ${change >= 0 ? '' : 'rotate-180'}`} fill="currentColor" viewBox="0 0 20 20">
          <path fillRule="evenodd" d="M5.293 7.707a1 1 0 010-1.414l4-4a1 1 0 011.414 0l4 4a1 1 0 01-1.414 1.414L11 5.414V17a1 1 0 11-2 0V5.414L6.707 7.707a1 1 0 01-1.414 0z" clipRule="evenodd" />
        </svg>
        {Math.abs(change).toFixed(1)}% from peak
      </div>
    )}
  </div>
);

// Utility Functions
function calculateChange(current: number, peak: number): number {
  if (peak === 0) return 0;
  return ((current - peak) / peak) * 100;
}

function formatDuration(seconds: number): string {
  const hours = Math.floor(seconds / 3600);
  const minutes = Math.floor((seconds % 3600) / 60);

  if (hours > 0) {
    return `${hours}h ${minutes}m`;
  }
  return `${minutes}m`;
}

function exportToCSV(analytics: StreamAnalytics, historical: HistoricalData[]) {
  const rows = [
    ['Timestamp', 'Viewers', 'Bitrate (kbps)', 'Buffer Events'],
    ...historical.map(d => [
      d.timestamp,
      d.viewers.toString(),
      d.bitrate.toString(),
      d.bufferEvents.toString()
    ])
  ];

  const csvContent = rows.map(row => row.join(',')).join('\n');
  const blob = new Blob([csvContent], { type: 'text/csv' });
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = `analytics-${analytics.streamId}-${Date.now()}.csv`;
  a.click();
  URL.revokeObjectURL(url);
}

async function exportToPDF(analytics: StreamAnalytics) {
  const { jsPDF } = await import('jspdf');
  const doc = new jsPDF();

  doc.setFontSize(20);
  doc.text('Stream Analytics Report', 20, 20);

  doc.setFontSize(12);
  doc.text(`Stream ID: ${analytics.streamId}`, 20, 35);
  doc.text(`Generated: ${new Date().toLocaleString()}`, 20, 42);

  doc.setFontSize(14);
  doc.text('Key Metrics', 20, 55);

  doc.setFontSize(10);
  const metrics = [
    `Total Views: ${analytics.totalViews.toLocaleString()}`,
    `Peak Concurrent Viewers: ${analytics.peakConcurrentViewers.toLocaleString()}`,
    `Average Watch Duration: ${formatDuration(analytics.averageWatchDuration)}`,
    `Engagement Rate: ${analytics.engagementRate.toFixed(1)}%`,
    `Chat Messages: ${analytics.chatMessages.toLocaleString()}`,
    `Reactions: ${analytics.reactions.toLocaleString()}`
  ];

  metrics.forEach((metric, index) => {
    doc.text(metric, 20, 65 + (index * 7));
  });

  doc.save(`analytics-${analytics.streamId}-${Date.now()}.pdf`);
}

export default StreamAnalyticsDashboard;

Step 3: Real-time WebSocket Integration

Custom WebSocket Hook

// hooks/useRealtimeAnalytics.ts
import { useEffect, useState, useCallback } from 'react';
import { WaveAnalyticsClient } from '@wave/analytics-sdk';
import type { StreamAnalytics } from '@/types/analytics';

export function useRealtimeAnalytics(
  streamId: string,
  apiKey: string,
  options: {
    enabled?: boolean;
    reconnectInterval?: number;
  } = {}
) {
  const { enabled = true, reconnectInterval = 5000 } = options;

  const [analytics, setAnalytics] = useState<StreamAnalytics | null>(null);
  const [isConnected, setIsConnected] = useState(false);
  const [error, setError] = useState<Error | null>(null);

  const [client] = useState(() => new WaveAnalyticsClient({ apiKey }));

  useEffect(() => {
    if (!enabled) return;

    let ws: any;
    let reconnectTimeout: NodeJS.Timeout;

    const connect = () => {
      try {
        ws = client.subscribeToRealtimeUpdates(streamId);

        ws.on('connect', () => {
          setIsConnected(true);
          setError(null);
        });

        ws.on('disconnect', () => {
          setIsConnected(false);
          // Attempt reconnection
          reconnectTimeout = setTimeout(connect, reconnectInterval);
        });

        ws.on('analytics', (update: Partial<StreamAnalytics>) => {
          setAnalytics(prev => prev ? { ...prev, ...update } : null);
        });

        ws.on('error', (err: Error) => {
          setError(err);
          setIsConnected(false);
        });
      } catch (err) {
        setError(err as Error);
      }
    };

    // Initial connection
    connect();

    // Cleanup
    return () => {
      if (ws) ws.disconnect();
      if (reconnectTimeout) clearTimeout(reconnectTimeout);
    };
  }, [client, streamId, enabled, reconnectInterval]);

  const refresh = useCallback(async () => {
    try {
      const data = await client.getStreamAnalytics(streamId);
      setAnalytics(data);
      setError(null);
    } catch (err) {
      setError(err as Error);
    }
  }, [client, streamId]);

  return {
    analytics,
    isConnected,
    error,
    refresh
  };
}

Step 4: Custom Alert Configuration

Alert System Implementation

// components/AnalyticsAlerts.tsx
import { useEffect, useState } from 'react';
import { WaveAnalyticsClient } from '@wave/analytics-sdk';
import type { AlertConfig } from '@/types/analytics';

import { DesignTokens, getContainer, getSection } from '@/lib/design-tokens';
export function setupAnalyticsAlerts(
  client: WaveAnalyticsClient,
  streamId: string,
  alerts: AlertConfig[]
) {
  alerts.forEach(alert => {
    if (!alert.enabled) return;

    client.createAlert({
      streamId,
      type: alert.type,
      condition: {
        metric: alert.type,
        threshold: alert.threshold,
        comparison: alert.comparison
      },
      actions: [
        {
          type: 'webhook',
          url: 'https://your-webhook-url.com/alerts',
          method: 'POST'
        },
        {
          type: 'email',
          recipients: ['[email protected]']
        },
        {
          type: 'push',
          service: 'fcm', // Firebase Cloud Messaging
          tokens: ['device-token-here']
        }
      ]
    });
  });
}

// Example usage
const alertConfigs: AlertConfig[] = [
  {
    type: 'viewers',
    threshold: 10000,
    comparison: 'above',
    enabled: true
  },
  {
    type: 'bitrate',
    threshold: 1000,
    comparison: 'below',
    enabled: true
  },
  {
    type: 'buffer',
    threshold: 5, // percentage
    comparison: 'above',
    enabled: true
  },
  {
    type: 'engagement',
    threshold: 20, // percentage
    comparison: 'below',
    enabled: true
  }
];

Troubleshooting Common Issues

WebSocket connection keeps dropping

  • Implement automatic reconnection with exponential backoff
  • Check firewall settings and WebSocket support on your hosting
  • Use heartbeat/ping-pong to keep connection alive

Charts not rendering correctly

  • Ensure all Chart.js components are registered
  • Verify data format matches chart expectations
  • Check for console errors and Chart.js version compatibility

Performance issues with large datasets

  • Implement data pagination and limit historical data points
  • Use React.memo() and useMemo() for expensive computations
  • Consider data aggregation on the backend for long time ranges
Examples - Code Samples & Implementation Guides | WAVE | WAVE