Kulcare Docs
DeveloperMonitoring

Monitoring and Analytics Guide

This guide covers the implementation of comprehensive monitoring and analytics for the Kulcare platform's micro-frontend architecture.

Overview

The Kulcare platform requires robust monitoring to ensure reliability, track performance, and understand user behavior across multiple micro-frontends. This includes error tracking, analytics, performance monitoring, and logging.

Current State

Environment Variables Defined

The following monitoring-related environment variables are already defined in the deployment configuration but not yet implemented:

  • VITE_SENTRY_DSN - Sentry error tracking
  • VITE_GOOGLE_ANALYTICS_ID - Google Analytics tracking
  • VITE_ENABLE_ANALYTICS - Feature flag for analytics
  • VITE_ENABLE_PERFORMANCE_MONITORING - Performance monitoring flag
  • VITE_LOG_LEVEL - Logging configuration

Existing Components

  • ErrorBoundary component in @kulcare/ui-kit with basic error handling
  • Module Federation error handling mentioned in deployment docs
  • No actual implementation of monitoring services

Implementation Roadmap

Phase 1: Core Infrastructure (Week 1-2)

  1. Create @kulcare/monitoring package
  2. Implement error tracking with Sentry
  3. Set up basic logging infrastructure
  4. Add health check endpoints

Phase 2: Analytics (Week 3)

  1. Implement Google Analytics 4
  2. Set up cross-microfrontend tracking
  3. Add custom event tracking
  4. Configure conversion tracking

Phase 3: Performance (Week 4)

  1. Implement Web Vitals tracking
  2. Add Module Federation monitoring
  3. Set up performance budgets
  4. Configure real-user monitoring (RUM)

Phase 4: Advanced Features (Week 5-6)

  1. Implement A/B testing infrastructure
  2. Add session replay capabilities
  3. Set up alerting and dashboards
  4. Create monitoring documentation

Detailed Implementation

1. Error Tracking (Sentry)

Installation

pnpm add @sentry/react @sentry/vite-plugin --filter @kulcare/monitoring

Basic Setup

Create packages/monitoring/src/sentry.ts:

import * as Sentry from '@sentry/react';
import { BrowserTracing } from '@sentry/tracing';

export interface SentryConfig {
  dsn: string;
  environment: string;
  enabled?: boolean;
  tracesSampleRate?: number;
  debug?: boolean;
}

export function initSentry(config: SentryConfig) {
  if (!config.enabled || !config.dsn) {
    return;
  }

  Sentry.init({
    dsn: config.dsn,
    environment: config.environment,
    integrations: [
      new BrowserTracing({
        routingInstrumentation: Sentry.reactRouterV6Instrumentation(
          React.useEffect,
          useLocation,
          useNavigationType,
          createRoutesFromChildren,
          matchRoutes
        ),
      }),
    ],
    tracesSampleRate: config.tracesSampleRate || 0.1,
    debug: config.debug || false,
    beforeSend(event, hint) {
      // Filter out known non-critical errors
      if (event.exception?.values?.[0]?.type === 'ChunkLoadError') {
        return null;
      }
      return event;
    },
  });
}

export function setUserContext(user: { id: string; email?: string; name?: string }) {
  Sentry.setUser(user);
}

export function captureException(error: Error, context?: Record<string, any>) {
  Sentry.captureException(error, { extra: context });
}

Integration with ErrorBoundary

Update the ErrorBoundary to report to Sentry:

import { captureException } from '@kulcare/monitoring';

componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
  console.error('ErrorBoundary caught an error:', error, errorInfo);
  
  // Report to Sentry
  captureException(error, {
    componentStack: errorInfo.componentStack,
    errorBoundary: true,
  });
  
  this.props.onError?.(error, errorInfo);
}

App-Level Integration

In each app's main.tsx:

import { initSentry, setUserContext } from '@kulcare/monitoring';
import { useAuth } from '@kulcare/auth';

// Initialize Sentry
initSentry({
  dsn: import.meta.env.VITE_SENTRY_DSN,
  environment: import.meta.env.MODE,
  enabled: import.meta.env.PROD,
  tracesSampleRate: 0.1,
});

// Set user context when authenticated
function AppWithMonitoring() {
  const { user } = useAuth();
  
  useEffect(() => {
    if (user) {
      setUserContext({
        id: user.id,
        email: user.email,
        name: user.name,
      });
    }
  }, [user]);
  
  return <App />;
}

2. Analytics (Google Analytics 4)

Installation

pnpm add gtag.js --filter @kulcare/monitoring

Setup

Create packages/monitoring/src/analytics.ts:

declare global {
  interface Window {
    gtag: (...args: any[]) => void;
    dataLayer: any[];
  }
}

export interface AnalyticsConfig {
  measurementId: string;
  enabled?: boolean;
  debug?: boolean;
}

export function initAnalytics(config: AnalyticsConfig) {
  if (!config.enabled || !config.measurementId) {
    return;
  }

  // Load GA4 script
  const script = document.createElement('script');
  script.async = true;
  script.src = `https://www.googletagmanager.com/gtag/js?id=${config.measurementId}`;
  document.head.appendChild(script);

  // Initialize dataLayer
  window.dataLayer = window.dataLayer || [];
  window.gtag = function gtag() {
    window.dataLayer.push(arguments);
  };
  
  window.gtag('js', new Date());
  window.gtag('config', config.measurementId, {
    debug_mode: config.debug,
  });
}

export function trackEvent(eventName: string, parameters?: Record<string, any>) {
  if (typeof window.gtag === 'function') {
    window.gtag('event', eventName, parameters);
  }
}

export function trackPageView(pagePath: string, pageTitle?: string) {
  if (typeof window.gtag === 'function') {
    window.gtag('event', 'page_view', {
      page_path: pagePath,
      page_title: pageTitle,
    });
  }
}

export function setUserProperties(properties: Record<string, any>) {
  if (typeof window.gtag === 'function') {
    window.gtag('set', 'user_properties', properties);
  }
}

Route Change Tracking

Create a hook for tracking route changes:

import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import { trackPageView } from '@kulcare/monitoring';

export function usePageTracking() {
  const location = useLocation();
  
  useEffect(() => {
    trackPageView(location.pathname + location.search);
  }, [location]);
}

Custom Events

Standard events to track across the platform:

// User actions
trackEvent('login', { method: 'email' });
trackEvent('signup', { method: 'google' });
trackEvent('logout');

// Feature usage
trackEvent('feature_used', {
  feature_name: 'appointment_booking',
  feature_category: 'cms',
});

// Errors
trackEvent('error', {
  error_type: 'api_error',
  error_message: error.message,
  error_code: error.code,
});

// Conversions
trackEvent('purchase', {
  transaction_id: '12345',
  value: 99.99,
  currency: 'INR',
  items: [{ id: 'pkg_001', name: 'Premium Package' }],
});

3. Performance Monitoring

Web Vitals

Create packages/monitoring/src/performance.ts:

import { getCLS, getFID, getFCP, getLCP, getTTFB } from 'web-vitals';

export interface PerformanceConfig {
  enabled?: boolean;
  reportHandler?: (metric: any) => void;
}

export function initPerformanceMonitoring(config: PerformanceConfig) {
  if (!config.enabled) {
    return;
  }

  const reportHandler = config.reportHandler || defaultReportHandler;

  getCLS(reportHandler);
  getFID(reportHandler);
  getFCP(reportHandler);
  getLCP(reportHandler);
  getTTFB(reportHandler);
}

function defaultReportHandler(metric: any) {
  // Report to analytics
  trackEvent('web_vitals', {
    metric_name: metric.name,
    metric_value: metric.value,
    metric_delta: metric.delta,
    metric_id: metric.id,
  });

  // Report to Sentry
  if (metric.value > getThreshold(metric.name)) {
    captureException(new Error(`Poor ${metric.name} performance`), {
      metric,
    });
  }
}

function getThreshold(metricName: string): number {
  const thresholds: Record<string, number> = {
    CLS: 0.25,
    FID: 300,
    FCP: 3000,
    LCP: 4000,
    TTFB: 800,
  };
  return thresholds[metricName] || Infinity;
}

Module Federation Monitoring

Monitor remote module loading:

export function monitorModuleFederation() {
  // Track load times
  const moduleLoadTimes: Record<string, number> = {};
  
  window.addEventListener('error', (event) => {
    if (event.filename?.includes('remoteEntry.js')) {
      captureException(new Error('Module Federation Load Error'), {
        filename: event.filename,
        message: event.message,
        colno: event.colno,
        lineno: event.lineno,
      });
      
      trackEvent('module_federation_error', {
        remote_url: event.filename,
        error_message: event.message,
      });
    }
  });

  // Intercept dynamic imports
  const originalImport = window.System.import;
  window.System.import = async function(id: string) {
    const startTime = performance.now();
    
    try {
      const module = await originalImport.call(this, id);
      const loadTime = performance.now() - startTime;
      
      moduleLoadTimes[id] = loadTime;
      
      trackEvent('module_loaded', {
        module_id: id,
        load_time: loadTime,
      });
      
      return module;
    } catch (error) {
      trackEvent('module_load_failed', {
        module_id: id,
        error_message: error.message,
      });
      throw error;
    }
  };
}

4. Logging Infrastructure

Logger Implementation

Create packages/monitoring/src/logger.ts:

export enum LogLevel {
  DEBUG = 0,
  INFO = 1,
  WARN = 2,
  ERROR = 3,
}

export interface LoggerConfig {
  level: LogLevel;
  enabled?: boolean;
  remote?: boolean;
  remoteUrl?: string;
}

class Logger {
  private config: LoggerConfig;
  private buffer: any[] = [];
  private flushInterval?: number;

  constructor(config: LoggerConfig) {
    this.config = config;
    
    if (config.remote && config.remoteUrl) {
      this.startRemoteLogging();
    }
  }

  private shouldLog(level: LogLevel): boolean {
    return this.config.enabled !== false && level >= this.config.level;
  }

  private formatMessage(level: string, message: string, data?: any): string {
    const timestamp = new Date().toISOString();
    const clinic = localStorage.getItem('selectedClinic');
    const user = localStorage.getItem('userId');
    
    return JSON.stringify({
      timestamp,
      level,
      message,
      data,
      context: {
        clinic,
        user,
        url: window.location.href,
        userAgent: navigator.userAgent,
      },
    });
  }

  private log(level: LogLevel, levelName: string, message: string, data?: any) {
    if (!this.shouldLog(level)) {
      return;
    }

    const formattedMessage = this.formatMessage(levelName, message, data);

    // Console output in development
    if (import.meta.env.DEV) {
      console[levelName.toLowerCase()](message, data);
    }

    // Buffer for remote logging
    if (this.config.remote) {
      this.buffer.push(formattedMessage);
    }

    // Send to Sentry for errors
    if (level === LogLevel.ERROR) {
      captureException(new Error(message), data);
    }
  }

  debug(message: string, data?: any) {
    this.log(LogLevel.DEBUG, 'DEBUG', message, data);
  }

  info(message: string, data?: any) {
    this.log(LogLevel.INFO, 'INFO', message, data);
  }

  warn(message: string, data?: any) {
    this.log(LogLevel.WARN, 'WARN', message, data);
  }

  error(message: string, data?: any) {
    this.log(LogLevel.ERROR, 'ERROR', message, data);
  }

  private startRemoteLogging() {
    this.flushInterval = window.setInterval(() => {
      this.flush();
    }, 5000); // Flush every 5 seconds
  }

  private async flush() {
    if (this.buffer.length === 0) {
      return;
    }

    const logs = [...this.buffer];
    this.buffer = [];

    try {
      await fetch(this.config.remoteUrl!, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ logs }),
      });
    } catch (error) {
      console.error('Failed to send logs:', error);
      // Put logs back in buffer
      this.buffer.unshift(...logs);
    }
  }

  destroy() {
    if (this.flushInterval) {
      clearInterval(this.flushInterval);
      this.flush();
    }
  }
}

export function createLogger(config: LoggerConfig): Logger {
  return new Logger(config);
}

5. Health Monitoring

Health Check Endpoints

Add health.json to each app's public folder:

{
  "status": "healthy",
  "version": "1.0.0",
  "app": "shell",
  "timestamp": "2024-01-01T00:00:00Z",
  "checks": {
    "database": "ok",
    "cache": "ok",
    "remotes": {
      "cms": "ok",
      "growth": "ok"
    }
  }
}

Health Check Service

Create packages/monitoring/src/health.ts:

export interface HealthCheckResult {
  status: 'healthy' | 'degraded' | 'unhealthy';
  checks: Record<string, boolean>;
  timestamp: string;
}

export async function checkHealth(remotes: string[]): Promise<HealthCheckResult> {
  const checks: Record<string, boolean> = {};
  
  // Check API
  try {
    const response = await fetch('/api/health', { 
      method: 'HEAD',
      timeout: 5000,
    });
    checks.api = response.ok;
  } catch {
    checks.api = false;
  }

  // Check remotes
  for (const remote of remotes) {
    try {
      const response = await fetch(`${remote}/health.json`, {
        timeout: 5000,
      });
      checks[`remote_${remote}`] = response.ok;
    } catch {
      checks[`remote_${remote}`] = false;
    }
  }

  const allHealthy = Object.values(checks).every(v => v);
  const someHealthy = Object.values(checks).some(v => v);

  return {
    status: allHealthy ? 'healthy' : someHealthy ? 'degraded' : 'unhealthy',
    checks,
    timestamp: new Date().toISOString(),
  };
}

export function startHealthMonitoring(interval = 60000) {
  setInterval(async () => {
    const health = await checkHealth(['cms', 'growth']);
    
    if (health.status !== 'healthy') {
      trackEvent('health_check_failed', health);
    }
  }, interval);
}

Implementation Checklist

Phase 1: Core Setup

  • Create @kulcare/monitoring package
  • Implement Sentry integration
  • Update ErrorBoundary component
  • Add Sentry to all apps
  • Configure source maps
  • Test error reporting

Phase 2: Analytics

  • Implement GA4 integration
  • Add page tracking
  • Define standard events
  • Add custom event tracking
  • Set up user properties
  • Configure conversions

Phase 3: Performance

  • Add Web Vitals tracking
  • Implement performance budgets
  • Monitor Module Federation
  • Track API performance
  • Set up alerts

Phase 4: Infrastructure

  • Implement logger
  • Add health checks
  • Set up dashboards
  • Configure alerts
  • Create runbooks
  • Train team

Privacy and Compliance

GDPR Compliance

  • Implement cookie consent banner
  • Allow users to opt-out of tracking
  • Provide data export functionality
  • Document data retention policies

HIPAA Compliance

  • Ensure no PHI in analytics
  • Implement audit logging
  • Encrypt all data in transit
  • Regular security audits

Testing

Unit Tests

describe('Analytics', () => {
  it('should track events when enabled', () => {
    initAnalytics({ measurementId: 'G-TEST', enabled: true });
    trackEvent('test_event');
    expect(window.gtag).toHaveBeenCalledWith('event', 'test_event', undefined);
  });

  it('should not track when disabled', () => {
    initAnalytics({ measurementId: 'G-TEST', enabled: false });
    trackEvent('test_event');
    expect(window.gtag).not.toHaveBeenCalled();
  });
});

Integration Tests

  • Test cross-microfrontend tracking
  • Verify error reporting
  • Check performance metrics
  • Validate health checks

Monitoring Dashboard

Key Metrics

  1. Error Rate: Errors per minute/hour
  2. Performance: Core Web Vitals scores
  3. Availability: Uptime percentage
  4. Usage: Active users, page views
  5. Business: Conversions, feature adoption

Alerting Rules

  • Error rate > 1% of requests
  • Any Web Vital in "poor" range
  • Health check failures
  • Module Federation load failures
  • API response time > 3s

Best Practices

  1. Sampling: Use appropriate sampling rates to control costs
  2. PII Protection: Never log personally identifiable information
  3. Context: Always include relevant context with errors
  4. Versioning: Track app versions with all metrics
  5. Documentation: Keep runbooks updated

Troubleshooting

Common Issues

Sentry Not Reporting

  1. Check DSN is correctly set
  2. Verify environment is production
  3. Check network tab for requests
  4. Look for console errors

Analytics Not Tracking

  1. Verify measurement ID
  2. Check ad blockers
  3. Validate consent status
  4. Test in incognito mode

Performance Issues

  1. Check sampling rates
  2. Optimize bundle sizes
  3. Review network requests
  4. Profile with DevTools

Resources

On this page