Bugsnag provides powerful error monitoring and reporting capabilities for Java applications, helping you detect, diagnose, and resolve errors in real-time.
Understanding Bugsnag
What is Bugsnag?
- Error monitoring and reporting platform
- Real-time error detection and alerts
- Rich error context and diagnostics
- Support for multiple environments and releases
Key Features:
- Automatic Error Detection: Captures unhandled exceptions
- Custom Error Reporting: Manual error reporting with context
- Release Tracking: Tracks errors by application version
- User Sessions: Associates errors with user sessions
- Breadcrumbs: Trail of events leading to errors
Setup and Dependencies
1. Maven Dependencies
<properties> <bugsnag.version>3.6.4</bugsnag.version> <spring-boot.version>3.1.0</spring-boot.version> </properties> <dependencies> <!-- Bugsnag Core --> <dependency> <groupId>com.bugsnag</groupId> <artifactId>bugsnag</artifactId> <version>${bugsnag.version}</version> </dependency> <!-- Spring Boot Starter (Optional) --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>${spring-boot.version}</version> </dependency> <!-- For JSON processing --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.15.2</version> </dependency> </dependencies> 2. Gradle Dependencies
dependencies { implementation 'com.bugsnag:bugsnag:3.6.4' implementation 'org.springframework.boot:spring-boot-starter-web:3.1.0' } Basic Configuration and Setup
1. Environment Configuration
# application.properties bugsnag.api-key=your-bugsnag-api-key-here bugsnag.release-stage=production bugsnag.app-version=1.0.0 bugsnag.enabled=true # Optional configuration bugsnag.endpoint=https://notify.bugsnag.com bugsnag.session-endpoint=https://sessions.bugsnag.com bugsnag.send-uncaught-exceptions=true bugsnag.send-threads=true
2. Basic Bugsnag Setup
package com.yourapp.config; import com.bugsnag.Bugsnag; import com.bugsnag.BugsnagSpringConfiguration; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class BugsnagConfig { @Value("${bugsnag.api-key:}") private String bugsnagApiKey; @Value("${bugsnag.release-stage:development}") private String releaseStage; @Value("${bugsnag.app-version:1.0.0}") private String appVersion; @Value("${bugsnag.enabled:false}") private boolean enabled; @Bean public Bugsnag bugsnag() { Bugsnag bugsnag = new Bugsnag(bugsnagApiKey); // Basic configuration bugsnag.setReleaseStage(releaseStage); bugsnag.setAppVersion(appVersion); bugsnag.setNotifyReleaseStages("production", "staging"); bugsnag.setSendUncaughtExceptions(true); bugsnag.setSendThreads(true); // Set project packages for better grouping bugsnag.setProjectPackages("com.yourapp"); return bugsnag; } @Bean public BugsnagSpringConfiguration bugsnagSpringConfiguration(Bugsnag bugsnag) { return new BugsnagSpringConfiguration(bugsnag); } } 3. Advanced Configuration
@Configuration public class AdvancedBugsnagConfig { @Bean public Bugsnag bugsnag(@Value("${bugsnag.api-key}") String apiKey) { Bugsnag bugsnag = new Bugsnag(apiKey); // Configure release stage bugsnag.setReleaseStage(getReleaseStage()); // Set application version bugsnag.setAppVersion(getAppVersion()); // Only notify in production and staging bugsnag.setNotifyReleaseStages("production", "staging"); // Configure project packages for better error grouping bugsnag.setProjectPackages( "com.yourapp.controllers", "com.yourapp.services", "com.yourapp.repositories" ); // Add global metadata bugsnag.addCallback(report -> { report.addToTab("App", "Environment", getReleaseStage()); report.addToTab("App", "Deployment", System.getenv("DEPLOYMENT_ID")); report.addToTab("App", "Server", System.getenv("HOSTNAME")); }); return bugsnag; } private String getReleaseStage() { return System.getenv().getOrDefault("APP_ENV", System.getProperty("spring.profiles.active", "development")); } private String getAppVersion() { return System.getenv().getOrDefault("APP_VERSION", getClass().getPackage().getImplementationVersion()); } } Core Implementation
1. Bugsnag Service Wrapper
package com.yourapp.service; import com.bugsnag.Bugsnag; import com.bugsnag.Report; import com.bugsnag.Severity; import org.springframework.stereotype.Service; import java.util.HashMap; import java.util.Map; @Service public class BugsnagService { private final Bugsnag bugsnag; public BugsnagService(Bugsnag bugsnag) { this.bugsnag = bugsnag; } // Basic error reporting public void reportError(Throwable throwable) { bugsnag.notify(throwable); } public void reportError(Throwable throwable, Severity severity) { bugsnag.notify(throwable, severity); } // Error reporting with context public void reportError(Throwable throwable, String context) { bugsnag.notify(throwable, report -> { report.setContext(context); }); } // Error reporting with custom metadata public void reportError(Throwable throwable, String context, Map<String, Object> metadata) { bugsnag.notify(throwable, report -> { report.setContext(context); metadata.forEach((key, value) -> report.addToTab("Custom", key, value)); }); } // Report error with user information public void reportError(Throwable throwable, String context, String userId, String userEmail, String userName) { bugsnag.notify(throwable, report -> { report.setContext(context); report.setUser(userId, userEmail, userName); }); } // Report handled exception with full context public void reportHandledError(String errorName, String message, Map<String, Object> metadata, Severity severity) { Exception exception = new Exception(message); bugsnag.notify(exception, report -> { report.setContext(errorName); report.setSeverity(severity); report.setGroupingHash(errorName); if (metadata != null) { metadata.forEach((key, value) -> report.addToTab("Custom", key, value)); } }); } // Leave breadcrumb for event tracking public void leaveBreadcrumb(String message, Map<String, Object> metadata) { bugsnag.leaveBreadcrumb(message, metadata); } // Start a session for user activity tracking public void startSession() { bugsnag.startSession(); } // Report custom event (non-error) public void reportEvent(String name, Map<String, Object> data) { bugsnag.notify(new Exception("Custom Event: " + name), report -> { report.setSeverity(Severity.INFO); report.setUnhandled(false); report.setGroupingHash("custom_event"); if (data != null) { data.forEach((key, value) -> report.addToTab("Event Data", key, value)); } }); } // Add global metadata to all reports public void addGlobalMetadata(String tab, String key, Object value) { bugsnag.addCallback(report -> report.addToTab(tab, key, value)); } } 2. Error Context Manager
package com.yourapp.service; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest; import java.util.HashMap; import java.util.Map; @Component public class ErrorContextManager { private final ThreadLocal<Map<String, Object>> context = new ThreadLocal<>(); public void initializeContext() { context.set(new HashMap<>()); } public void setContext(String key, Object value) { Map<String, Object> ctx = context.get(); if (ctx != null) { ctx.put(key, value); } } public Map<String, Object> getContext() { return context.get(); } public void clearContext() { context.remove(); } public Map<String, Object> buildErrorContext(Throwable error) { Map<String, Object> errorContext = new HashMap<>(); // Add thread-local context Map<String, Object> threadContext = getContext(); if (threadContext != null) { errorContext.putAll(threadContext); } // Add HTTP request context addHttpRequestContext(errorContext); // Add system context addSystemContext(errorContext); // Add error-specific context addErrorContext(errorContext, error); return errorContext; } private void addHttpRequestContext(Map<String, Object> context) { try { ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); if (attributes != null) { HttpServletRequest request = attributes.getRequest(); Map<String, Object> httpContext = new HashMap<>(); httpContext.put("method", request.getMethod()); httpContext.put("url", request.getRequestURL().toString()); httpContext.put("query", request.getQueryString()); httpContext.put("userAgent", request.getHeader("User-Agent")); httpContext.put("referer", request.getHeader("Referer")); httpContext.put("clientIp", getClientIpAddress(request)); context.put("http", httpContext); } } catch (Exception e) { // Ignore errors in context building } } private void addSystemContext(Map<String, Object> context) { Map<String, Object> systemContext = new HashMap<>(); systemContext.put("javaVersion", System.getProperty("java.version")); systemContext.put("os", System.getProperty("os.name")); systemContext.put("availableProcessors", Runtime.getRuntime().availableProcessors()); systemContext.put("freeMemory", Runtime.getRuntime().freeMemory()); systemContext.put("totalMemory", Runtime.getRuntime().totalMemory()); context.put("system", systemContext); } private void addErrorContext(Map<String, Object> context, Throwable error) { Map<String, Object> errorContext = new HashMap<>(); errorContext.put("type", error.getClass().getName()); errorContext.put("message", error.getMessage()); errorContext.put("stackTrace", getStackTrace(error)); context.put("error", errorContext); } private String getClientIpAddress(HttpServletRequest request) { String xForwardedFor = request.getHeader("X-Forwarded-For"); if (xForwardedFor != null && !xForwardedFor.isEmpty()) { return xForwardedFor.split(",")[0].trim(); } return request.getRemoteAddr(); } private String getStackTrace(Throwable error) { StringBuilder sb = new StringBuilder(); for (StackTraceElement element : error.getStackTrace()) { sb.append(element.toString()).append("\n"); } return sb.toString(); } } Spring Boot Integration
1. Global Exception Handler
package com.yourapp.handler; import com.bugsnag.Bugsnag; import com.bugsnag.Severity; import com.yourapp.service.BugsnagService; import com.yourapp.service.ErrorContextManager; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.context.request.WebRequest; import java.util.HashMap; import java.util.Map; @RestControllerAdvice public class GlobalExceptionHandler { private final BugsnagService bugsnagService; private final ErrorContextManager contextManager; public GlobalExceptionHandler(BugsnagService bugsnagService, ErrorContextManager contextManager) { this.bugsnagService = bugsnagService; this.contextManager = contextManager; } @ExceptionHandler(Exception.class) public ResponseEntity<Map<String, Object>> handleAllExceptions( Exception ex, WebRequest request) { // Build error context Map<String, Object> errorContext = contextManager.buildErrorContext(ex); // Report to Bugsnag bugsnagService.reportError(ex, "Global Exception Handler", errorContext); // Create response Map<String, Object> response = new HashMap<>(); response.put("timestamp", System.currentTimeMillis()); response.put("status", HttpStatus.INTERNAL_SERVER_ERROR.value()); response.put("error", "Internal Server Error"); response.put("message", "An unexpected error occurred"); response.put("path", request.getDescription(false)); return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR); } @ExceptionHandler(BusinessException.class) public ResponseEntity<Map<String, Object>> handleBusinessException( BusinessException ex, WebRequest request) { // Report business exception with WARNING severity Map<String, Object> errorContext = contextManager.buildErrorContext(ex); errorContext.put("businessErrorCode", ex.getErrorCode()); bugsnagService.reportError(ex, "Business Exception", ex.getUserId(), ex.getUserEmail(), ex.getUserName()); Map<String, Object> response = new HashMap<>(); response.put("timestamp", System.currentTimeMillis()); response.put("status", HttpStatus.BAD_REQUEST.value()); response.put("error", "Business Error"); response.put("message", ex.getMessage()); response.put("errorCode", ex.getErrorCode()); response.put("path", request.getDescription(false)); return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST); } @ExceptionHandler(ResourceNotFoundException.class) public ResponseEntity<Map<String, Object>> handleResourceNotFoundException( ResourceNotFoundException ex, WebRequest request) { // Report as INFO since 404s are usually not critical errors Map<String, Object> errorContext = contextManager.buildErrorContext(ex); errorContext.put("resourceType", ex.getResourceType()); errorContext.put("resourceId", ex.getResourceId()); bugsnagService.reportError(ex, "Resource Not Found", errorContext); Map<String, Object> response = new HashMap<>(); response.put("timestamp", System.currentTimeMillis()); response.put("status", HttpStatus.NOT_FOUND.value()); response.put("error", "Not Found"); response.put("message", ex.getMessage()); response.put("resourceType", ex.getResourceType()); response.put("resourceId", ex.getResourceId()); response.put("path", request.getDescription(false)); return new ResponseEntity<>(response, HttpStatus.NOT_FOUND); } } // Custom exception classes class BusinessException extends RuntimeException { private String errorCode; private String userId; private String userEmail; private String userName; public BusinessException(String message, String errorCode) { super(message); this.errorCode = errorCode; } // getters and setters } class ResourceNotFoundException extends RuntimeException { private String resourceType; private String resourceId; public ResourceNotFoundException(String resourceType, String resourceId) { super(String.format("%s with id %s not found", resourceType, resourceId)); this.resourceType = resourceType; this.resourceId = resourceId; } // getters and setters } 2. HTTP Request Filter for Context
package com.yourapp.filter; import com.yourapp.service.ErrorContextManager; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @Component public class BugsnagContextFilter extends OncePerRequestFilter { private final ErrorContextManager contextManager; public BugsnagContextFilter(ErrorContextManager contextManager) { this.contextManager = contextManager; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // Initialize context for this request contextManager.initializeContext(); try { // Add request-specific context contextManager.setContext("requestId", request.getHeader("X-Request-ID")); contextManager.setContext("sessionId", request.getSession().getId()); contextManager.setContext("userAgent", request.getHeader("User-Agent")); contextManager.setContext("clientIp", getClientIpAddress(request)); // Continue with the filter chain filterChain.doFilter(request, response); } finally { // Clear context to prevent memory leaks contextManager.clearContext(); } } private String getClientIpAddress(HttpServletRequest request) { String xForwardedFor = request.getHeader("X-Forwarded-For"); if (xForwardedFor != null && !xForwardedFor.isEmpty()) { return xForwardedFor.split(",")[0].trim(); } return request.getRemoteAddr(); } } 3. Spring AOP for Method-Level Error Handling
package com.yourapp.aspect; import com.bugsnag.Severity; import com.yourapp.service.BugsnagService; import com.yourapp.service.ErrorContextManager; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import java.util.HashMap; import java.util.Map; @Aspect @Component public class BugsnagMonitoringAspect { private static final Logger logger = LoggerFactory.getLogger(BugsnagMonitoringAspect.class); private final BugsnagService bugsnagService; private final ErrorContextManager contextManager; public BugsnagMonitoringAspect(BugsnagService bugsnagService, ErrorContextManager contextManager) { this.bugsnagService = bugsnagService; this.contextManager = contextManager; } @Around("@annotation(MonitorErrors)") public Object monitorMethodExecution(ProceedingJoinPoint joinPoint) throws Throwable { String methodName = joinPoint.getSignature().toShortString(); // Leave breadcrumb Map<String, Object> breadcrumbData = new HashMap<>(); breadcrumbData.put("method", methodName); breadcrumbData.put("args", joinPoint.getArgs()); bugsnagService.leaveBreadcrumb("Method Execution: " + methodName, breadcrumbData); try { Object result = joinPoint.proceed(); // Log successful execution logger.debug("Method executed successfully: {}", methodName); return result; } catch (Exception e) { // Report error to Bugsnag Map<String, Object> errorContext = new HashMap<>(); errorContext.put("method", methodName); errorContext.put("arguments", joinPoint.getArgs()); bugsnagService.reportError(e, "Method Execution Error", errorContext); logger.error("Method execution failed: {}", methodName, e); throw e; } } @Around("@annotation(MonitorPerformance)") public Object monitorPerformance(ProceedingJoinPoint joinPoint) throws Throwable { long startTime = System.currentTimeMillis(); String methodName = joinPoint.getSignature().toShortString(); try { Object result = joinPoint.proceed(); long executionTime = System.currentTimeMillis() - startTime; // Report slow methods if (executionTime > 1000) { // 1 second threshold Map<String, Object> metadata = new HashMap<>(); metadata.put("executionTime", executionTime); metadata.put("method", methodName); bugsnagService.reportHandledError( "Slow Method Execution", "Method took " + executionTime + "ms to execute", metadata, Severity.WARNING ); } return result; } catch (Exception e) { long executionTime = System.currentTimeMillis() - startTime; logger.error("Performance monitoring error in method: {}", methodName, e); throw e; } } } 4. Custom Annotations
package com.yourapp.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface MonitorErrors { String value() default ""; Severity severity() default Severity.ERROR; } @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface MonitorPerformance { long threshold() default 1000L; // milliseconds } Service Implementation with Bugsnag
1. User Service Example
package com.yourapp.service; import com.yourapp.annotation.MonitorErrors; import com.yourapp.annotation.MonitorPerformance; import com.yourapp.exception.BusinessException; import com.yourapp.exception.ResourceNotFoundException; import com.yourapp.model.User; import com.yourapp.repository.UserRepository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import java.util.Map; @Service public class UserService { private static final Logger logger = LoggerFactory.getLogger(UserService.class); private final UserRepository userRepository; private final BugsnagService bugsnagService; private final ErrorContextManager contextManager; public UserService(UserRepository userRepository, BugsnagService bugsnagService, ErrorContextManager contextManager) { this.userRepository = userRepository; this.bugsnagService = bugsnagService; this.contextManager = contextManager; } @MonitorErrors @MonitorPerformance(threshold = 500) public User findUserById(String userId) { // Set context for error reporting contextManager.setContext("userId", userId); contextManager.setContext("operation", "findUserById"); bugsnagService.leaveBreadcrumb("Finding user by ID", Map.of("userId", userId)); try { User user = userRepository.findById(userId) .orElseThrow(() -> new ResourceNotFoundException("User", userId)); // Leave success breadcrumb bugsnagService.leaveBreadcrumb("User found successfully", Map.of("userId", userId, "userName", user.getName())); return user; } catch (ResourceNotFoundException e) { // This is expected behavior, so we report it as info bugsnagService.reportError(e, "User Not Found", Severity.INFO); throw e; } catch (Exception e) { // Unexpected error - report as error logger.error("Unexpected error finding user: {}", userId, e); throw e; } } @MonitorErrors public User createUser(User user) { contextManager.setContext("operation", "createUser"); contextManager.setContext("userEmail", user.getEmail()); bugsnagService.leaveBreadcrumb("Creating new user", Map.of("email", user.getEmail(), "name", user.getName())); try { // Validate user data validateUser(user); User savedUser = userRepository.save(user); // Report successful user creation as event bugsnagService.reportEvent("User Created", Map.of("userId", savedUser.getId(), "email", savedUser.getEmail())); return savedUser; } catch (BusinessException e) { // Business validation error - report as warning bugsnagService.reportError(e, "User Validation Failed", Severity.WARNING); throw e; } } @MonitorErrors public void updateUserProfile(String userId, Map<String, Object> updates) { contextManager.setContext("userId", userId); contextManager.setContext("operation", "updateUserProfile"); contextManager.setContext("updates", updates); bugsnagService.leaveBreadcrumb("Updating user profile", Map.of("userId", userId, "updates", updates.keySet())); try { User user = findUserById(userId); // Apply updates updates.forEach((key, value) -> { switch (key) { case "name": user.setName((String) value); break; case "email": user.setEmail((String) value); break; default: logger.warn("Unknown update field: {}", key); } }); userRepository.save(user); bugsnagService.leaveBreadcrumb("User profile updated successfully", Map.of("userId", userId)); } catch (Exception e) { logger.error("Failed to update user profile: {}", userId, e); throw e; } } private void validateUser(User user) { if (user.getEmail() == null || user.getEmail().trim().isEmpty()) { throw new BusinessException("Email is required", "VALIDATION_ERROR"); } if (userRepository.findByEmail(user.getEmail()).isPresent()) { throw new BusinessException("Email already exists", "DUPLICATE_EMAIL"); } } // Session management public void startUserSession(String userId, String userEmail, String userName) { bugsnagService.startSession(); // Set user context for all subsequent errors contextManager.setContext("userId", userId); contextManager.setContext("userEmail", userEmail); contextManager.setContext("userName", userName); bugsnagService.leaveBreadcrumb("User session started", Map.of("userId", userId, "userEmail", userEmail)); } public void endUserSession(String userId) { bugsnagService.leaveBreadcrumb("User session ended", Map.of("userId", userId)); contextManager.setContext("userId", null); contextManager.setContext("userEmail", null); contextManager.setContext("userName", null); } } 2. REST Controller with Bugsnag Integration
package com.yourapp.controller; import com.yourapp.model.User; import com.yourapp.service.BugsnagService; import com.yourapp.service.UserService; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import javax.servlet.http.HttpServletRequest; import java.util.Map; @RestController @RequestMapping("/api/users") public class UserController { private final UserService userService; private final BugsnagService bugsnagService; public UserController(UserService userService, BugsnagService bugsnagService) { this.userService = userService; this.bugsnagService = bugsnagService; } @GetMapping("/{userId}") public ResponseEntity<User> getUser(@PathVariable String userId, HttpServletRequest request) { try { bugsnagService.leaveBreadcrumb("GET User API Called", Map.of("userId", userId, "path", request.getRequestURI())); User user = userService.findUserById(userId); return ResponseEntity.ok(user); } catch (Exception e) { // Error is already handled by GlobalExceptionHandler throw e; } } @PostMapping public ResponseEntity<User> createUser(@RequestBody User user, HttpServletRequest request) { try { bugsnagService.leaveBreadcrumb("POST User API Called", Map.of("userEmail", user.getEmail(), "path", request.getRequestURI())); User createdUser = userService.createUser(user); return ResponseEntity.status(HttpStatus.CREATED).body(createdUser); } catch (Exception e) { throw e; } } @PutMapping("/{userId}/profile") public ResponseEntity<Void> updateUserProfile(@PathVariable String userId, @RequestBody Map<String, Object> updates, HttpServletRequest request) { try { bugsnagService.leaveBreadcrumb("PUT User Profile API Called", Map.of("userId", userId, "updates", updates.keySet())); userService.updateUserProfile(userId, updates); return ResponseEntity.ok().build(); } catch (Exception e) { throw e; } } @PostMapping("/{userId}/session/start") public ResponseEntity<Void> startSession(@PathVariable String userId, @RequestParam String userEmail, @RequestParam String userName) { userService.startUserSession(userId, userEmail, userName); return ResponseEntity.ok().build(); } @PostMapping("/{userId}/session/end") public ResponseEntity<Void> endSession(@PathVariable String userId) { userService.endUserSession(userId); return ResponseEntity.ok().build(); } } Advanced Features
1. Custom Error Reporting for Async Operations
package com.yourapp.service; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import java.util.concurrent.CompletableFuture; @Service public class AsyncService { private final BugsnagService bugsnagService; public AsyncService(BugsnagService bugsnagService) { this.bugsnagService = bugsnagService; } @Async public CompletableFuture<String> processAsyncTask(String taskId) { try { bugsnagService.leaveBreadcrumb("Starting async task", Map.of("taskId", taskId)); // Simulate async work Thread.sleep(1000); if (taskId.equals("error-task")) { throw new RuntimeException("Simulated async error"); } bugsnagService.leaveBreadcrumb("Async task completed", Map.of("taskId", taskId)); return CompletableFuture.completedFuture("Task " + taskId + " completed"); } catch (Exception e) { // Report async errors with proper context bugsnagService.reportError(e, "Async Task Error", Map.of("taskId", taskId, "thread", Thread.currentThread().getName())); throw new RuntimeException("Async processing failed", e); } } } 2. Database Health Monitoring
package com.yourapp.monitoring; import com.bugsnag.Severity; import com.yourapp.service.BugsnagService; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import javax.sql.DataSource; import java.sql.Connection; import java.sql.SQLException; import java.util.Map; @Component public class DatabaseHealthMonitor { private final DataSource dataSource; private final BugsnagService bugsnagService; public DatabaseHealthMonitor(DataSource dataSource, BugsnagService bugsnagService) { this.dataSource = dataSource; this.bugsnagService = bugsnagService; } @Scheduled(fixedRate = 60000) // Check every minute public void checkDatabaseHealth() { try (Connection connection = dataSource.getConnection()) { // Test connection boolean isValid = connection.isValid(5); // 5 second timeout if (!isValid) { bugsnagService.reportHandledError( "Database Connection Issue", "Database connection validation failed", Map.of("timeout", 5), Severity.ERROR ); } } catch (SQLException e) { bugsnagService.reportError(e, "Database Health Check Failed", Map.of("checkType", "connection_validation")); } } } Testing
1. Unit Tests
package com.yourapp.service; import com.bugsnag.Bugsnag; import com.bugsnag.Report; import com.bugsnag.Severity; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import java.util.Map; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) class BugsnagServiceTest { @Mock private Bugsnag bugsnag; @Captor private ArgumentCaptor<Report> reportCaptor; private BugsnagService bugsnagService; @BeforeEach void setUp() { bugsnagService = new BugsnagService(bugsnag); } @Test void shouldReportError() { // Given Exception exception = new RuntimeException("Test error"); // When bugsnagService.reportError(exception); // Then verify(bugsnag).notify(exception); } @Test void shouldReportErrorWithContext() { // Given Exception exception = new RuntimeException("Test error"); String context = "Test Context"; Map<String, Object> metadata = Map.of("key", "value"); // When bugsnagService.reportError(exception, context, metadata); // Then verify(bugsnag).notify(eq(exception), any()); } @Test void shouldLeaveBreadcrumb() { // Given String message = "Test breadcrumb"; Map<String, Object> metadata = Map.of("key", "value"); // When bugsnagService.leaveBreadcrumb(message, metadata); // Then verify(bugsnag).leaveBreadcrumb(message, metadata); } } 2. Integration Test
package com.yourapp.integration; import com.yourapp.service.BugsnagService; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.context.TestPropertySource; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.verify; @SpringBootTest @TestPropertySource(properties = { "bugsnag.api-key=test-key", "bugsnag.enabled=true" }) class BugsnagIntegrationTest { @Autowired private BugsnagService bugsnagService; @MockBean private com.bugsnag.Bugsnag bugsnag; @Test void shouldInitializeBugsnagService() { // Given Exception testException = new RuntimeException("Integration test error"); // When bugsnagService.reportError(testException); // Then verify(bugsnag).notify(any(Exception.class)); } } Best Practices
- Context is Key: Always provide meaningful context with errors
- Proper Severity: Use appropriate severity levels (ERROR, WARNING, INFO)
- Breadcrumbs: Leave breadcrumbs to trace event flow
- User Sessions: Track user sessions for better error correlation
- Avoid Duplicates: Use grouping hashes to prevent duplicate errors
- Performance: Don't overload Bugsnag with too many non-critical errors
// Good practice - meaningful context bugsnagService.reportError(exception, "Payment Processing", Map.of("orderId", orderId, "amount", amount, "paymentMethod", method)); // Bad practice - minimal context bugsnagService.reportError(exception); Conclusion
Bugsnag Java SDK provides:
- Comprehensive error monitoring and reporting
- Rich context for faster debugging
- User session tracking for better error correlation
- Flexible integration with Spring Boot applications
- Powerful breadcrumb system for event tracing
By implementing the patterns shown above, you can significantly improve your application's observability, reduce mean time to resolution (MTTR) for errors, and gain valuable insights into your application's health and user experience.