# 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](/sdk/core-concepts/lists) and [data loading](/sdk/core-concepts/data-management)
- Error tracking service (Sentry, DataDog, or similar)

## Error tracking with Sentry

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

### Install Sentry

```bash group="sentry-install" tab="npm"
npm install @sentry/browser
```

```bash group="sentry-install" tab="yarn"
yarn add @sentry/browser
```

```bash group="sentry-install" tab="pnpm"
pnpm add @sentry/browser
```

### Initialize Sentry with dcupl context

```typescript filename="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](#track-data-quality-errors).

### Capture loader errors

Track data loading failures with full context.

```typescript filename="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.

```typescript filename="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:

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

See the [DcuplAppLoader API reference](/sdk/api/loader) 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](/sdk/advanced/quality-validation#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.

```typescript filename="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

```typescript filename="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

```typescript filename="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

```typescript filename="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](/sdk/guides/error-handling#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](#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`:

```typescript filename="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

```typescript filename="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

```typescript filename="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

```typescript filename="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

```typescript filename="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.

```typescript filename="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](/sdk/guides/debugging#connect-your-app) for how the Console opens and connects your app.

## Common production problems

### Problem: data not loading

**Symptoms:** Empty results, missing models

**Debug steps:**

```typescript
// 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](#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:**

```typescript
// 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:**

```typescript
// 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:**

```typescript
// 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`):

```typescript
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](/sdk/api/loader#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

```typescript filename="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;
  }
}
```

## Next steps

- [Debugging with Console](/sdk/guides/debugging) — Live inspection with dcupl Console
- [Testing dcupl Applications](/sdk/guides/testing) — Write tests for your dcupl code
- [Common mistakes](/sdk/guides/common-mistakes) — Avoid common pitfalls
