.md

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, // Vite; use process.env.NODE_ENV with other build tools
    // The core SDK logs only to the console: this reports its errors, including
    // the cause of a failed init() ('[Dcupl] Initialization failed')
    integrations: [Sentry.captureConsoleIntegration({ levels: ['error'] })],
  });
}

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,
  });
}

dcupl does not throw for invalid data. Data-quality problems (wrong types, missing required values, unknown references) are collected per model and attribute and read from dcupl.quality — see Track Data Quality Errors.

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) => {
    // Every update carries the whole run so far, so failedItems repeats earlier
    // failures. Report once, when the run is finished.
    if (progress.remaining !== 0) return;

    // Resources that failed to download, already collected for you
    const failedResources = progress.failedItems;

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

    // Alert on high failure rate
    const failureRate = failedResources.length / progress.totalItems.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.totalItems.length,
        },
      });
    }
  });
}

Detect skipped resources

A failed resource is loud — it shows up in failedItems. A resource that was never scheduled is silent: if the run filters by resourceTags and a resource carries no matching tag, it is simply not part of the run. totalItems covers only what was scheduled, so on its own it cannot tell "loaded everything" apart from "loaded everything this application was allowed to see".

skippedItems closes that gap — every declared resource that dropped out, with the reason — and run records what the run filtered by.

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

export function trackSkippedResources(loader: DcuplAppLoader, expectedModels: string[]) {
  loader.on((progress: AppLoaderProgress.ProgressItem) => {
    if (progress.remaining !== 0) return;

    const skipped = progress.skippedItems ?? [];

    // A model you expected in production that no resource in this run provides
    const scheduledModels = new Set(progress.totalItems.flatMap((r) => (r.type === 'data' ? [r.model] : [])));
    const missing = expectedModels.filter((m) => !scheduledModels.has(m));

    if (missing.length) {
      Sentry.captureMessage('Expected model not scheduled for this run', {
        level: 'warning',
        extra: {
          missing,
          run: progress.run, // applicationKey / resourceTags / environmentKeys
          skipped: skipped.map((s) => ({
            url: s.resource.url,
            model: s.resource.type === 'data' ? s.resource.model : undefined,
            reason: s.reason, // 'no-tags' | 'tag-mismatch'
          })),
        },
      });
    }
  });
}

reason tells you which mistake you made:

Reason Meaning Usual cause
no-tags The run filters by resourceTags, and the resource carries no tags at all A resource added to the config without tagging it — it silently disappears from every tag-filtered run
tag-mismatch The run filters by resourceTags, and none matched the resource's tags A typo in the tag, or a deploy passing the wrong resourceTags

To catch this before a deploy rather than from production telemetry, resolve the same plan without loading anything:

const plan = loader.resources.plan({ resourceTags: ['critical'] });
console.log(plan.skipped); // same SkippedResource[], no I/O

See the DcuplAppLoader API reference for the full ResourcePlan shape.

Track data quality errors

Monitor data validation errors that occur during initialization and updates. Quality checks are on by default (new Dcupl({ quality: { enabled: true } })).

  • dcupl.quality.errors.summary() returns one entry per model, attribute and error type with its count: { key, model, attribute, errorGroup, errorType, type, totalCount, storedCount, truncated }. Loader errors (type: 'loader') are included.
  • The individual error records live in the built-in $DcuplErrorTrackingErrors model; query it like any other model. At most maxErrorsPerGroup records (default 1000) are stored per group — truncated: true in the summary means the records are a sample, while totalCount keeps counting. Failed type coercions are consolidated into one WrongDataType record per attribute; when more values of one attribute fail than maxErrorsPerGroup, those counts are not exact (see Limiting Stored Errors).
  • The $DcuplErrorTrackingErrors model is rebuilt by init() and, after a partial update, whenever an error record changed, so it and summary() are current after dcupl.update().
  • A partial update leaves the same error records and summary() counts as a fresh init() on the same data: the changed rows are re-validated (unique included), the errors of removed rows are gone, and a value of the wrong type in data.update() is recorded as WrongDataType and dropped. The item then has no value for that attribute; its previous value is not kept. One exception: a keyless row in a data.upsert() that a partial update applies is currently dropped without a MissingKey record; init() and a full rebuild record it.
monitoring/quality-tracking.ts
import * as Sentry from '@sentry/browser';
import type { Dcupl } from '@dcupl/core';

export function trackQualityErrors(dcupl: Dcupl) {
  // After init() / update(): one entry per model, attribute and error type
  const summary = dcupl.quality.errors.summary();

  if (summary.length > 0) {
    const totalErrors = summary.reduce((sum, group) => sum + group.totalCount, 0);

    // Count errors by type
    const errorsByType = summary.reduce(
      (acc, group) => {
        acc[group.errorType] = (acc[group.errorType] || 0) + group.totalCount;
        return acc;
      },
      {} as Record<string, number>,
    );

    // A few individual records; model, errorGroup and errorType are references ({ key })
    const sampleErrors = dcupl.query.execute({ modelKey: '$DcuplErrorTrackingErrors', queries: [], count: 5 });

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

To list the records of one model, filter the error model: dcupl.query.execute({ modelKey: '$DcuplErrorTrackingErrors', queries: [{ attribute: 'model', operator: 'eq', value: 'Product' }] }). With quality: { enabled: false }, the $DcuplErrorTrackingErrors model does not exist and summary() lists only loader errors (resources that failed to load, type: 'loader'), so it is not empty when a resource failed.

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 (init() resolves false instead of throwing)
  const initStart = performance.now();
  if (!(await dcupl.init())) {
    throw new Error('dcupl initialization failed');
  }
  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', () => {
    // setCondition replaces the previous category condition
    list.catalog.query.setCondition({
      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;
  loadTime: number;
  byType: Record<string, number>; // resources per type
}

// Call it before loader.process(): it resolves when the next run has finished
export function collectResourceMetrics(loader: DcuplAppLoader): Promise<ResourceMetrics> {
  return new Promise((resolve) => {
    const metrics: ResourceMetrics = {
      totalResources: 0,
      loadedResources: 0,
      failedResources: 0,
      loadTime: 0,
      byType: {},
    };

    const startTime = performance.now();

    const unsubscribe = loader.on((progress: AppLoaderProgress.ProgressItem) => {
      // Every scheduled resource has been processed
      if (progress.remaining === 0) {
        metrics.totalResources = progress.totalItems.length;
        metrics.loadedResources = progress.downloadedItems.length;
        metrics.failedResources = progress.failedItems.length;
        metrics.loadTime = performance.now() - startTime;

        // Group by type
        for (const resource of progress.totalItems) {
          const type = resource.type || 'unknown';
          metrics.byType[type] = (metrics.byType[type] ?? 0) + 1;
        }

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

Start it before the run and await it afterwards: const metrics = collectResourceMetrics(loader); await loader.process(); console.log(await metrics);. The progress items carry no byte sizes. For transfer sizes, read the browser's performance.getEntriesByType('resource') entries for the resource URLs.

Logging best practices

Route dcupl's logs

dcupl writes two kinds of logs, and only one of them can be redirected:

  • Loader logs ([AppLoader] …, [CsvParser] …) go through the logger from @dcupl/common. Call logger.configure({ onLog }) before you create the loader to send them to your monitoring service (Logging Configuration).
  • Core SDK logs ([Dcupl] …, [ModelParser] …, [Catalog] …) always go to the console. That includes the only copy of the cause of a failed init() or update(). Capture console errors to keep them, as the captureConsoleIntegration in Initialize Sentry with dcupl Context does.

Structured logger

A logger of your own for your app's messages (it doesn't receive dcupl's logs). It's named appLogger so it doesn't clash with the logger from @dcupl/common:

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

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

class AppLogger {
  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 (import.meta.env is Vite's; use process.env.NODE_ENV elsewhere)
    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 appLogger = new AppLogger();

Log dcupl lifecycle events

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

const dcupl = new Dcupl();
const loader = new DcuplAppLoader();
dcupl.loaders.add(loader);

// Track loader progress
loader.on((progress) => {
  appLogger.info('Loader progress', {
    context: progress.context,
    processed: progress.processed,
    total: progress.total,
    percentage: `${progress.progress}%`,
  });

  // failedItems lists every failure of the run so far: log them once, when the run is finished
  if (progress.remaining !== 0) return;
  for (const failure of progress.failedItems) {
    appLogger.error('Resource load failed', undefined, {
      url: failure.url,
      type: failure.type,
      httpStatus: failure.status,
    });
  }
});

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

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

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

    // init() doesn't throw: it resolves false when initialization failed
    if (!(await dcupl.init())) {
      throw new Error('dcupl initialization failed');
    }
    appLogger.info('dcupl initialized', {
      models: dcupl.models.keys(),
    });
  } catch (error) {
    appLogger.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 { appLogger } 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) {
        appLogger.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 (price undefined or null)
    list.catalog.query.addCondition({
      attribute: 'price',
      operator: 'typeof',
      value: 'number',
      options: { invert: 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 { appLogger } 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 (with quality disabled, summary() still lists loader errors such as InvalidResource)
  const criticalErrors = dcupl.quality.errors
    .summary()
    .filter((group) => group.errorType === 'MissingModel' || group.errorType === 'InvalidResource')
    .reduce((sum, group) => sum + group.totalCount, 0);

  checks.push({
    name: 'quality',
    passed: criticalErrors === 0,
    message: criticalErrors === 0 ? 'No critical quality errors' : `${criticalErrors} critical errors`,
  });

  // 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') {
    appLogger.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 { appLogger } 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: appLogger.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
  const errorMap = new Map<string, number>();

  for (const group of dcupl.quality.errors.summary()) {
    const key = `${group.errorType}:${group.model ?? 'unknown'}`;
    errorMap.set(key, (errorMap.get(key) || 0) + group.totalCount);
  }

  for (const [key, count] of errorMap) {
    const [type, model] = key.split(':');
    snapshot.qualityErrors.push({ type, model, count });
  }

  return snapshot;
}

// Expose for browser console debugging
if (typeof window !== 'undefined') {
  (window as any).__dcuplDebug = {
    createSnapshot: createDebugSnapshot,
    getLogs: () => appLogger.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 });
  // Not awaited: when a page other than the Console opened the app, init() never settles
  void connectInstance.init();

  // Connects only when the app window was opened from dcupl Connect in the Console
  console.log('Remote debug enabled - connect via dcupl Console');
}

export function disableRemoteDebug() {
  if (connectInstance) {
    connectInstance.destroy(); // a no-op when init() never connected
    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;
}

See Debugging with Console for how the Console opens and connects your app.

Common production problems

Problem: data not loading

Symptoms: Empty results, missing models

Debug steps:

// 1. Check loader progress
loader.on((progress) => {
  console.log('Scheduled:', progress.totalItems);
  console.log('Failed:', progress.failedItems);

  // Declared in the config but never scheduled for this run
  console.log('Skipped:', progress.skippedItems);
  console.log('Ran as:', progress.run);
});

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

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

What the messages mean:

  • loader.config.fetch() throws Invalid Loader Configuration, logged as [AppLoader] Could not fetch loader configuration. Are you sure your projectId + apiKey are correct?: a wrong projectId or apiKey, a version that doesn't exist, or a network error. With a non-dcupl baseUrl the log line ends after "configuration".
  • A model or data file is missing: [AppLoader] Failed to load data resource with { url, model }, or [AppLoader] Failed to load model resource with { url }, and an InvalidResource error in summary() with type: 'loader'. The file doesn't exist at that URL, or it was refused (status in failedItems, e.g. 403 on a private project when the key is missing or doesn't cover the path).
  • A model has no items although init() was true: if the log shows [ModelParser] Model not found during data processing, data was staged with data.set() for a model key that isn't registered, or a data resource's model isn't declared while missing-model handling is off (missingModelHandling.autoGenerateProperties is false or not set inside a missingModelHandling object). With the default options there is no log line: the loader creates a model under the data resource's model key and loads the rows there, so check dcupl.models.keys() for a key you didn't expect.

Common causes:

  • Network errors (CORS, DNS, SSL)
  • Invalid API key or project ID
  • CDN version mismatch
  • Malformed model/data files
  • The resource was never scheduled — check skippedItems for a no-tags / tag-mismatch reason before assuming a network failure (see Detect Skipped Resources)
  • An unresolved ${variable} in the url — an unknown variable is substituted with an empty string, so a broken config looks like a missing file. Check unresolvedVariables on the scheduled item.

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());

Refresh the data: fetch the configuration and run loader.process() again, then apply the result with dcupl.update(). A plain process() reloads every resource; to skip the model files, reload only the data resources, for example by tagging them (here data):

await loader.config.fetch(); // Picks up a new configuration and renews the ?v= version parameter of CDN urls
await loader.process({ resourceTags: ['data'] }); // Only the resources tagged 'data'
await dcupl.update();

Data resources replace their model's rows by default (updateType: 'set'), so the refreshed rows take the place of the old ones. A re-process also rebuilds the models it loads from their current files and drops the resources that left the configuration, so changed models need no new Dcupl instance, and the lists your app already holds serve the new rows (Processing). Re-process the loader you have: a second loader for the same models rejects with SharedModelKeyError. Without a loader, fetch the data yourself and stage it with dcupl.data.set(items, { model }) (or upsert) before await dcupl.update().

Common causes:

  • An app or process that stays open and doesn't reload the data after a publish
  • config.version names a different version than the one you published to (the default is draft)
  • Files on your own host served with long cache headers

Solutions:

  • Reload the data after each publish as shown above: loader.config.fetch() renews the ?v= version parameter of dcupl CDN URLs, so browsers and caches fetch the new files
  • Set config.version to the version you publish to
  • For files on your own host, set short cache headers or put a version in their URLs

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 { appLogger } 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.loaders.add(loader, 'main');

  // 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);

    appLogger.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') {
      appLogger.warn('Application started in degraded state', { health });
    }

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