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/browseryarn add @sentry/browserpnpm add @sentry/browserInitialize Sentry with dcupl context
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.
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.
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/OSee 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
$DcuplErrorTrackingErrorsmodel; query it like any other model. At mostmaxErrorsPerGrouprecords (default 1000) are stored per group —truncated: truein the summary means the records are a sample, whiletotalCountkeeps counting. Failed type coercions are consolidated into oneWrongDataTyperecord per attribute; when more values of one attribute fail thanmaxErrorsPerGroup, those counts are not exact (see Limiting Stored Errors). - The
$DcuplErrorTrackingErrorsmodel is rebuilt byinit()and, after a partial update, whenever an error record changed, so it andsummary()are current afterdcupl.update(). - A partial update leaves the same error records and
summary()counts as a freshinit()on the same data: the changed rows are re-validated (uniqueincluded), the errors of removed rows are gone, and a value of the wrong type indata.update()is recorded asWrongDataTypeand dropped. The item then has no value for that attribute; its previous value is not kept. One exception: a keyless row in adata.upsert()that a partial update applies is currently dropped without aMissingKeyrecord;init()and a full rebuild record it.
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
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
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
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 theloggerfrom@dcupl/common. Calllogger.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 failedinit()orupdate(). Capture console errors to keep them, as thecaptureConsoleIntegrationin 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:
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
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
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
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
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.
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()throwsInvalid Loader Configuration, logged as[AppLoader] Could not fetch loader configuration. Are you sure your projectId + apiKey are correct?: a wrongprojectIdorapiKey, aversionthat doesn't exist, or a network error. With a non-dcuplbaseUrlthe log line ends after "configuration".- A model or data file is missing:
[AppLoader] Failed to load data resourcewith{ url, model }, or[AppLoader] Failed to load model resourcewith{ url }, and anInvalidResourceerror insummary()withtype: 'loader'. The file doesn't exist at that URL, or it was refused (statusinfailedItems, e.g.403on a private project when the key is missing or doesn't cover the path). - A model has no items although
init()wastrue: if the log shows[ModelParser] Model not found during data processing, data was staged withdata.set()for a model key that isn't registered, or a data resource'smodelisn't declared while missing-model handling is off (missingModelHandling.autoGeneratePropertiesisfalseor not set inside amissingModelHandlingobject). With the default options there is no log line: the loader creates a model under the data resource'smodelkey and loads the rows there, so checkdcupl.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
skippedItemsfor ano-tags/tag-mismatchreason 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. CheckunresolvedVariableson 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.versionnames a different version than the one you published to (the default isdraft)- 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.versionto 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
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;
}
}