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,
beforeSend(event) {
// Filter out non-critical dcupl errors if needed
if (event.exception?.values?.[0]?.type === 'DcuplValidationError') {
event.level = 'warning';
}
return event;
},
});
}
export function setDcuplContext(dcupl: Dcupl) {
// Add dcupl state as context for all errors
Sentry.setContext('dcupl', {
models: dcupl.models.keys(),
listsCount: dcupl.lists.getAll().length,
});
}Capture Loader Errors
Track data loading failures with full context.
import * as Sentry from '@sentry/browser';
import type { DcuplAppLoader } from '@dcupl/loader';
import type { AppLoaderProgress } from '@dcupl/loader';
export function trackLoaderErrors(loader: DcuplAppLoader) {
loader.on((progress: AppLoaderProgress.ProgressItem) => {
// Track failed resources
const failedResources = progress.resources.filter((r) => r.status === 'error');
for (const resource of failedResources) {
Sentry.captureException(new Error(`Failed to load resource: ${resource.url}`), {
tags: {
resourceType: resource.type,
model: resource.model || 'unknown',
},
extra: {
url: resource.url,
httpStatus: resource.httpStatus,
resourceKey: resource.key,
},
});
}
// Alert on high failure rate
const failureRate = failedResources.length / progress.resources.length;
if (failureRate > 0.1) {
Sentry.captureMessage('High resource failure rate detected', {
level: 'error',
extra: {
failureRate: `${(failureRate * 100).toFixed(1)}%`,
failedCount: failedResources.length,
totalCount: progress.resources.length,
},
});
}
});
}Track Quality Controller Errors
Monitor data validation errors that occur during initialization.
import * as Sentry from '@sentry/browser';
import type { Dcupl } from '@dcupl/core';
export function trackQualityErrors(dcupl: Dcupl) {
// Enable error tracking in dcupl
// Note: errorTracking must be enabled in init options
// After initialization, check for quality errors
const errors = dcupl.quality.getErrors();
if (errors.length > 0) {
// Group errors by type
const errorsByType = errors.reduce(
(acc, error) => {
const type = error.errorType || 'Unknown';
acc[type] = (acc[type] || 0) + 1;
return acc;
},
{} as Record<string, number>
);
Sentry.captureMessage('dcupl quality errors detected', {
level: 'warning',
extra: {
totalErrors: errors.length,
errorsByType,
sampleErrors: errors.slice(0, 5).map((e) => ({
type: e.errorType,
model: e.model,
attribute: e.attribute,
description: e.description,
})),
},
});
}
}Performance Monitoring
Track initialization time, query performance, and resource loading.
Measure Initialization Time
import type { Dcupl } from '@dcupl/core';
import type { DcuplAppLoader } from '@dcupl/loader';
interface PerformanceMetrics {
configFetch: number;
resourceLoading: number;
initialization: number;
total: number;
}
export async function measureStartupPerformance(
dcupl: Dcupl,
loader: DcuplAppLoader
): Promise<PerformanceMetrics> {
const metrics: PerformanceMetrics = {
configFetch: 0,
resourceLoading: 0,
initialization: 0,
total: 0,
};
const totalStart = performance.now();
// Measure config fetch
const configStart = performance.now();
await loader.config.fetch();
metrics.configFetch = performance.now() - configStart;
// Measure resource loading
const loadStart = performance.now();
await loader.process();
metrics.resourceLoading = performance.now() - loadStart;
// Measure initialization
const initStart = performance.now();
await dcupl.init();
metrics.initialization = performance.now() - initStart;
metrics.total = performance.now() - totalStart;
return metrics;
}Track Query Performance
import type { DcuplList } from '@dcupl/core';
interface QueryMetric {
operation: string;
duration: number;
resultCount: number;
modelKey: string;
}
const queryMetrics: QueryMetric[] = [];
export function trackQueryPerformance<T>(
list: DcuplList,
operation: string,
fn: () => T
): T {
const start = performance.now();
const result = fn();
const duration = performance.now() - start;
const metric: QueryMetric = {
operation,
duration,
resultCount: Array.isArray(result) ? result.length : 1,
modelKey: list.catalog.fn.metadata().key,
};
queryMetrics.push(metric);
// Alert on slow queries
if (duration > 100) {
console.warn(`Slow query detected: ${operation} took ${duration.toFixed(2)}ms`);
}
return result;
}
// Usage example
function getFilteredProducts(list: DcuplList, category: string) {
return trackQueryPerformance(list, 'filterByCategory', () => {
list.catalog.query.apply({
groupKey: 'category',
attribute: 'category',
operator: 'eq',
value: category,
});
return list.catalog.query.items();
});
}
export function getQueryMetrics(): QueryMetric[] {
return [...queryMetrics];
}
export function getSlowQueries(thresholdMs = 50): QueryMetric[] {
return queryMetrics.filter((m) => m.duration > thresholdMs);
}Resource Loading Metrics
import type { DcuplAppLoader, AppLoaderProgress } from '@dcupl/loader';
interface ResourceMetrics {
totalResources: number;
loadedResources: number;
failedResources: number;
totalSize: number;
loadTime: number;
byType: Record<string, { count: number; size: number }>;
}
export function collectResourceMetrics(
loader: DcuplAppLoader
): Promise<ResourceMetrics> {
return new Promise((resolve) => {
let metrics: ResourceMetrics = {
totalResources: 0,
loadedResources: 0,
failedResources: 0,
totalSize: 0,
loadTime: 0,
byType: {},
};
const startTime = performance.now();
const unsubscribe = loader.on((progress: AppLoaderProgress.ProgressItem) => {
if (progress.status === 'complete' || progress.status === 'error') {
metrics.totalResources = progress.resources.length;
metrics.loadedResources = progress.resources.filter(
(r) => r.status === 'success'
).length;
metrics.failedResources = progress.resources.filter(
(r) => r.status === 'error'
).length;
metrics.loadTime = performance.now() - startTime;
// Group by type
for (const resource of progress.resources) {
const type = resource.type || 'unknown';
if (!metrics.byType[type]) {
metrics.byType[type] = { count: 0, size: 0 };
}
metrics.byType[type].count++;
}
unsubscribe();
resolve(metrics);
}
});
});
}Logging Best Practices
Implement structured logging for production debugging.
Structured Logger
type LogLevel = 'debug' | 'info' | 'warn' | 'error';
interface LogEntry {
timestamp: string;
level: LogLevel;
message: string;
context?: Record<string, unknown>;
error?: Error;
}
class DcuplLogger {
private logs: LogEntry[] = [];
private maxLogs = 1000;
private log(level: LogLevel, message: string, context?: Record<string, unknown>, error?: Error) {
const entry: LogEntry = {
timestamp: new Date().toISOString(),
level,
message,
context,
error,
};
this.logs.push(entry);
// Keep log buffer bounded
if (this.logs.length > this.maxLogs) {
this.logs.shift();
}
// Console output in development
if (import.meta.env.DEV) {
const logFn = level === 'error' ? console.error : level === 'warn' ? console.warn : console.log;
logFn(`[dcupl:${level}]`, message, context || '', error || '');
}
// Send critical logs to monitoring service in production
if (level === 'error' && import.meta.env.PROD) {
this.sendToMonitoring(entry);
}
}
debug(message: string, context?: Record<string, unknown>) {
this.log('debug', message, context);
}
info(message: string, context?: Record<string, unknown>) {
this.log('info', message, context);
}
warn(message: string, context?: Record<string, unknown>) {
this.log('warn', message, context);
}
error(message: string, error?: Error, context?: Record<string, unknown>) {
this.log('error', message, context, error);
}
getLogs(level?: LogLevel): LogEntry[] {
if (level) {
return this.logs.filter((l) => l.level === level);
}
return [...this.logs];
}
private sendToMonitoring(entry: LogEntry) {
// Send to your monitoring service
// Example: Sentry, DataDog, CloudWatch, etc.
}
}
export const logger = new DcuplLogger();Log dcupl Lifecycle Events
import { Dcupl } from '@dcupl/core';
import { DcuplAppLoader } from '@dcupl/loader';
import { logger } from './monitoring/logger';
const dcupl = new Dcupl();
const loader = new DcuplAppLoader();
// Track loader progress
loader.on((progress) => {
logger.info('Loader progress', {
status: progress.status,
loaded: progress.loaded,
total: progress.total,
percentage: `${((progress.loaded / progress.total) * 100).toFixed(1)}%`,
});
// Log individual resource failures
const failures = progress.resources.filter((r) => r.status === 'error');
for (const failure of failures) {
logger.error('Resource load failed', undefined, {
url: failure.url,
type: failure.type,
httpStatus: failure.httpStatus,
});
}
});
async function initialize() {
try {
logger.info('Starting dcupl initialization');
await loader.config.fetch();
logger.info('Config fetched', {
resourceCount: loader.resources.getAll().length,
});
await loader.process();
logger.info('Resources processed');
await dcupl.init();
logger.info('dcupl initialized', {
models: dcupl.models.keys(),
});
} catch (error) {
logger.error('Initialization failed', error as Error);
throw error;
}
}Data Integrity Alerts
Monitor data quality and alert on integrity issues.
Data Validation Service
import type { Dcupl } from '@dcupl/core';
import { logger } from './logger';
interface ValidationRule {
modelKey: string;
check: (dcupl: Dcupl) => ValidationResult;
}
interface ValidationResult {
passed: boolean;
message: string;
details?: Record<string, unknown>;
}
const validationRules: ValidationRule[] = [];
export function addValidationRule(rule: ValidationRule) {
validationRules.push(rule);
}
export function runValidations(dcupl: Dcupl): ValidationResult[] {
const results: ValidationResult[] = [];
for (const rule of validationRules) {
try {
const result = rule.check(dcupl);
results.push(result);
if (!result.passed) {
logger.warn('Validation failed', {
model: rule.modelKey,
message: result.message,
details: result.details,
});
}
} catch (error) {
results.push({
passed: false,
message: `Validation error: ${(error as Error).message}`,
});
}
}
return results;
}
// Example validation rules
addValidationRule({
modelKey: 'Product',
check: (dcupl) => {
const list = dcupl.lists.create({ modelKey: 'Product' });
const count = list.catalog.query.count();
list.destroy();
if (count === 0) {
return {
passed: false,
message: 'No products loaded',
details: { expectedMinimum: 1, actual: 0 },
};
}
return { passed: true, message: 'Products loaded successfully' };
},
});
addValidationRule({
modelKey: 'Product',
check: (dcupl) => {
const list = dcupl.lists.create({ modelKey: 'Product' });
// Check for products without prices
list.catalog.query.apply({
attribute: 'price',
operator: 'isNull',
value: true,
});
const missingPrices = list.catalog.query.count();
list.destroy();
if (missingPrices > 0) {
return {
passed: false,
message: 'Products with missing prices detected',
details: { count: missingPrices },
};
}
return { passed: true, message: 'All products have prices' };
},
});Scheduled Health Checks
import type { Dcupl } from '@dcupl/core';
import { runValidations } from './data-validation';
import { logger } from './logger';
interface HealthStatus {
status: 'healthy' | 'degraded' | 'unhealthy';
checks: {
name: string;
passed: boolean;
message: string;
}[];
timestamp: string;
}
export function checkHealth(dcupl: Dcupl): HealthStatus {
const checks: HealthStatus['checks'] = [];
// Check if dcupl is initialized
try {
const modelCount = dcupl.models.keys().length;
checks.push({
name: 'initialization',
passed: modelCount > 0,
message: modelCount > 0 ? `${modelCount} models loaded` : 'No models loaded',
});
} catch (error) {
checks.push({
name: 'initialization',
passed: false,
message: (error as Error).message,
});
}
// Run data validations
const validationResults = runValidations(dcupl);
for (const result of validationResults) {
checks.push({
name: 'data-validation',
passed: result.passed,
message: result.message,
});
}
// Check quality errors
try {
const errors = dcupl.quality.getErrors();
const criticalErrors = errors.filter(
(e) => e.errorType === 'MissingModel' || e.errorType === 'InvalidResource'
);
checks.push({
name: 'quality',
passed: criticalErrors.length === 0,
message:
criticalErrors.length === 0
? 'No critical quality errors'
: `${criticalErrors.length} critical errors`,
});
} catch {
// Quality may not be enabled
checks.push({
name: 'quality',
passed: true,
message: 'Quality tracking not enabled',
});
}
// Determine overall status
const failedChecks = checks.filter((c) => !c.passed);
let status: HealthStatus['status'] = 'healthy';
if (failedChecks.length > 0) {
status = failedChecks.some((c) => c.name === 'initialization') ? 'unhealthy' : 'degraded';
}
const healthStatus: HealthStatus = {
status,
checks,
timestamp: new Date().toISOString(),
};
// Log health status
if (status !== 'healthy') {
logger.warn('Health check failed', { status: healthStatus });
}
return healthStatus;
}Debugging Production Issues
Techniques for diagnosing problems in production.
Export Debug Information
import type { Dcupl } from '@dcupl/core';
import { logger } from './logger';
interface DebugSnapshot {
timestamp: string;
models: {
key: string;
itemCount: number;
properties: string[];
}[];
lists: {
modelKey: string;
currentSize: number;
initialSize: number;
}[];
qualityErrors: {
type: string;
model: string;
count: number;
}[];
recentLogs: unknown[];
}
export function createDebugSnapshot(dcupl: Dcupl): DebugSnapshot {
const snapshot: DebugSnapshot = {
timestamp: new Date().toISOString(),
models: [],
lists: [],
qualityErrors: [],
recentLogs: logger.getLogs().slice(-100),
};
// Collect model info
for (const modelKey of dcupl.models.keys()) {
const model = dcupl.models.get(modelKey);
if (model) {
const list = dcupl.lists.create({ modelKey });
snapshot.models.push({
key: modelKey,
itemCount: list.catalog.query.count(),
properties: model.properties?.map((p) => p.key) || [],
});
list.destroy();
}
}
// Collect list info
for (const list of dcupl.lists.getAll()) {
const meta = list.catalog.fn.metadata();
snapshot.lists.push({
modelKey: meta.key,
currentSize: meta.currentSize,
initialSize: meta.initialSize,
});
}
// Collect quality errors grouped by type and model
try {
const errors = dcupl.quality.getErrors();
const errorMap = new Map<string, number>();
for (const error of errors) {
const key = `${error.errorType}:${error.model}`;
errorMap.set(key, (errorMap.get(key) || 0) + 1);
}
for (const [key, count] of errorMap) {
const [type, model] = key.split(':');
snapshot.qualityErrors.push({ type, model, count });
}
} catch {
// Quality may not be enabled
}
return snapshot;
}
// Expose for browser console debugging
if (typeof window !== 'undefined') {
(window as any).__dcuplDebug = {
createSnapshot: createDebugSnapshot,
getLogs: () => logger.getLogs(),
};
}Remote Debugging with Console
Enable dcupl Console connection only when needed.
import type { Dcupl } from '@dcupl/core';
import { DcuplConnect } from '@dcupl/connect';
let connectInstance: DcuplConnect | null = null;
export function enableRemoteDebug(dcupl: Dcupl) {
if (connectInstance) {
console.warn('Remote debug already enabled');
return;
}
connectInstance = new DcuplConnect({ dcuplInstance: dcupl });
connectInstance.init();
console.log('Remote debug enabled - connect via dcupl Console');
}
export function disableRemoteDebug() {
if (connectInstance) {
connectInstance = null;
console.log('Remote debug disabled');
}
}
// Expose to browser console for on-demand debugging
if (typeof window !== 'undefined') {
(window as any).__dcuplEnableDebug = enableRemoteDebug;
(window as any).__dcuplDisableDebug = disableRemoteDebug;
}Common Production Problems
Problem: Data Not Loading
Symptoms: Empty results, missing models
Debug steps:
// 1. Check loader progress
loader.on((progress) => {
console.log('Resources:', progress.resources);
const failed = progress.resources.filter((r) => r.status === 'error');
console.log('Failed:', failed);
});
// 2. Check registered models
console.log('Models:', dcupl.models.keys());
// 3. Check quality errors
console.log('Errors:', dcupl.quality.getErrors());Common causes:
- Network errors (CORS, DNS, SSL)
- Invalid API key or project ID
- CDN version mismatch
- Malformed model/data files
Problem: Slow Initialization
Symptoms: Long startup time, timeout errors
Debug steps:
// Measure each phase
const metrics = await measureStartupPerformance(dcupl, loader);
console.log('Config fetch:', metrics.configFetch, 'ms');
console.log('Resource loading:', metrics.resourceLoading, 'ms');
console.log('Initialization:', metrics.initialization, 'ms');Common causes:
- Too many resources loaded at once
- Large data files without compression
- Slow CDN response times
- Complex model relationships
Solutions:
- Use resource tags to load only needed data
- Enable gzip/brotli compression
- Use a CDN closer to users
- Consider lazy loading for large datasets
Problem: Memory Issues
Symptoms: Browser crashes, slow performance over time
Debug steps:
// Check list count
console.log('Active lists:', dcupl.lists.getAll().length);
// Check data size per model
for (const modelKey of dcupl.models.keys()) {
const list = dcupl.lists.create({ modelKey });
console.log(`${modelKey}: ${list.catalog.query.count()} items`);
list.destroy();
}Common causes:
- Lists not destroyed after use
- Loading more data than needed
- Unused models consuming memory
Solutions:
- Always call
list.destroy()when done - Use resource tags to limit loaded data
- Implement pagination for large result sets
Problem: Stale Data
Symptoms: Old data displayed, updates not reflected
Debug steps:
// Check CDN version
console.log('Variables:', loader.variables.getAll());
// Force cache bypass
await loader.config.fetch({ skipApply: false });
await loader.process({
environmentKeys: ['production'],
});
await dcupl.init();Common causes:
- Browser cache serving old files
- CDN cache not invalidated
- Missing version query parameter
Solutions:
- Ensure CDN version is updated on publish
- Use cache-busting query parameters
- Configure appropriate cache headers
Complete Monitoring Setup
import { Dcupl } from '@dcupl/core';
import { DcuplAppLoader } from '@dcupl/loader';
import { initSentry, setDcuplContext } from './sentry';
import { trackLoaderErrors } from './loader-tracking';
import { trackQualityErrors } from './quality-tracking';
import { measureStartupPerformance } from './performance';
import { checkHealth } from './health-check';
import { logger } from './logger';
export async function initializeWithMonitoring(config: {
projectId: string;
apiKey: string;
sentryDsn?: string;
}) {
// Initialize Sentry
if (config.sentryDsn) {
initSentry(config.sentryDsn);
}
const dcupl = new Dcupl();
const loader = new DcuplAppLoader();
dcupl.addRunner(loader);
// Set up loader variables
loader.variables.global.set('projectId', config.projectId);
loader.variables.global.set('apiKey', config.apiKey);
// Track loader errors
trackLoaderErrors(loader);
try {
// Measure and log performance
const metrics = await measureStartupPerformance(dcupl, loader);
logger.info('Startup complete', {
configFetch: `${metrics.configFetch.toFixed(0)}ms`,
resourceLoading: `${metrics.resourceLoading.toFixed(0)}ms`,
initialization: `${metrics.initialization.toFixed(0)}ms`,
total: `${metrics.total.toFixed(0)}ms`,
});
// Set Sentry context
setDcuplContext(dcupl);
// Track quality errors
trackQualityErrors(dcupl);
// Run health check
const health = checkHealth(dcupl);
if (health.status !== 'healthy') {
logger.warn('Application started in degraded state', { health });
}
return { dcupl, loader, health };
} catch (error) {
logger.error('Initialization failed', error as Error);
throw error;
}
}Related
- Debugging with Console - Live inspection with dcupl Console
- Testing dcupl Applications - Write tests for your dcupl code
- Common Mistakes - Avoid common pitfalls