that persists state across the lifecycle.
3. Performance Integration: Errors often correlate with performance degradation. We integrate with the Performance API to capture metrics like INP (Interaction to Next Paint) and LCP (Largest Contentful Paint) alongside error events.
4. Batching and Transport: To minimize network overhead and preserve battery life, events are batched and sent via navigator.sendBeacon or asynchronous XHR, ensuring data is sent even if the page unloads.
Implementation
1. Context Manager
The context manager maintains state that is attached to every event.
// context-manager.ts
export interface FrontendContext {
userId?: string;
sessionId: string;
environment: string;
releaseVersion: string;
featureFlags: Record<string, boolean>;
breadcrumbs: Breadcrumb[];
}
export interface Breadcrumb {
timestamp: number;
category: string;
message: string;
level: 'info' | 'warn' | 'error';
}
export class ContextManager {
private context: FrontendContext;
constructor(config: { environment: string; releaseVersion: string }) {
this.context = {
sessionId: this.generateSessionId(),
environment: config.environment,
releaseVersion: config.releaseVersion,
featureFlags: {},
breadcrumbs: [],
};
}
private generateSessionId(): string {
return crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).substring(2);
}
setUserId(userId: string | undefined): void {
this.context.userId = userId;
}
setFeatureFlag(key: string, value: boolean): void {
this.context.featureFlags[key] = value;
}
addBreadcrumb(breadcrumb: Breadcrumb): void {
// Limit breadcrumbs to prevent memory bloat
if (this.context.breadcrumbs.length > 50) {
this.context.breadcrumbs.shift();
}
this.context.breadcrumbs.push(breadcrumb);
}
getContext(): FrontendContext {
return { ...this.context };
}
}
2. Error Collector
The collector handles error serialization, source map awareness, and event dispatching.
// error-collector.ts
import { ContextManager, Breadcrumb } from './context-manager';
export interface ErrorEvent {
error: Error | string;
timestamp: number;
url: string;
context: ReturnType<ContextManager['getContext']>;
performanceSnapshot?: PerformanceSnapshot;
}
export interface PerformanceSnapshot {
lcp?: number;
fid?: number;
cls?: number;
ttfb?: number;
}
export class ErrorCollector {
private contextManager: ContextManager;
private dsn: string;
private sampleRate: number;
constructor(dsn: string, contextManager: ContextManager, sampleRate: number = 1.0) {
this.dsn = dsn;
this.contextManager = contextManager;
this.sampleRate = sampleRate;
this.initGlobalHandlers();
}
private initGlobalHandlers(): void {
window.addEventListener('error', (event: ErrorEvent) => {
this.captureError(event.error || new Error(event.message), {
source: 'window.onerror',
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
});
});
window.addEventListener('unhandledrejection', (event: PromiseRejectionEvent) => {
this.captureError(event.reason, { source: 'unhandledrejection' });
});
// Capture breadcrumbs for navigation
window.addEventListener('popstate', () => {
this.contextManager.addBreadcrumb({
timestamp: Date.now(),
category: 'navigation',
message: `Navigation to ${window.location.pathname}`,
level: 'info',
});
});
}
captureError(error: Error | string, meta?: Record<string, any>): void {
// Sampling logic to reduce volume in high-traffic apps
if (Math.random() > this.sampleRate) return;
const errorObj = error instanceof Error ? error : new Error(String(error));
// Enrich with stack trace processing (in production, source maps handle this)
const stack = errorObj.stack || 'No stack trace available';
const event: ErrorEvent = {
error: errorObj,
timestamp: Date.now(),
url: window.location.href,
context: this.contextManager.getContext(),
performanceSnapshot: this.capturePerformanceMetrics(),
};
// Add metadata
if (meta) {
event.context.breadcrumbs.push({
timestamp: Date.now(),
category: 'error_context',
message: JSON.stringify(meta),
level: 'error',
});
}
this.sendEvent(event);
}
private capturePerformanceMetrics(): PerformanceSnapshot {
const snapshot: PerformanceSnapshot = {};
const perfEntries = performance.getEntriesByType('paint');
// LCP
const lcpEntry = performance.getEntriesByType('largest-contentful-paint').pop();
if (lcpEntry) snapshot.lcp = lcpEntry.startTime;
// FID (Note: FID is deprecated in favor of INP, but retained for legacy support)
const fidEntries = performance.getEntriesByType('first-input');
if (fidEntries.length > 0) {
const entry = fidEntries[0];
snapshot.fid = entry.processingStart - entry.startTime;
}
// TTFB
const navEntry = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
if (navEntry) snapshot.ttfb = navEntry.responseStart;
return snapshot;
}
private sendEvent(event: ErrorEvent): void {
// PII Scrubbing before transmission
const payload = this.scrubPII(event);
// Use sendBeacon for reliability during page unload
const blob = new Blob([JSON.stringify(payload)], { type: 'application/json' });
if (navigator.sendBeacon) {
navigator.sendBeacon(this.dsn, blob);
} else {
// Fallback for older browsers
fetch(this.dsn, {
method: 'POST',
body: JSON.stringify(payload),
keepalive: true,
headers: { 'Content-Type': 'application/json' },
}).catch(() => {
// Silent fail; data is lost, but UX is preserved
});
}
}
private scrubPII(event: ErrorEvent): ErrorEvent {
// Implement regex or library-based scrubbing for emails, tokens, etc.
const scrubbedContext = { ...event.context };
if (scrubbedContext.userId) {
// Hash or remove PII
scrubbedContext.userId = scrubbedContext.userId.replace(/@.*/, '***@***');
}
return { ...event, context: scrubbedContext };
}
}
3. Initialization and Usage
// monitor.ts
import { ContextManager } from './context-manager';
import { ErrorCollector } from './error-collector';
export function initMonitoring(config: {
dsn: string;
environment: string;
releaseVersion: string;
sampleRate?: number;
}) {
const contextManager = new ContextManager({
environment: config.environment,
releaseVersion: config.releaseVersion,
});
const collector = new ErrorCollector(
config.dsn,
contextManager,
config.sampleRate || 1.0
);
return {
contextManager,
collector,
setUser: (userId: string | undefined) => contextManager.setUserId(userId),
track: (message: string, level: 'info' | 'warn' | 'error' = 'info') => {
contextManager.addBreadcrumb({
timestamp: Date.now(),
category: 'custom',
message,
level,
});
},
};
}
Rationale:
- TypeScript Interfaces: Ensure type safety for context and events, reducing runtime errors in the monitoring code itself.
- Sampling: The
sampleRate parameter allows teams to control data volume. In high-traffic applications, sampling at 10-20% is often sufficient for error detection while preserving quota.
sendBeacon: Ensures errors are reported even if the user navigates away immediately after the error occurs, a common scenario in single-page applications.
- Performance Snapshot: Capturing metrics alongside errors helps distinguish between functional bugs and performance-induced failures (e.g., timeouts due to slow network).
Pitfall Guide
1. Ignoring Source Maps
Mistake: Deploying minified code without uploading source maps to the monitoring backend.
Impact: Error stacks show obfuscated variable names and line numbers in the bundle, making debugging impossible.
Best Practice: Integrate source map upload into your CI/CD pipeline. Tools like Sentry, Datadog, and New Relic provide CLI utilities for this. Ensure source maps are uploaded with the correct release version to map errors to the exact code deployment.
2. Cross-Origin Script Errors (Script error)
Mistake: Loading third-party scripts without crossorigin="anonymous" and CORS headers.
Impact: The browser suppresses error details for cross-origin scripts, resulting in a generic "Script error" message with no stack trace.
Best Practice: Add crossorigin="anonymous" to all script tags loading external resources. Ensure the CDN serving the script returns Access-Control-Allow-Origin: * or the specific origin. This allows the browser to expose error details to the monitoring SDK.
3. PII Leakage in Breadcrumbs
Mistake: Capturing URLs, form inputs, or API responses in breadcrumbs without scrubbing.
Impact: Sensitive user data (emails, passwords, tokens) is transmitted to the monitoring backend, violating GDPR/CCPA and creating a security liability.
Best Practice: Implement a beforeSend hook or scrubbing function that regex-matches and redacts PII patterns. Avoid capturing full request/response bodies; capture only status codes and error messages.
4. Blocking the Main Thread
Mistake: Performing synchronous serialization or heavy computation during error capture.
Impact: The monitoring code delays the browser's response to user input, increasing INP and degrading UX.
Best Practice: Offload serialization to a Web Worker if the payload is large. Use asynchronous APIs for network transport. Ensure error handlers return immediately and do not throw secondary errors.
5. Alert Fatigue
Mistake: Configuring alerts for every error occurrence without filtering.
Impact: Teams become desensitized to alerts, missing critical regressions amidst noise from known issues or low-impact bugs.
Best Practice: Implement alerting rules based on error velocity, regression detection, and impact scope. Use error grouping to aggregate similar errors. Set thresholds for "new errors" vs. "existing errors."
6. Sampling Bias
Mistake: Applying random sampling without considering error severity or user impact.
Impact: Rare but critical errors affecting high-value users may be dropped, while common benign errors consume quota.
Best Practice: Use weighted sampling. Increase the sample rate for errors occurring in critical paths (e.g., checkout) or for authenticated users. Ensure sampling is deterministic per session so you can reconstruct the user journey.
7. Neglecting unhandledrejection
Mistake: Only listening to window.onerror and ignoring promise rejections.
Impact: Modern async code often fails via unhandled promise rejections, which do not trigger window.onerror. These errors go unreported.
Best Practice: Always register a listener for unhandledrejection. Ensure your framework (React, Vue, Angular) is configured to catch errors in async lifecycle methods and propagate them to the global handler.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Startup / MVP | SaaS with Free Tier (e.g., Sentry, LogRocket) | Rapid setup, low maintenance, sufficient features for early validation. | Low upfront; scales with usage. |
| Enterprise / Compliance | Self-Hosted Open Source or Private Cloud SaaS | Data residency requirements, strict GDPR/CCPA control, custom retention policies. | High infrastructure and maintenance cost. |
| High Performance / Niche | Custom Lightweight Collector | Strict bundle size constraints, unique data requirements, existing logging infrastructure. | High engineering cost; lower ongoing SaaS fees. |
| Regulated Industry | Dedicated Observability + Audit Trail | Need for immutable logs, role-based access, and detailed audit trails for compliance. | Premium SaaS pricing; high compliance value. |
Configuration Template
// monitoring.config.ts
export const monitoringConfig = {
dsn: process.env.NEXT_PUBLIC_MONITORING_DSN,
environment: process.env.NODE_ENV,
releaseVersion: process.env.APP_VERSION,
// Sampling: 1.0 = 100%, 0.1 = 10%
sampleRate: process.env.NODE_ENV === 'production' ? 0.2 : 1.0,
// Breadcrumbs
maxBreadcrumbs: 50,
// Performance
enablePerformance: true,
tracesSampleRate: 0.1, // For distributed tracing
// Hooks
beforeSend: (event: any) => {
// Scrub PII
if (event.request?.url) {
event.request.url = event.request.url.replace(/token=[^&]+/, 'token=***');
}
// Filter known noise
if (event.exception?.values?.[0]?.value?.includes('ResizeObserver loop')) {
return null;
}
return event;
},
};
Quick Start Guide
- Install Package: Run
npm install @codcompass/frontend-monitor (or your chosen SDK).
- Initialize Client:
import { initMonitoring } from '@codcompass/frontend-monitor';
const monitor = initMonitoring({
dsn: 'https://your-dsn@monitoring.io/123',
environment: 'production',
releaseVersion: '1.0.0',
});
- Set User Context: Call
monitor.setUser(user.id) after authentication to associate errors with users.
- Add Breadcrumbs: Use
monitor.track('User clicked checkout') at key interaction points.
- Verify: Trigger a test error (
throw new Error('Test')) and confirm the event appears in your dashboard with full context.