Skip to content

Application Monitoring Guide

This guide focuses on monitoring application-level metrics, user behavior analytics, business KPIs, and performance tracking for Pocketbook.

Overview

While deployment monitoring (internal documentation) covers infrastructure and system metrics, this guide focuses on:

  • Application performance metrics
  • User behavior analytics
  • Business KPIs
  • Custom application events
  • Feature usage tracking

Application Performance Monitoring

Response Time Tracking

typescript
// src/middleware/performance.ts
import { Request, Response, NextFunction } from 'express';

interface PerformanceMetrics {
  endpoint: string;
  method: string;
  duration: number;
  statusCode: number;
  timestamp: Date;
}

export function performanceMiddleware(
  req: Request,
  res: Response,
  next: NextFunction
) {
  const start = process.hrtime.bigint();

  // Capture response
  res.on('finish', () => {
    const end = process.hrtime.bigint();
    const duration = Number(end - start) / 1_000_000; // Convert to ms

    const metrics: PerformanceMetrics = {
      endpoint: req.route?.path || req.path,
      method: req.method,
      duration,
      statusCode: res.statusCode,
      timestamp: new Date(),
    };

    // Log metrics
    logPerformance(metrics);

    // Send to monitoring service
    sendMetrics(metrics);
  });

  next();
}

function logPerformance(metrics: PerformanceMetrics) {
  logger.info('Request completed', {
    ...metrics,
    slow: metrics.duration > 1000, // Flag slow requests
  });
}

function sendMetrics(metrics: PerformanceMetrics) {
  // Send to Prometheus, Datadog, etc.
  prometheusMetrics.histogram('http_request_duration_ms', metrics.duration, {
    endpoint: metrics.endpoint,
    method: metrics.method,
    status: metrics.statusCode.toString(),
  });
}

Database Query Performance

typescript
// src/monitoring/database.ts
import { Pool } from 'pg';

class MonitoredPool extends Pool {
  async query(...args: any[]) {
    const start = Date.now();
    const query = args[0];

    try {
      const result = await super.query(...args);
      const duration = Date.now() - start;

      // Log slow queries
      if (duration > 100) {
        logger.warn('Slow query detected', {
          query: typeof query === 'string' ? query : query.text,
          duration,
          rows: result.rowCount,
        });
      }

      // Track metrics
      metrics.histogram('db_query_duration', duration, {
        slow: duration > 100,
      });

      return result;
    } catch (error) {
      metrics.increment('db_query_errors');
      throw error;
    }
  }
}

export const db = new MonitoredPool({
  connectionString: process.env.DATABASE_URL,
});

User Behavior Analytics

Event Tracking

typescript
// src/analytics/events.ts
interface AnalyticsEvent {
  userId?: string;
  sessionId: string;
  event: string;
  properties: Record<string, any>;
  timestamp: Date;
}

class Analytics {
  track(event: string, properties: Record<string, any> = {}) {
    const analyticsEvent: AnalyticsEvent = {
      userId: this.getUserId(),
      sessionId: this.getSessionId(),
      event,
      properties,
      timestamp: new Date(),
    };

    // Send to analytics service
    this.send(analyticsEvent);
  }

  private send(event: AnalyticsEvent) {
    // Send to Mixpanel, Amplitude, Segment, etc.
    if (process.env.ANALYTICS_ENABLED === 'true') {
      fetch(process.env.ANALYTICS_ENDPOINT, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(event),
      });
    }

    // Also store locally for analysis
    db.analyticsEvents.create(event);
  }

  private getUserId(): string | undefined {
    // Get from request context
    return RequestContext.getCurrentUserId();
  }

  private getSessionId(): string {
    return RequestContext.getSessionId();
  }
}

export const analytics = new Analytics();

// Usage
analytics.track('transaction_created', {
  amount: 5000,
  currency: 'USD',
  paymentMethod: 'card',
});

analytics.track('user_signed_up', {
  method: 'email',
  referralSource: 'google',
});

Feature Usage Tracking

typescript
// src/analytics/features.ts
interface FeatureUsage {
  feature: string;
  userId: string;
  action: 'viewed' | 'clicked' | 'completed';
  metadata?: Record<string, any>;
}

export function trackFeatureUsage(feature: string, action: 'viewed' | 'clicked' | 'completed', metadata?: Record<string, any>) {
  const usage: FeatureUsage = {
    feature,
    userId: RequestContext.getCurrentUserId(),
    action,
    metadata,
  };

  // Increment feature usage counter
  metrics.increment('feature_usage', {
    feature,
    action,
  });

  // Store for analysis
  db.featureUsage.create({
    ...usage,
    timestamp: new Date(),
  });

  // Track in analytics
  analytics.track(`feature_${action}`, {
    feature,
    ...metadata,
  });
}

// Usage in routes
app.get('/api/analytics/dashboard', async (req, res) => {
  trackFeatureUsage('analytics_dashboard', 'viewed');

  const data = await getAnalyticsDashboard();
  res.json(data);
});

app.post('/api/export', async (req, res) => {
  trackFeatureUsage('data_export', 'clicked', {
    format: req.body.format,
  });

  // ... export logic
});

Business Metrics

Revenue Tracking

typescript
// src/monitoring/business.ts
interface RevenueMetrics {
  totalRevenue: number;
  transactionCount: number;
  averageTransactionValue: number;
  period: 'hour' | 'day' | 'month';
}

class BusinessMetrics {
  async trackRevenue(amount: number, currency: string) {
    // Convert to USD for consistent tracking
    const usdAmount = await this.convertToUSD(amount, currency);

    // Increment revenue counter
    metrics.counter('revenue_usd', usdAmount);

    // Track transaction count
    metrics.increment('transaction_count');

    // Store for historical analysis
    await db.revenueMetrics.create({
      amount: usdAmount,
      currency: 'USD',
      originalAmount: amount,
      originalCurrency: currency,
      timestamp: new Date(),
    });
  }

  async getRevenueMetrics(period: 'hour' | 'day' | 'month'): Promise<RevenueMetrics> {
    const since = this.getPeriodStart(period);

    const transactions = await db.transactions.find({
      createdAt: { $gte: since },
      status: 'completed',
    });

    const totalRevenue = transactions.reduce((sum, t) => sum + t.amount, 0);
    const transactionCount = transactions.length;
    const averageTransactionValue = totalRevenue / transactionCount || 0;

    return {
      totalRevenue,
      transactionCount,
      averageTransactionValue,
      period,
    };
  }

  private getPeriodStart(period: 'hour' | 'day' | 'month'): Date {
    const now = new Date();
    switch (period) {
      case 'hour':
        now.setHours(now.getHours() - 1);
        break;
      case 'day':
        now.setDate(now.getDate() - 1);
        break;
      case 'month':
        now.setMonth(now.getMonth() - 1);
        break;
    }
    return now;
  }

  private async convertToUSD(amount: number, currency: string): Promise<number> {
    if (currency === 'USD') return amount;
    // Implement currency conversion
    const rate = await this.getExchangeRate(currency, 'USD');
    return amount * rate;
  }

  private async getExchangeRate(from: string, to: string): Promise<number> {
    // Fetch from exchange rate API
    return 1; // Placeholder
  }
}

export const businessMetrics = new BusinessMetrics();

// Usage
app.post('/api/transactions', async (req, res) => {
  const transaction = await createTransaction(req.body);

  await businessMetrics.trackRevenue(
    transaction.amount,
    transaction.currency
  );

  res.json(transaction);
});

User Growth Tracking

typescript
// src/monitoring/growth.ts
interface GrowthMetrics {
  newUsers: number;
  activeUsers: number;
  churnedUsers: number;
  retentionRate: number;
  period: string;
}

export async function trackUserGrowth(): Promise<GrowthMetrics> {
  const today = new Date();
  const yesterday = new Date(today);
  yesterday.setDate(yesterday.getDate() - 1);

  // New users today
  const newUsers = await db.users.count({
    createdAt: { $gte: yesterday },
  });

  // Active users (logged in within last 24h)
  const activeUsers = await db.users.count({
    lastLoginAt: { $gte: yesterday },
  });

  // Churned users (no activity in 30 days)
  const thirtyDaysAgo = new Date(today);
  thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);

  const churnedUsers = await db.users.count({
    lastLoginAt: { $lt: thirtyDaysAgo },
    status: 'active',
  });

  // Calculate retention rate
  const totalUsers = await db.users.count();
  const retentionRate = ((totalUsers - churnedUsers) / totalUsers) * 100;

  const metrics: GrowthMetrics = {
    newUsers,
    activeUsers,
    churnedUsers,
    retentionRate,
    period: 'daily',
  };

  // Send to monitoring
  Object.entries(metrics).forEach(([key, value]) => {
    if (typeof value === 'number') {
      prometheusMetrics.gauge(`user_${key}`, value);
    }
  });

  return metrics;
}

// Run daily
cron.schedule('0 0 * * *', async () => {
  const metrics = await trackUserGrowth();
  logger.info('Daily growth metrics', metrics);
});

Custom Dashboards

Grafana Dashboard Configuration

json
{
  "dashboard": {
    "title": "Pocketbook Business Metrics",
    "panels": [
      {
        "title": "Revenue (24h)",
        "targets": [
          {
            "expr": "sum(revenue_usd)",
            "legendFormat": "Total Revenue"
          }
        ],
        "type": "stat"
      },
      {
        "title": "Transaction Rate",
        "targets": [
          {
            "expr": "rate(transaction_count[5m])",
            "legendFormat": "Transactions/sec"
          }
        ],
        "type": "graph"
      },
      {
        "title": "Active Users",
        "targets": [
          {
            "expr": "user_activeUsers",
            "legendFormat": "Active Users (24h)"
          }
        ],
        "type": "stat"
      },
      {
        "title": "Feature Usage",
        "targets": [
          {
            "expr": "topk(10, sum by (feature) (feature_usage))",
            "legendFormat": "{{feature}}"
          }
        ],
        "type": "bar"
      }
    ]
  }
}

Error Monitoring

Error Rate Tracking

typescript
// src/monitoring/errors.ts
interface ErrorMetrics {
  type: string;
  count: number;
  lastOccurrence: Date;
  affectedUsers: string[];
}

class ErrorMonitor {
  private errors: Map<string, ErrorMetrics> = new Map();

  trackError(error: Error, userId?: string) {
    const errorType = error.constructor.name;

    // Update error metrics
    const existing = this.errors.get(errorType) || {
      type: errorType,
      count: 0,
      lastOccurrence: new Date(),
      affectedUsers: [],
    };

    existing.count++;
    existing.lastOccurrence = new Date();
    if (userId && !existing.affectedUsers.includes(userId)) {
      existing.affectedUsers.push(userId);
    }

    this.errors.set(errorType, existing);

    // Send to monitoring
    metrics.increment('application_errors', {
      type: errorType,
    });

    // Alert on high error rate
    if (this.isHighErrorRate(errorType)) {
      this.alertTeam(errorType, existing);
    }

    // Send to Sentry
    Sentry.captureException(error, {
      tags: { errorType },
      user: userId ? { id: userId } : undefined,
    });
  }

  private isHighErrorRate(errorType: string): boolean {
    const metrics = this.errors.get(errorType);
    if (!metrics) return false;

    // Alert if more than 10 errors in last 5 minutes
    return metrics.count > 10;
  }

  private alertTeam(errorType: string, metrics: ErrorMetrics) {
    // Send alert to Slack, PagerDuty, etc.
  }

  getErrorSummary(): ErrorMetrics[] {
    return Array.from(this.errors.values())
      .sort((a, b) => b.count - a.count)
      .slice(0, 10); // Top 10 errors
  }
}

export const errorMonitor = new ErrorMonitor();

// Global error handler
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
  errorMonitor.trackError(err, req.user?.id);

  res.status(500).json({
    error: {
      message: 'Internal server error',
      requestId: req.id,
    },
  });
});

Real-Time Monitoring

WebSocket Connection Tracking

typescript
// src/monitoring/websockets.ts
class WebSocketMonitor {
  private connections: Set<string> = new Set();

  onConnect(connectionId: string) {
    this.connections.add(connectionId);
    metrics.gauge('websocket_connections', this.connections.size);

    logger.info('WebSocket connected', {
      connectionId,
      totalConnections: this.connections.size,
    });
  }

  onDisconnect(connectionId: string) {
    this.connections.delete(connectionId);
    metrics.gauge('websocket_connections', this.connections.size);

    logger.info('WebSocket disconnected', {
      connectionId,
      totalConnections: this.connections.size,
    });
  }

  getConnectionCount(): number {
    return this.connections.size;
  }
}

export const wsMonitor = new WebSocketMonitor();

Performance Budgets

Set Performance Targets

typescript
// src/monitoring/budgets.ts
interface PerformanceBudget {
  endpoint: string;
  maxDuration: number; // milliseconds
  p95Threshold: number;
  p99Threshold: number;
}

const budgets: PerformanceBudget[] = [
  {
    endpoint: '/api/transactions',
    maxDuration: 500,
    p95Threshold: 300,
    p99Threshold: 400,
  },
  {
    endpoint: '/api/users',
    maxDuration: 200,
    p95Threshold: 150,
    p99Threshold: 180,
  },
];

export function checkPerformanceBudget(
  endpoint: string,
  duration: number
): boolean {
  const budget = budgets.find((b) => b.endpoint === endpoint);
  if (!budget) return true;

  if (duration > budget.maxDuration) {
    logger.warn('Performance budget exceeded', {
      endpoint,
      duration,
      budget: budget.maxDuration,
      exceeded: duration - budget.maxDuration,
    });

    metrics.increment('performance_budget_exceeded', { endpoint });

    return false;
  }

  return true;
}

Monitoring Best Practices

  1. Track the right metrics: Focus on actionable metrics
  2. Set up alerts: Be notified of issues before users report them
  3. Monitor trends: Look for patterns over time
  4. Segment data: Break down metrics by user cohorts, regions, etc.
  5. Privacy-first: Anonymize user data in analytics
  6. Performance budgets: Set and enforce performance targets
  7. Business metrics: Track KPIs alongside technical metrics
  8. Regular reviews: Review dashboards weekly
  9. A/B testing: Monitor impact of feature changes
  10. User feedback: Combine metrics with qualitative feedback

Compliance & Privacy

GDPR-Compliant Analytics

typescript
// src/analytics/gdpr.ts
export function trackEvent(event: string, properties: any, consent: boolean) {
  if (!consent) {
    // Only track anonymized metrics
    metrics.increment(event);
    return;
  }

  // Full tracking with user consent
  analytics.track(event, properties);
}

// Right to deletion
export async function deleteUserAnalytics(userId: string) {
  await db.analyticsEvents.deleteMany({ userId });
  await db.featureUsage.deleteMany({ userId });

  logger.info('User analytics deleted', { userId });
}

Next Steps

Resources

Released under the MIT License.