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.
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.
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.
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.
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.
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.
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
- • Error detection latency (are you catching issues quickly?)
- • Cost trends (are you within budget?)
- • Signal-to-noise ratio (are you seeing important errors?)