tryError Documentation

Error Sampling Guide

Implement intelligent error sampling to control costs and reduce noise in production while maintaining visibility into critical issues

tryError's onError hook provides the perfect integration point for implementing custom sampling strategies. This guide shows various sampling approaches you can use based on your needs.

Basic Random Sampling

The simplest approach: randomly sample a percentage of errors.

Random Sampling with Critical Error Exceptionstypescript
1import { configure } from '@try-error/core';
2import * as Sentry from '@sentry/nextjs';
3
4// Sample 10% of non-critical errors, 100% of critical errors
5const SAMPLE_RATE = 0.1;
6
7configure({
8  onError: (error) => {
9    // Always log critical errors (100% sampling)
10    if (error.type === 'PaymentError' || error.type === 'SecurityError') {
11      Sentry.captureException(error);
12      return error;
13    }
14
15    // Random sampling for other errors (10% sampling)
16    if (Math.random() < SAMPLE_RATE) {
17      Sentry.captureException(error, {
18        tags: {
19          sampled: true,
20          sampleRate: SAMPLE_RATE,
21        },
22      });
23    }
24
25    return error;
26  },
27});

When to use random sampling

  • • You have uniform error distribution
  • • You want a simple, predictable cost model
  • • You don't need to catch every unique error

Rate-Based Sampling

Limit the number of errors sent per time window to control costs during error spikes.

Rate Limiter Implementationtypescript
1class ErrorRateLimiter {
2  private counts = new Map<string, number>();
3  private windowStart = Date.now();
4  
5  constructor(
6    private maxPerMinute: number = 100,
7    private windowMs: number = 60000 // 1 minute
8  ) {}
9
10  shouldSample(errorType: string): boolean {
11    const now = Date.now();
12    
13    // Reset window if expired
14    if (now - this.windowStart > this.windowMs) {
15      this.counts.clear();
16      this.windowStart = now;
17    }
18    
19    const count = this.counts.get(errorType) || 0;
20    
21    if (count >= this.maxPerMinute) {
22      return false;
23    }
24    
25    this.counts.set(errorType, count + 1);
26    return true;
27  }
28  
29  getRemainingQuota(errorType: string): number {
30    const count = this.counts.get(errorType) || 0;
31    return Math.max(0, this.maxPerMinute - count);
32  }
33}
34
35// Usage with tryError
36const rateLimiter = new ErrorRateLimiter(100); // 100 errors per minute
37
38configure({
39  onError: (error) => {
40    if (rateLimiter.shouldSample(error.type)) {
41      Sentry.captureException(error, {
42        tags: {
43          sampled: true,
44          samplingMethod: 'rate-limit',
45          remainingQuota: rateLimiter.getRemainingQuota(error.type),
46        },
47      });
48    } else {
49      // Still log locally or to a cheaper service
50      console.warn(`Rate limited: ${error.type}`, {
51        message: error.message,
52        timestamp: error.timestamp,
53      });
54    }
55    
56    return error;
57  },
58});

Intelligent Sampling Strategies

More sophisticated approaches that ensure you capture important errors while reducing noise.

Error Fingerprinting
Sample unique errors more aggressively than duplicates
Fingerprint-based Samplingtypescript
1class ErrorFingerprinter {
2  private seenErrors = new Map<string, {
3    count: number;
4    firstSeen: number;
5    lastSeen: number;
6  }>();
7  
8  private generateFingerprint(error: TryError): string {
9    // Create a unique fingerprint based on error characteristics
10    return `${error.type}:${error.message}:${error.source}`;
11  }
12  
13  shouldSample(error: TryError): boolean {
14    const fingerprint = this.generateFingerprint(error);
15    const seen = this.seenErrors.get(fingerprint);
16    
17    if (!seen) {
18      // Always sample first occurrence
19      this.seenErrors.set(fingerprint, {
20        count: 1,
21        firstSeen: Date.now(),
22        lastSeen: Date.now(),
23      });
24      return true;
25    }
26    
27    // Update stats
28    seen.count++;
29    seen.lastSeen = Date.now();
30    
31    // Sample logic: decreasing probability for repeated errors
32    // 100% for first, 50% for second, 10% for third, 1% after that
33    const sampleRates = [1, 0.5, 0.1, 0.01];
34    const rate = sampleRates[Math.min(seen.count - 1, sampleRates.length - 1)];
35    
36    return Math.random() < rate;
37  }
38  
39  getErrorStats(error: TryError) {
40    const fingerprint = this.generateFingerprint(error);
41    return this.seenErrors.get(fingerprint);
42  }
43}
44
45const fingerprinter = new ErrorFingerprinter();
46
47configure({
48  onError: (error) => {
49    const shouldSample = fingerprinter.shouldSample(error);
50    const stats = fingerprinter.getErrorStats(error);
51    
52    if (shouldSample) {
53      Sentry.captureException(error, {
54        tags: {
55          sampled: true,
56          errorCount: stats?.count || 1,
57          samplingMethod: 'fingerprint',
58        },
59        extra: {
60          firstSeen: stats?.firstSeen,
61          lastSeen: stats?.lastSeen,
62        },
63      });
64    }
65    
66    return error;
67  },
68});

User-Based Sampling

Sample all errors from a subset of users for complete user journey visibility.

User-Based Samplingtypescript
1class UserSampler {
2  private sampledUsers = new Set<string>();
3  private userErrorCounts = new Map<string, number>();
4  
5  constructor(
6    private sampleRate: number = 0.01, // 1% of users
7    private maxErrorsPerUser: number = 100
8  ) {}
9  
10  private hashUserId(userId: string): number {
11    // Simple hash function for consistent sampling
12    let hash = 0;
13    for (let i = 0; i < userId.length; i++) {
14      hash = ((hash << 5) - hash) + userId.charCodeAt(i);
15      hash = hash & hash; // Convert to 32-bit integer
16    }
17    return Math.abs(hash);
18  }
19  
20  isUserSampled(userId: string): boolean {
21    if (this.sampledUsers.has(userId)) {
22      return true;
23    }
24    
25    // Consistent sampling based on user ID hash
26    const hash = this.hashUserId(userId);
27    const threshold = this.sampleRate * 0x7FFFFFFF; // Max 32-bit int
28    
29    if (hash < threshold) {
30      this.sampledUsers.add(userId);
31      return true;
32    }
33    
34    return false;
35  }
36  
37  shouldSample(error: TryError): boolean {
38    const userId = error.context?.userId as string;
39    
40    if (!userId) {
41      // Sample anonymous errors at a lower rate
42      return Math.random() < this.sampleRate * 0.1;
43    }
44    
45    if (!this.isUserSampled(userId)) {
46      return false;
47    }
48    
49    // Check per-user rate limit
50    const count = this.userErrorCounts.get(userId) || 0;
51    if (count >= this.maxErrorsPerUser) {
52      return false;
53    }
54    
55    this.userErrorCounts.set(userId, count + 1);
56    return true;
57  }
58}
59
60const userSampler = new UserSampler(0.01); // Sample 1% of users
61
62configure({
63  onError: (error) => {
64    if (userSampler.shouldSample(error)) {
65      const userId = error.context?.userId as string;
66      
67      Sentry.captureException(error, {
68        user: userId ? { id: userId } : undefined,
69        tags: {
70          sampled: true,
71          samplingMethod: 'user-based',
72          userSampled: userSampler.isUserSampled(userId),
73        },
74      });
75    }
76    
77    return error;
78  },
79});

Benefits of user-based sampling

  • • Complete error journey for sampled users
  • • Better debugging with full context
  • • Consistent user experience monitoring
  • • Useful for A/B testing error rates

Complete Production Example

A comprehensive sampling system combining multiple strategies for production use.

lib/error-sampling.tstypescript
1import { configure, TryError } from '@try-error/core';
2import * as Sentry from '@sentry/nextjs';
3import { track } from '@vercel/analytics';
4
5// Comprehensive sampling system
6class ProductionErrorSampler {
7  private fingerprinter = new ErrorFingerprinter();
8  private rateLimiter = new ErrorRateLimiter(1000); // 1000/min
9  private userSampler = new UserSampler(0.01); // 1% of users
10  private adaptiveSampler = new AdaptiveSampler(5000); // 5000/min target
11  
12  shouldSample(error: TryError): {
13    sample: boolean;
14    reason: string;
15    metadata: Record<string, any>;
16  } {
17    // 1. Always sample critical errors
18    const criticalTypes = ['PaymentError', 'SecurityError', 'DataLossError'];
19    if (criticalTypes.includes(error.type)) {
20      return {
21        sample: true,
22        reason: 'critical-error',
23        metadata: { priority: 'critical' },
24      };
25    }
26    
27    // 2. Check rate limits first (cheapest check)
28    if (!this.rateLimiter.shouldSample(error.type)) {
29      return {
30        sample: false,
31        reason: 'rate-limited',
32        metadata: {
33          remainingQuota: this.rateLimiter.getRemainingQuota(error.type),
34        },
35      };
36    }
37    
38    // 3. User-based sampling for complete journeys
39    const userId = error.context?.userId as string;
40    if (userId && this.userSampler.isUserSampled(userId)) {
41      return {
42        sample: true,
43        reason: 'user-sampled',
44        metadata: { userId, userSampled: true },
45      };
46    }
47    
48    // 4. Fingerprint-based for unique errors
49    const isFirstOccurrence = !this.fingerprinter.getErrorStats(error);
50    if (isFirstOccurrence || this.fingerprinter.shouldSample(error)) {
51      return {
52        sample: true,
53        reason: 'fingerprint-sampled',
54        metadata: {
55          errorStats: this.fingerprinter.getErrorStats(error),
56          isFirstOccurrence,
57        },
58      };
59    }
60    
61    // 5. Adaptive sampling for everything else
62    if (this.adaptiveSampler.shouldSample(error)) {
63      return {
64        sample: true,
65        reason: 'adaptive-sampled',
66        metadata: {
67          sampleRate: this.adaptiveSampler.getSampleRate(error.type),
68        },
69      };
70    }
71    
72    return {
73      sample: false,
74      reason: 'not-sampled',
75      metadata: {},
76    };
77  }
78}
79
80// Initialize the sampler
81const sampler = new ProductionErrorSampler();
82
83// Configure tryError with comprehensive sampling
84export function setupErrorSampling() {
85  configure({
86    onError: (error) => {
87      const { sample, reason, metadata } = sampler.shouldSample(error);
88      
89      // Track sampling metrics in analytics
90      track('error_sampling', {
91        errorType: error.type,
92        sampled: sample,
93        reason,
94        ...metadata,
95      });
96      
97      if (sample) {
98        // Send to Sentry with sampling context
99        Sentry.captureException(error, {
100          tags: {
101            sampled: true,
102            samplingReason: reason,
103            errorType: error.type,
104          },
105          extra: {
106            samplingMetadata: metadata,
107            errorContext: error.context,
108          },
109        });
110      } else {
111        // Still log locally for debugging
112        if (process.env.NODE_ENV === 'development') {
113          console.warn(`Error not sampled (${reason}):`, error);
114        }
115        
116        // Consider sending to a cheaper logging service
117        // or aggregate locally and batch send
118        logToLocalStorage(error, reason);
119      }
120      
121      return error;
122    },
123  });
124}
125
126// Local storage for non-sampled errors (with cleanup)
127function logToLocalStorage(error: TryError, reason: string) {
128  if (typeof window === 'undefined') return;
129  
130  try {
131    const key = 'tryError-unsampled';
132    const existing = JSON.parse(localStorage.getItem(key) || '[]');
133    
134    existing.push({
135      type: error.type,
136      message: error.message,
137      timestamp: error.timestamp,
138      reason,
139      // Don't store sensitive context
140    });
141    
142    // Keep only last 100 unsampled errors
143    if (existing.length > 100) {
144      existing.splice(0, existing.length - 100);
145    }
146    
147    localStorage.setItem(key, JSON.stringify(existing));
148  } catch {
149    // Ignore localStorage errors
150  }
151}

Monitoring Your Sampling

Track your sampling effectiveness to ensure you're not missing important errors.

Sampling Metrics Dashboardtypescript
1// Track sampling metrics
2class SamplingMetrics {
3  private metrics = {
4    total: 0,
5    sampled: 0,
6    byType: new Map<string, { total: number; sampled: number }>(),
7    byReason: new Map<string, number>(),
8  };
9  
10  record(error: TryError, sampled: boolean, reason: string) {
11    this.metrics.total++;
12    if (sampled) this.metrics.sampled++;
13    
14    // By type
15    const typeMetrics = this.metrics.byType.get(error.type) || { total: 0, sampled: 0 };
16    typeMetrics.total++;
17    if (sampled) typeMetrics.sampled++;
18    this.metrics.byType.set(error.type, typeMetrics);
19    
20    // By reason
21    this.metrics.byReason.set(reason, (this.metrics.byReason.get(reason) || 0) + 1);
22  }
23  
24  getReport() {
25    const overallRate = this.metrics.sampled / this.metrics.total;
26    
27    const byTypeRates = Array.from(this.metrics.byType.entries()).map(([type, metrics]) => ({
28      type,
29      rate: metrics.sampled / metrics.total,
30      total: metrics.total,
31      sampled: metrics.sampled,
32    }));
33    
34    return {
35      overall: {
36        rate: overallRate,
37        total: this.metrics.total,
38        sampled: this.metrics.sampled,
39      },
40      byType: byTypeRates.sort((a, b) => b.total - a.total),
41      byReason: Object.fromEntries(this.metrics.byReason),
42    };
43  }
44}
45
46// Send metrics to monitoring dashboard
47setInterval(() => {
48  const report = samplingMetrics.getReport();
49  
50  // Send to your metrics service
51  track('sampling_metrics', report);
52  
53  // Or log for analysis
54  console.log('Sampling Report:', report);
55}, 60000); // Every minute