Production Debugging and Monitoring

Monitor dcupl applications in production with error tracking, performance metrics, and data integrity alerts.

Prerequisites

  • Working dcupl application deployed to production
  • Basic understanding of lists and data loading
  • Error tracking service (Sentry, DataDog, or similar)

Error Tracking with Sentry

Integrate Sentry to capture dcupl errors and provide context for debugging.

Install Sentry

npm install @sentry/browser
yarn add @sentry/browser
pnpm add @sentry/browser

Initialize Sentry with dcupl Context

monitoring/sentry.ts
import * as Sentry from '@sentry/browser';
import type { Dcupl } from '@dcupl/core';

export function initSentry(dsn: string) {
  Sentry.init({
    dsn,
    environment: import.meta.env.MODE,
    beforeSend(event) {
      // Filter out non-critical dcupl errors if needed
      if (event.exception?.values?.[0]?.type === 'DcuplValidationError') {
        event.level = 'warning';
      }
      return event;
    },
  });
}

export function setDcuplContext(dcupl: Dcupl) {
  // Add dcupl state as context for all errors
  Sentry.setContext('dcupl', {
    models: dcupl.models.keys(),
    listsCount: dcupl.lists.getAll().length,
  });
}

Capture Loader Errors

Track data loading failures with full context.

monitoring/loader-tracking.ts
import * as Sentry from '@sentry/browser';
import type { DcuplAppLoader } from '@dcupl/loader';
import type { AppLoaderProgress } from '@dcupl/loader';

export function trackLoaderErrors(loader: DcuplAppLoader) {
  loader.on((progress: AppLoaderProgress.ProgressItem) => {
    // Track failed resources
    const failedResources = progress.resources.filter((r) => r.status === 'error');

    for (const resource of failedResources) {
      Sentry.captureException(new Error(`Failed to load resource: ${resource.url}`), {
        tags: {
          resourceType: resource.type,
          model: resource.model || 'unknown',
        },
        extra: {
          url: resource.url,
          httpStatus: resource.httpStatus,
          resourceKey: resource.key,
        },
      });
    }

    // Alert on high failure rate
    const failureRate = failedResources.length / progress.resources.length;
    if (failureRate > 0.1) {
      Sentry.captureMessage('High resource failure rate detected', {
        level: 'error',
        extra: {
          failureRate: `${(failureRate * 100).toFixed(1)}%`,
          failedCount: failedResources.length,
          totalCount: progress.resources.length,
        },
      });
    }
  });
}

Track Quality Controller Errors

Monitor data validation errors that occur during initialization.

monitoring/quality-tracking.ts
import * as Sentry from '@sentry/browser';
import type { Dcupl } from '@dcupl/core';

export function trackQualityErrors(dcupl: Dcupl) {
  // Enable error tracking in dcupl
  // Note: errorTracking must be enabled in init options

  // After initialization, check for quality errors
  const errors = dcupl.quality.getErrors();

  if (errors.length > 0) {
    // Group errors by type
    const errorsByType = errors.reduce(
      (acc, error) => {
        const type = error.errorType || 'Unknown';
        acc[type] = (acc[type] || 0) + 1;
        return acc;
      },
      {} as Record<string, number>
    );

    Sentry.captureMessage('dcupl quality errors detected', {
      level: 'warning',
      extra: {
        totalErrors: errors.length,
        errorsByType,
        sampleErrors: errors.slice(0, 5).map((e) => ({
          type: e.errorType,
          model: e.model,
          attribute: e.attribute,
          description: e.description,
        })),
      },
    });
  }
}

Performance Monitoring

Track initialization time, query performance, and resource loading.

Measure Initialization Time

monitoring/performance.ts
import type { Dcupl } from '@dcupl/core';
import type { DcuplAppLoader } from '@dcupl/loader';

interface PerformanceMetrics {
  configFetch: number;
  resourceLoading: number;
  initialization: number;
  total: number;
}

export async function measureStartupPerformance(
  dcupl: Dcupl,
  loader: DcuplAppLoader
): Promise<PerformanceMetrics> {
  const metrics: PerformanceMetrics = {
    configFetch: 0,
    resourceLoading: 0,
    initialization: 0,
    total: 0,
  };

  const totalStart = performance.now();

  // Measure config fetch
  const configStart = performance.now();
  await loader.config.fetch();
  metrics.configFetch = performance.now() - configStart;

  // Measure resource loading
  const loadStart = performance.now();
  await loader.process();
  metrics.resourceLoading = performance.now() - loadStart;

  // Measure initialization
  const initStart = performance.now();
  await dcupl.init();
  metrics.initialization = performance.now() - initStart;

  metrics.total = performance.now() - totalStart;

  return metrics;
}

Track Query Performance

monitoring/query-performance.ts
import type { DcuplList } from '@dcupl/core';

interface QueryMetric {
  operation: string;
  duration: number;
  resultCount: number;
  modelKey: string;
}

const queryMetrics: QueryMetric[] = [];

export function trackQueryPerformance<T>(
  list: DcuplList,
  operation: string,
  fn: () => T
): T {
  const start = performance.now();
  const result = fn();
  const duration = performance.now() - start;

  const metric: QueryMetric = {
    operation,
    duration,
    resultCount: Array.isArray(result) ? result.length : 1,
    modelKey: list.catalog.fn.metadata().key,
  };

  queryMetrics.push(metric);

  // Alert on slow queries
  if (duration > 100) {
    console.warn(`Slow query detected: ${operation} took ${duration.toFixed(2)}ms`);
  }

  return result;
}

// Usage example
function getFilteredProducts(list: DcuplList, category: string) {
  return trackQueryPerformance(list, 'filterByCategory', () => {
    list.catalog.query.apply({
      groupKey: 'category',
      attribute: 'category',
      operator: 'eq',
      value: category,
    });
    return list.catalog.query.items();
  });
}

export function getQueryMetrics(): QueryMetric[] {
  return [...queryMetrics];
}

export function getSlowQueries(thresholdMs = 50): QueryMetric[] {
  return queryMetrics.filter((m) => m.duration > thresholdMs);
}

Resource Loading Metrics

monitoring/resource-metrics.ts
import type { DcuplAppLoader, AppLoaderProgress } from '@dcupl/loader';

interface ResourceMetrics {
  totalResources: number;
  loadedResources: number;
  failedResources: number;
  totalSize: number;
  loadTime: number;
  byType: Record<string, { count: number; size: number }>;
}

export function collectResourceMetrics(
  loader: DcuplAppLoader
): Promise<ResourceMetrics> {
  return new Promise((resolve) => {
    let metrics: ResourceMetrics = {
      totalResources: 0,
      loadedResources: 0,
      failedResources: 0,
      totalSize: 0,
      loadTime: 0,
      byType: {},
    };

    const startTime = performance.now();

    const unsubscribe = loader.on((progress: AppLoaderProgress.ProgressItem) => {
      if (progress.status === 'complete' || progress.status === 'error') {
        metrics.totalResources = progress.resources.length;
        metrics.loadedResources = progress.resources.filter(
          (r) => r.status === 'success'
        ).length;
        metrics.failedResources = progress.resources.filter(
          (r) => r.status === 'error'
        ).length;
        metrics.loadTime = performance.now() - startTime;

        // Group by type
        for (const resource of progress.resources) {
          const type = resource.type || 'unknown';
          if (!metrics.byType[type]) {
            metrics.byType[type] = { count: 0, size: 0 };
          }
          metrics.byType[type].count++;
        }

        unsubscribe();
        resolve(metrics);
      }
    });
  });
}

Logging Best Practices

Implement structured logging for production debugging.

Structured Logger

monitoring/logger.ts
type LogLevel = 'debug' | 'info' | 'warn' | 'error';

interface LogEntry {
  timestamp: string;
  level: LogLevel;
  message: string;
  context?: Record<string, unknown>;
  error?: Error;
}

class DcuplLogger {
  private logs: LogEntry[] = [];
  private maxLogs = 1000;

  private log(level: LogLevel, message: string, context?: Record<string, unknown>, error?: Error) {
    const entry: LogEntry = {
      timestamp: new Date().toISOString(),
      level,
      message,
      context,
      error,
    };

    this.logs.push(entry);

    // Keep log buffer bounded
    if (this.logs.length > this.maxLogs) {
      this.logs.shift();
    }

    // Console output in development
    if (import.meta.env.DEV) {
      const logFn = level === 'error' ? console.error : level === 'warn' ? console.warn : console.log;
      logFn(`[dcupl:${level}]`, message, context || '', error || '');
    }

    // Send critical logs to monitoring service in production
    if (level === 'error' && import.meta.env.PROD) {
      this.sendToMonitoring(entry);
    }
  }

  debug(message: string, context?: Record<string, unknown>) {
    this.log('debug', message, context);
  }

  info(message: string, context?: Record<string, unknown>) {
    this.log('info', message, context);
  }

  warn(message: string, context?: Record<string, unknown>) {
    this.log('warn', message, context);
  }

  error(message: string, error?: Error, context?: Record<string, unknown>) {
    this.log('error', message, context, error);
  }

  getLogs(level?: LogLevel): LogEntry[] {
    if (level) {
      return this.logs.filter((l) => l.level === level);
    }
    return [...this.logs];
  }

  private sendToMonitoring(entry: LogEntry) {
    // Send to your monitoring service
    // Example: Sentry, DataDog, CloudWatch, etc.
  }
}

export const logger = new DcuplLogger();

Log dcupl Lifecycle Events

app.ts
import { Dcupl } from '@dcupl/core';
import { DcuplAppLoader } from '@dcupl/loader';
import { logger } from './monitoring/logger';

const dcupl = new Dcupl();
const loader = new DcuplAppLoader();

// Track loader progress
loader.on((progress) => {
  logger.info('Loader progress', {
    status: progress.status,
    loaded: progress.loaded,
    total: progress.total,
    percentage: `${((progress.loaded / progress.total) * 100).toFixed(1)}%`,
  });

  // Log individual resource failures
  const failures = progress.resources.filter((r) => r.status === 'error');
  for (const failure of failures) {
    logger.error('Resource load failed', undefined, {
      url: failure.url,
      type: failure.type,
      httpStatus: failure.httpStatus,
    });
  }
});

async function initialize() {
  try {
    logger.info('Starting dcupl initialization');

    await loader.config.fetch();
    logger.info('Config fetched', {
      resourceCount: loader.resources.getAll().length,
    });

    await loader.process();
    logger.info('Resources processed');

    await dcupl.init();
    logger.info('dcupl initialized', {
      models: dcupl.models.keys(),
    });
  } catch (error) {
    logger.error('Initialization failed', error as Error);
    throw error;
  }
}

Data Integrity Alerts

Monitor data quality and alert on integrity issues.

Data Validation Service

monitoring/data-validation.ts
import type { Dcupl } from '@dcupl/core';
import { logger } from './logger';

interface ValidationRule {
  modelKey: string;
  check: (dcupl: Dcupl) => ValidationResult;
}

interface ValidationResult {
  passed: boolean;
  message: string;
  details?: Record<string, unknown>;
}

const validationRules: ValidationRule[] = [];

export function addValidationRule(rule: ValidationRule) {
  validationRules.push(rule);
}

export function runValidations(dcupl: Dcupl): ValidationResult[] {
  const results: ValidationResult[] = [];

  for (const rule of validationRules) {
    try {
      const result = rule.check(dcupl);
      results.push(result);

      if (!result.passed) {
        logger.warn('Validation failed', {
          model: rule.modelKey,
          message: result.message,
          details: result.details,
        });
      }
    } catch (error) {
      results.push({
        passed: false,
        message: `Validation error: ${(error as Error).message}`,
      });
    }
  }

  return results;
}

// Example validation rules
addValidationRule({
  modelKey: 'Product',
  check: (dcupl) => {
    const list = dcupl.lists.create({ modelKey: 'Product' });
    const count = list.catalog.query.count();
    list.destroy();

    if (count === 0) {
      return {
        passed: false,
        message: 'No products loaded',
        details: { expectedMinimum: 1, actual: 0 },
      };
    }

    return { passed: true, message: 'Products loaded successfully' };
  },
});

addValidationRule({
  modelKey: 'Product',
  check: (dcupl) => {
    const list = dcupl.lists.create({ modelKey: 'Product' });

    // Check for products without prices
    list.catalog.query.apply({
      attribute: 'price',
      operator: 'isNull',
      value: true,
    });

    const missingPrices = list.catalog.query.count();
    list.destroy();

    if (missingPrices > 0) {
      return {
        passed: false,
        message: 'Products with missing prices detected',
        details: { count: missingPrices },
      };
    }

    return { passed: true, message: 'All products have prices' };
  },
});

Scheduled Health Checks

monitoring/health-check.ts
import type { Dcupl } from '@dcupl/core';
import { runValidations } from './data-validation';
import { logger } from './logger';

interface HealthStatus {
  status: 'healthy' | 'degraded' | 'unhealthy';
  checks: {
    name: string;
    passed: boolean;
    message: string;
  }[];
  timestamp: string;
}

export function checkHealth(dcupl: Dcupl): HealthStatus {
  const checks: HealthStatus['checks'] = [];

  // Check if dcupl is initialized
  try {
    const modelCount = dcupl.models.keys().length;
    checks.push({
      name: 'initialization',
      passed: modelCount > 0,
      message: modelCount > 0 ? `${modelCount} models loaded` : 'No models loaded',
    });
  } catch (error) {
    checks.push({
      name: 'initialization',
      passed: false,
      message: (error as Error).message,
    });
  }

  // Run data validations
  const validationResults = runValidations(dcupl);
  for (const result of validationResults) {
    checks.push({
      name: 'data-validation',
      passed: result.passed,
      message: result.message,
    });
  }

  // Check quality errors
  try {
    const errors = dcupl.quality.getErrors();
    const criticalErrors = errors.filter(
      (e) => e.errorType === 'MissingModel' || e.errorType === 'InvalidResource'
    );

    checks.push({
      name: 'quality',
      passed: criticalErrors.length === 0,
      message:
        criticalErrors.length === 0
          ? 'No critical quality errors'
          : `${criticalErrors.length} critical errors`,
    });
  } catch {
    // Quality may not be enabled
    checks.push({
      name: 'quality',
      passed: true,
      message: 'Quality tracking not enabled',
    });
  }

  // Determine overall status
  const failedChecks = checks.filter((c) => !c.passed);
  let status: HealthStatus['status'] = 'healthy';

  if (failedChecks.length > 0) {
    status = failedChecks.some((c) => c.name === 'initialization') ? 'unhealthy' : 'degraded';
  }

  const healthStatus: HealthStatus = {
    status,
    checks,
    timestamp: new Date().toISOString(),
  };

  // Log health status
  if (status !== 'healthy') {
    logger.warn('Health check failed', { status: healthStatus });
  }

  return healthStatus;
}

Debugging Production Issues

Techniques for diagnosing problems in production.

Export Debug Information

monitoring/debug-export.ts
import type { Dcupl } from '@dcupl/core';
import { logger } from './logger';

interface DebugSnapshot {
  timestamp: string;
  models: {
    key: string;
    itemCount: number;
    properties: string[];
  }[];
  lists: {
    modelKey: string;
    currentSize: number;
    initialSize: number;
  }[];
  qualityErrors: {
    type: string;
    model: string;
    count: number;
  }[];
  recentLogs: unknown[];
}

export function createDebugSnapshot(dcupl: Dcupl): DebugSnapshot {
  const snapshot: DebugSnapshot = {
    timestamp: new Date().toISOString(),
    models: [],
    lists: [],
    qualityErrors: [],
    recentLogs: logger.getLogs().slice(-100),
  };

  // Collect model info
  for (const modelKey of dcupl.models.keys()) {
    const model = dcupl.models.get(modelKey);
    if (model) {
      const list = dcupl.lists.create({ modelKey });
      snapshot.models.push({
        key: modelKey,
        itemCount: list.catalog.query.count(),
        properties: model.properties?.map((p) => p.key) || [],
      });
      list.destroy();
    }
  }

  // Collect list info
  for (const list of dcupl.lists.getAll()) {
    const meta = list.catalog.fn.metadata();
    snapshot.lists.push({
      modelKey: meta.key,
      currentSize: meta.currentSize,
      initialSize: meta.initialSize,
    });
  }

  // Collect quality errors grouped by type and model
  try {
    const errors = dcupl.quality.getErrors();
    const errorMap = new Map<string, number>();

    for (const error of errors) {
      const key = `${error.errorType}:${error.model}`;
      errorMap.set(key, (errorMap.get(key) || 0) + 1);
    }

    for (const [key, count] of errorMap) {
      const [type, model] = key.split(':');
      snapshot.qualityErrors.push({ type, model, count });
    }
  } catch {
    // Quality may not be enabled
  }

  return snapshot;
}

// Expose for browser console debugging
if (typeof window !== 'undefined') {
  (window as any).__dcuplDebug = {
    createSnapshot: createDebugSnapshot,
    getLogs: () => logger.getLogs(),
  };
}

Remote Debugging with Console

Enable dcupl Console connection only when needed.

debug/remote-debug.ts
import type { Dcupl } from '@dcupl/core';
import { DcuplConnect } from '@dcupl/connect';

let connectInstance: DcuplConnect | null = null;

export function enableRemoteDebug(dcupl: Dcupl) {
  if (connectInstance) {
    console.warn('Remote debug already enabled');
    return;
  }

  connectInstance = new DcuplConnect({ dcuplInstance: dcupl });
  connectInstance.init();

  console.log('Remote debug enabled - connect via dcupl Console');
}

export function disableRemoteDebug() {
  if (connectInstance) {
    connectInstance = null;
    console.log('Remote debug disabled');
  }
}

// Expose to browser console for on-demand debugging
if (typeof window !== 'undefined') {
  (window as any).__dcuplEnableDebug = enableRemoteDebug;
  (window as any).__dcuplDisableDebug = disableRemoteDebug;
}

Common Production Problems

Problem: Data Not Loading

Symptoms: Empty results, missing models

Debug steps:

// 1. Check loader progress
loader.on((progress) => {
  console.log('Resources:', progress.resources);
  const failed = progress.resources.filter((r) => r.status === 'error');
  console.log('Failed:', failed);
});

// 2. Check registered models
console.log('Models:', dcupl.models.keys());

// 3. Check quality errors
console.log('Errors:', dcupl.quality.getErrors());

Common causes:

  • Network errors (CORS, DNS, SSL)
  • Invalid API key or project ID
  • CDN version mismatch
  • Malformed model/data files

Problem: Slow Initialization

Symptoms: Long startup time, timeout errors

Debug steps:

// Measure each phase
const metrics = await measureStartupPerformance(dcupl, loader);
console.log('Config fetch:', metrics.configFetch, 'ms');
console.log('Resource loading:', metrics.resourceLoading, 'ms');
console.log('Initialization:', metrics.initialization, 'ms');

Common causes:

  • Too many resources loaded at once
  • Large data files without compression
  • Slow CDN response times
  • Complex model relationships

Solutions:

  • Use resource tags to load only needed data
  • Enable gzip/brotli compression
  • Use a CDN closer to users
  • Consider lazy loading for large datasets

Problem: Memory Issues

Symptoms: Browser crashes, slow performance over time

Debug steps:

// Check list count
console.log('Active lists:', dcupl.lists.getAll().length);

// Check data size per model
for (const modelKey of dcupl.models.keys()) {
  const list = dcupl.lists.create({ modelKey });
  console.log(`${modelKey}: ${list.catalog.query.count()} items`);
  list.destroy();
}

Common causes:

  • Lists not destroyed after use
  • Loading more data than needed
  • Unused models consuming memory

Solutions:

  • Always call list.destroy() when done
  • Use resource tags to limit loaded data
  • Implement pagination for large result sets

Problem: Stale Data

Symptoms: Old data displayed, updates not reflected

Debug steps:

// Check CDN version
console.log('Variables:', loader.variables.getAll());

// Force cache bypass
await loader.config.fetch({ skipApply: false });
await loader.process({
  environmentKeys: ['production'],
});
await dcupl.init();

Common causes:

  • Browser cache serving old files
  • CDN cache not invalidated
  • Missing version query parameter

Solutions:

  • Ensure CDN version is updated on publish
  • Use cache-busting query parameters
  • Configure appropriate cache headers

Complete Monitoring Setup

monitoring/index.ts
import { Dcupl } from '@dcupl/core';
import { DcuplAppLoader } from '@dcupl/loader';
import { initSentry, setDcuplContext } from './sentry';
import { trackLoaderErrors } from './loader-tracking';
import { trackQualityErrors } from './quality-tracking';
import { measureStartupPerformance } from './performance';
import { checkHealth } from './health-check';
import { logger } from './logger';

export async function initializeWithMonitoring(config: {
  projectId: string;
  apiKey: string;
  sentryDsn?: string;
}) {
  // Initialize Sentry
  if (config.sentryDsn) {
    initSentry(config.sentryDsn);
  }

  const dcupl = new Dcupl();
  const loader = new DcuplAppLoader();

  dcupl.addRunner(loader);

  // Set up loader variables
  loader.variables.global.set('projectId', config.projectId);
  loader.variables.global.set('apiKey', config.apiKey);

  // Track loader errors
  trackLoaderErrors(loader);

  try {
    // Measure and log performance
    const metrics = await measureStartupPerformance(dcupl, loader);

    logger.info('Startup complete', {
      configFetch: `${metrics.configFetch.toFixed(0)}ms`,
      resourceLoading: `${metrics.resourceLoading.toFixed(0)}ms`,
      initialization: `${metrics.initialization.toFixed(0)}ms`,
      total: `${metrics.total.toFixed(0)}ms`,
    });

    // Set Sentry context
    setDcuplContext(dcupl);

    // Track quality errors
    trackQualityErrors(dcupl);

    // Run health check
    const health = checkHealth(dcupl);

    if (health.status !== 'healthy') {
      logger.warn('Application started in degraded state', { health });
    }

    return { dcupl, loader, health };
  } catch (error) {
    logger.error('Initialization failed', error as Error);
    throw error;
  }
}