Bugsnag provides powerful error monitoring and reporting for Java applications. It helps you detect, diagnose, and resolve exceptions and errors in production environments.
Dependencies and Setup
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 Integration --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>${spring-boot.version}</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> <version>${spring-boot.version}</version> </dependency> <!-- For web applications --> <dependency> <groupId>com.bugsnag</groupId> <artifactId>bugsnag-spring</artifactId> <version>${bugsnag.version}</version> </dependency> </dependencies> 2. Application Properties
# application.yml bugsnag: api-key: ${BUGSNAG_API_KEY:your-api-key-here} enabled: true release-stage: ${APP_ENV:production} app-version: 1.0.0 notify-release-stages: production,staging project-packages: com.example,org.example send-uncaught-exceptions: true filters: password,secret,token,authorization management: endpoints: web: exposure: include: health,metrics,info endpoint: health: show-details: always Basic Configuration
1. Bugsnag Configuration Class
package com.example.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; import org.springframework.context.annotation.Import; @Configuration @Import(BugsnagSpringConfiguration.class) public class BugsnagConfig { @Value("${bugsnag.api-key}") private String bugsnagApiKey; @Value("${bugsnag.release-stage:production}") private String releaseStage; @Value("${bugsnag.app-version:1.0.0}") private String appVersion; @Value("${bugsnag.enabled:true}") private boolean enabled; @Bean public Bugsnag bugsnag() { Bugsnag bugsnag = new Bugsnag(bugsnagApiKey); // Configure Bugsnag bugsnag.setReleaseStage(releaseStage); bugsnag.setAppVersion(appVersion); bugsnag.setNotifyReleaseStages("production", "staging"); bugsnag.setProjectPackages("com.example"); bugsnag.setSendUncaughtExceptions(true); // Add filters for sensitive data bugsnag.addCallback(report -> { // Filter sensitive data from metadata report.getMetaData().addToTab("user", "password", "[FILTERED]"); report.getMetaData().addToTab("request", "authorization", "[FILTERED]"); report.getMetaData().addToTab("request", "cookie", "[FILTERED]"); }); return bugsnag; } } 2. Enhanced Configuration with Custom Callbacks
package com.example.config; import com.bugsnag.Bugsnag; import com.bugsnag.Report; import com.bugsnag.callbacks.Callback; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.env.Environment; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; import java.util.HashMap; import java.util.Map; @Component public class BugsnagInitializer { @Autowired private Bugsnag bugsnag; @Autowired private Environment environment; @PostConstruct public void initialize() { // Add global metadata bugsnag.addCallback(new Callback() { @Override public void beforeNotify(Report report) { // Add application metadata report.addToTab("App", "Environment", environment.getActiveProfiles()); report.addToTab("App", "Spring Version", getSpringVersion()); report.addToTab("App", "Java Version", System.getProperty("java.version")); // Add system information report.addToTab("System", "Available Processors", Runtime.getRuntime().availableProcessors()); report.addToTab("System", "Free Memory", Runtime.getRuntime().freeMemory()); report.addToTab("System", "Max Memory", Runtime.getRuntime().maxMemory()); report.addToTab("System", "Total Memory", Runtime.getRuntime().totalMemory()); // Add thread information report.addToTab("Thread", "Name", Thread.currentThread().getName()); report.addToTab("Thread", "State", Thread.currentThread().getState().toString()); // Filter sensitive headers filterSensitiveData(report); } }); // Add callback for grouping errors bugsnag.addCallback(report -> { String errorClass = report.getException().getClass().getSimpleName(); String message = report.getException().getMessage(); // Custom grouping for specific error types if (errorClass.equals("DatabaseConnectionException")) { report.setGroupingHash("database-connection-issues"); } else if (message != null && message.contains("timeout")) { report.setGroupingHash("timeout-errors"); } }); } private String getSpringVersion() { try { return org.springframework.core.SpringVersion.getVersion(); } catch (Exception e) { return "unknown"; } } private void filterSensitiveData(Report report) { // Filter sensitive data from request metadata if (report.getMetaData().getTab("request") != null) { Map<String, Object> requestData = report.getMetaData().getTab("request"); filterMap(requestData); } // Filter sensitive data from environment metadata if (report.getMetaData().getTab("environment") != null) { Map<String, Object> envData = report.getMetaData().getTab("environment"); filterMap(envData); } } private void filterMap(Map<String, Object> data) { String[] sensitiveKeys = {"password", "secret", "token", "authorization", "cookie", "key"}; for (String sensitiveKey : sensitiveKeys) { if (data.containsKey(sensitiveKey)) { data.put(sensitiveKey, "[FILTERED]"); } } } } Spring Boot Integration
1. Service Layer with Error Reporting
package com.example.service; import com.bugsnag.Bugsnag; import com.bugsnag.Report; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.HashMap; import java.util.Map; @Service public class UserService { private static final Logger logger = LoggerFactory.getLogger(UserService.class); @Autowired private Bugsnag bugsnag; @Autowired private UserRepository userRepository; public User getUserById(String userId) { try { logger.info("Fetching user with ID: {}", userId); User user = userRepository.findById(userId) .orElseThrow(() -> new UserNotFoundException("User not found: " + userId)); logger.debug("Successfully fetched user: {}", user.getEmail()); return user; } catch (UserNotFoundException e) { // This is an expected error - log but don't report to Bugsnag logger.warn("User not found: {}", userId); throw e; } catch (Exception e) { // Unexpected error - report to Bugsnag reportErrorToBugsnag(e, Map.of( "user_id", userId, "operation", "get_user", "service", "UserService" )); throw new ServiceException("Failed to fetch user", e); } } public User createUser(CreateUserRequest request) { long startTime = System.currentTimeMillis(); try { validateUserRequest(request); User user = new User(); user.setEmail(request.getEmail()); user.setName(request.getName()); user.setActive(true); User savedUser = userRepository.save(user); long duration = System.currentTimeMillis() - startTime; logger.info("User created successfully: {} (took {}ms)", savedUser.getId(), duration); // Record business metric bugsnag.leaveBreadcrumb("User Created", Map.of("user_id", savedUser.getId(), "duration_ms", duration), "process"); return savedUser; } catch (ValidationException e) { // Expected validation error logger.warn("Validation failed for user creation: {}", e.getMessage()); throw e; } catch (Exception e) { // Unexpected error - report with detailed context Map<String, Object> metadata = new HashMap<>(); metadata.put("user_email", request.getEmail()); metadata.put("user_name", request.getName()); metadata.put("operation", "create_user"); metadata.put("duration_ms", System.currentTimeMillis() - startTime); reportErrorToBugsnag(e, metadata); throw new ServiceException("Failed to create user", e); } } public void updateUserProfile(String userId, UpdateProfileRequest request) { try { User user = getUserById(userId); // Add breadcrumb for tracking bugsnag.leaveBreadcrumb("Profile Update Started", Map.of("user_id", userId, "fields_updated", request.getUpdatedFields()), "process"); if (request.getEmail() != null) { user.setEmail(request.getEmail()); } if (request.getName() != null) { user.setName(request.getName()); } userRepository.save(user); bugsnag.leaveBreadcrumb("Profile Update Completed", Map.of("user_id", userId), "process"); } catch (Exception e) { reportErrorToBugsnag(e, Map.of( "user_id", userId, "operation", "update_profile", "request_data", request.toString() )); throw e; } } private void reportErrorToBugsnag(Exception error, Map<String, Object> metadata) { try { Report report = bugsnag.buildReport(error); // Add custom metadata if (metadata != null) { metadata.forEach((key, value) -> report.addToTab("Custom", key, value)); } // Set severity based on error type if (error instanceof DatabaseException) { report.setSeverity("error"); } else if (error instanceof ExternalServiceException) { report.setSeverity("warning"); } else { report.setSeverity("error"); } bugsnag.notify(report); logger.error("Error reported to Bugsnag: {}", error.getMessage(), error); } catch (Exception bugsnagError) { logger.error("Failed to report error to Bugsnag: {}", bugsnagError.getMessage()); // Don't throw - we don't want Bugsnag failures to break the application } } private void validateUserRequest(CreateUserRequest request) { if (request.getEmail() == null || request.getEmail().trim().isEmpty()) { throw new ValidationException("Email is required"); } if (!isValidEmail(request.getEmail())) { throw new ValidationException("Invalid email format"); } } private boolean isValidEmail(String email) { return email.matches("^[A-Za-z0-9+_.-]+@(.+)$"); } } 2. Custom Exceptions
package com.example.exception; public class UserNotFoundException extends RuntimeException { public UserNotFoundException(String message) { super(message); } } public class ValidationException extends RuntimeException { public ValidationException(String message) { super(message); } } public class ServiceException extends RuntimeException { public ServiceException(String message) { super(message); } public ServiceException(String message, Throwable cause) { super(message, cause); } } public class DatabaseException extends RuntimeException { public DatabaseException(String message, Throwable cause) { super(message, cause); } } public class ExternalServiceException extends RuntimeException { public ExternalServiceException(String message, Throwable cause) { super(message, cause); } } 3. REST Controller with Error Handling
package com.example.controller; import com.bugsnag.Bugsnag; import com.example.model.User; import com.example.service.UserService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import javax.servlet.http.HttpServletRequest; import java.util.HashMap; import java.util.Map; @RestController @RequestMapping("/api/users") public class UserController { private static final Logger logger = LoggerFactory.getLogger(UserController.class); @Autowired private UserService userService; @Autowired private Bugsnag bugsnag; @GetMapping("/{userId}") public ResponseEntity<?> getUser(@PathVariable String userId, HttpServletRequest request) { try { // Add breadcrumb for request tracking bugsnag.leaveBreadcrumb("API Request", Map.of( "method", "GET", "path", "/api/users/" + userId, "user_id", userId, "client_ip", getClientIp(request) ), "request"); User user = userService.getUserById(userId); return ResponseEntity.ok(user); } catch (UserNotFoundException e) { return ResponseEntity.status(HttpStatus.NOT_FOUND) .body(createErrorResponse("USER_NOT_FOUND", e.getMessage())); } catch (Exception e) { logger.error("Failed to fetch user: {}", userId, e); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body(createErrorResponse("INTERNAL_ERROR", "Failed to fetch user")); } } @PostMapping public ResponseEntity<?> createUser(@RequestBody CreateUserRequest request, HttpServletRequest httpRequest) { long startTime = System.currentTimeMillis(); try { // Add request context to Bugsnag bugsnag.leaveBreadcrumb("Create User API", Map.of( "method", "POST", "path", "/api/users", "user_email", request.getEmail(), "client_ip", getClientIp(httpRequest) ), "request"); User user = userService.createUser(request); long duration = System.currentTimeMillis() - startTime; logger.info("User creation API completed in {}ms", duration); return ResponseEntity.status(HttpStatus.CREATED).body(user); } catch (ValidationException e) { return ResponseEntity.status(HttpStatus.BAD_REQUEST) .body(createErrorResponse("VALIDATION_ERROR", e.getMessage())); } catch (Exception e) { logger.error("Failed to create user", e); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body(createErrorResponse("INTERNAL_ERROR", "Failed to create user")); } } @PutMapping("/{userId}/profile") public ResponseEntity<?> updateProfile(@PathVariable String userId, @RequestBody UpdateProfileRequest request, HttpServletRequest httpRequest) { try { bugsnag.leaveBreadcrumb("Update Profile API", Map.of( "method", "PUT", "path", "/api/users/" + userId + "/profile", "user_id", userId, "client_ip", getClientIp(httpRequest) ), "request"); userService.updateUserProfile(userId, request); return ResponseEntity.ok().build(); } catch (UserNotFoundException e) { return ResponseEntity.status(HttpStatus.NOT_FOUND) .body(createErrorResponse("USER_NOT_FOUND", e.getMessage())); } catch (Exception e) { logger.error("Failed to update user profile: {}", userId, e); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body(createErrorResponse("INTERNAL_ERROR", "Failed to update profile")); } } private Map<String, Object> createErrorResponse(String code, String message) { Map<String, Object> errorResponse = new HashMap<>(); errorResponse.put("error", code); errorResponse.put("message", message); errorResponse.put("timestamp", System.currentTimeMillis()); return errorResponse; } private String getClientIp(HttpServletRequest request) { String xForwardedFor = request.getHeader("X-Forwarded-For"); if (xForwardedFor != null && !xForwardedFor.isEmpty()) { return xForwardedFor.split(",")[0].trim(); } return request.getRemoteAddr(); } } 4. Global Exception Handler
package com.example.config; import com.bugsnag.Bugsnag; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; 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 static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class); @Autowired private Bugsnag bugsnag; @ExceptionHandler(Exception.class) public ResponseEntity<Map<String, Object>> handleAllExceptions(Exception ex, WebRequest request) { // Report unexpected exceptions to Bugsnag if (!isExpectedException(ex)) { reportUnexpectedException(ex, request); } Map<String, Object> errorResponse = new HashMap<>(); errorResponse.put("error", "INTERNAL_SERVER_ERROR"); errorResponse.put("message", "An unexpected error occurred"); errorResponse.put("timestamp", System.currentTimeMillis()); errorResponse.put("path", request.getDescription(false)); logger.error("Unhandled exception occurred", ex); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse); } @ExceptionHandler(UserNotFoundException.class) public ResponseEntity<Map<String, Object>> handleUserNotFound(UserNotFoundException ex, WebRequest request) { Map<String, Object> errorResponse = new HashMap<>(); errorResponse.put("error", "USER_NOT_FOUND"); errorResponse.put("message", ex.getMessage()); errorResponse.put("timestamp", System.currentTimeMillis()); return ResponseEntity.status(HttpStatus.NOT_FOUND).body(errorResponse); } @ExceptionHandler(ValidationException.class) public ResponseEntity<Map<String, Object>> handleValidationException(ValidationException ex, WebRequest request) { Map<String, Object> errorResponse = new HashMap<>(); errorResponse.put("error", "VALIDATION_ERROR"); errorResponse.put("message", ex.getMessage()); errorResponse.put("timestamp", System.currentTimeMillis()); return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse); } private boolean isExpectedException(Exception ex) { return ex instanceof UserNotFoundException || ex instanceof ValidationException; } private void reportUnexpectedException(Exception ex, WebRequest request) { try { Map<String, Object> metadata = new HashMap<>(); metadata.put("path", request.getDescription(false)); metadata.put("parameters", request.getParameterMap()); metadata.put("user_agent", request.getHeader("User-Agent")); bugsnag.notify(ex, (report) -> { report.setSeverity("error"); metadata.forEach((key, value) -> report.addToTab("Request", key, value)); }); } catch (Exception bugsnagEx) { logger.error("Failed to report exception to Bugsnag", bugsnagEx); } } } Advanced Features
1. Custom Breadcrumb Service
package com.example.service; import com.bugsnag.Bugsnag; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.Map; @Service public class BreadcrumbService { @Autowired private Bugsnag bugsnag; public void leaveBreadcrumb(String message, String type) { bugsnag.leaveBreadcrumb(message, type); } public void leaveBreadcrumb(String message, Map<String, Object> metadata, String type) { bugsnag.leaveBreadcrumb(message, metadata, type); } public void recordUserAction(String userId, String action, Map<String, Object> details) { Map<String, Object> metadata = new HashMap<>(); metadata.put("user_id", userId); metadata.put("action", action); if (details != null) { metadata.putAll(details); } leaveBreadcrumb("User Action: " + action, metadata, "user"); } public void recordSystemEvent(String event, Map<String, Object> details) { Map<String, Object> metadata = new HashMap<>(); metadata.put("event", event); if (details != null) { metadata.putAll(details); } leaveBreadcrumb("System Event: " + event, metadata, "system"); } public void recordExternalCall(String service, String operation, long duration, boolean success) { Map<String, Object> metadata = new HashMap<>(); metadata.put("service", service); metadata.put("operation", operation); metadata.put("duration_ms", duration); metadata.put("success", success); leaveBreadcrumb("External Call: " + service + "." + operation, metadata, "network"); } } 2. Performance Monitoring Integration
package com.example.aspect; import com.bugsnag.Bugsnag; 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.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.util.HashMap; import java.util.Map; @Aspect @Component public class PerformanceMonitoringAspect { private static final Logger logger = LoggerFactory.getLogger(PerformanceMonitoringAspect.class); @Autowired private Bugsnag bugsnag; @Around("@annotation(MonitorPerformance)") public Object monitorPerformance(ProceedingJoinPoint joinPoint) throws Throwable { String methodName = joinPoint.getSignature().toShortString(); long startTime = System.currentTimeMillis(); try { Object result = joinPoint.proceed(); long duration = System.currentTimeMillis() - startTime; // Record performance breadcrumb if (duration > 1000) { // Log slow methods Map<String, Object> metadata = new HashMap<>(); metadata.put("method", methodName); metadata.put("duration_ms", duration); metadata.put("threshold_ms", 1000); bugsnag.leaveBreadcrumb("Slow Method Execution", metadata, "performance"); logger.warn("Slow method detected: {} took {}ms", methodName, duration); } return result; } catch (Exception e) { long duration = System.currentTimeMillis() - startTime; // Record error with performance context Map<String, Object> metadata = new HashMap<>(); metadata.put("method", methodName); metadata.put("duration_ms", duration); metadata.put("error", e.getClass().getSimpleName()); bugsnag.leaveBreadcrumb("Method Execution Failed", metadata, "error"); throw e; } } } 3. Database Monitoring
package com.example.repository; import com.bugsnag.Bugsnag; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataAccessException; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Repository; import java.util.List; import java.util.Map; @Repository public class UserRepository { private static final Logger logger = LoggerFactory.getLogger(UserRepository.class); @Autowired private JdbcTemplate jdbcTemplate; @Autowired private Bugsnag bugsnag; public Optional<User> findById(String userId) { long startTime = System.currentTimeMillis(); try { String sql = "SELECT * FROM users WHERE id = ? AND deleted = false"; List<User> users = jdbcTemplate.query(sql, new Object[]{userId}, new UserRowMapper()); long duration = System.currentTimeMillis() - startTime; // Record slow queries if (duration > 500) { Map<String, Object> metadata = new HashMap<>(); metadata.put("query", "findById"); metadata.put("user_id", userId); metadata.put("duration_ms", duration); metadata.put("result_count", users.size()); bugsnag.leaveBreadcrumb("Slow Database Query", metadata, "database"); } return users.isEmpty() ? Optional.empty() : Optional.of(users.get(0)); } catch (DataAccessException e) { long duration = System.currentTimeMillis() - startTime; // Report database errors to Bugsnag Map<String, Object> metadata = new HashMap<>(); metadata.put("query", "findById"); metadata.put("user_id", userId); metadata.put("duration_ms", duration); metadata.put("error_type", "DataAccessException"); bugsnag.notify(e, report -> { report.setSeverity("error"); metadata.forEach((key, value) -> report.addToTab("Database", key, value)); }); throw new DatabaseException("Database error while fetching user", e); } } public List<User> findByEmail(String email) { // Similar implementation with monitoring return jdbcTemplate.query( "SELECT * FROM users WHERE email = ? AND deleted = false", new Object[]{email}, new UserRowMapper() ); } public User save(User user) { long startTime = System.currentTimeMillis(); try { if (user.getId() == null) { // Insert new user String sql = "INSERT INTO users (id, email, name, active, created_at) VALUES (?, ?, ?, ?, ?)"; jdbcTemplate.update(sql, user.getId(), user.getEmail(), user.getName(), user.isActive(), user.getCreatedAt()); } else { // Update existing user String sql = "UPDATE users SET email = ?, name = ?, active = ?, updated_at = ? WHERE id = ?"; jdbcTemplate.update(sql, user.getEmail(), user.getName(), user.isActive(), user.getUpdatedAt(), user.getId()); } long duration = System.currentTimeMillis() - startTime; if (duration > 1000) { Map<String, Object> metadata = new HashMap<>(); metadata.put("operation", user.getId() == null ? "insert" : "update"); metadata.put("user_id", user.getId()); metadata.put("duration_ms", duration); bugsnag.leaveBreadcrumb("Slow Database Operation", metadata, "database"); } return user; } catch (DataAccessException e) { long duration = System.currentTimeMillis() - startTime; Map<String, Object> metadata = new HashMap<>(); metadata.put("operation", "save"); metadata.put("user_id", user.getId()); metadata.put("user_email", user.getEmail()); metadata.put("duration_ms", duration); bugsnag.notify(e, report -> { report.setSeverity("error"); metadata.forEach((key, value) -> report.addToTab("Database", key, value)); }); throw new DatabaseException("Database error while saving user", e); } } } 4. Scheduled Health Monitoring
package com.example.scheduler; import com.bugsnag.Bugsnag; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import java.util.HashMap; import java.util.Map; @Component public class HealthMonitorScheduler { private static final Logger logger = LoggerFactory.getLogger(HealthMonitorScheduler.class); @Autowired private Bugsnag bugsnag; @Autowired private UserRepository userRepository; @Scheduled(fixedRate = 300000) // Run every 5 minutes public void checkDatabaseHealth() { long startTime = System.currentTimeMillis(); try { // Simple health check query userRepository.countActiveUsers(); long duration = System.currentTimeMillis() - startTime; if (duration > 1000) { Map<String, Object> metadata = new HashMap<>(); metadata.put("check", "database_health"); metadata.put("duration_ms", duration); metadata.put("status", "slow"); bugsnag.leaveBreadcrumb("Slow Health Check", metadata, "monitoring"); } logger.debug("Database health check completed in {}ms", duration); } catch (Exception e) { Map<String, Object> metadata = new HashMap<>(); metadata.put("check", "database_health"); metadata.put("duration_ms", System.currentTimeMillis() - startTime); metadata.put("status", "failed"); bugsnag.notify(e, report -> { report.setSeverity("warning"); report.setContext("Health Check"); metadata.forEach((key, value) -> report.addToTab("Health", key, value)); }); logger.error("Database health check failed", e); } } @Scheduled(cron = "0 0 * * * *") // Run every hour public void reportApplicationMetrics() { try { Runtime runtime = Runtime.getRuntime(); Map<String, Object> metadata = new HashMap<>(); metadata.put("free_memory", runtime.freeMemory()); metadata.put("total_memory", runtime.totalMemory()); metadata.put("max_memory", runtime.maxMemory()); metadata.put("available_processors", runtime.availableProcessors()); bugsnag.leaveBreadcrumb("Application Metrics", metadata, "system"); logger.info("Application metrics recorded: freeMemory={}, totalMemory={}", runtime.freeMemory(), runtime.totalMemory()); } catch (Exception e) { logger.error("Failed to record application metrics", e); } } } Testing
1. Test Configuration
package com.example.config; import com.bugsnag.Bugsnag; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Primary; import static org.mockito.Mockito.mock; @TestConfiguration public class TestBugsnagConfig { @Bean @Primary public Bugsnag bugsnag() { return mock(Bugsnag.class); } } 2. Service Test with Mocked Bugsnag
package com.example.service; import com.bugsnag.Bugsnag; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) class UserServiceTest { @Mock private Bugsnag bugsnag; @Mock private UserRepository userRepository; private UserService userService; @BeforeEach void setUp() { userService = new UserService(); // Use reflection to set mocks or create a package-private setter } @Test void shouldReportUnexpectedErrorToBugsnag() { // Given CreateUserRequest request = new CreateUserRequest(); request.setEmail("[email protected]"); request.setName("Test User"); when(userRepository.save(any(User.class))).thenThrow(new RuntimeException("Database error")); // When & Then assertThrows(ServiceException.class, () -> userService.createUser(request)); // Verify Bugsnag was called verify(bugsnag, times(1)).notify(any(Report.class)); } @Test void shouldNotReportExpectedErrorsToBugsnag() { // Given CreateUserRequest request = new CreateUserRequest(); request.setEmail("invalid-email"); request.setName("Test User"); // When & Then assertThrows(ValidationException.class, () -> userService.createUser(request)); // Verify Bugsnag was NOT called for expected errors verify(bugsnag, never()).notify(any(Report.class)); } } Best Practices
- Error Filtering:
- Don't report expected business errors
- Filter sensitive data from reports
- Use appropriate severity levels
- Breadcrumb Strategy:
- Leave breadcrumbs for important user actions
- Track external service calls
- Monitor performance bottlenecks
- Context Management:
- Add relevant context to error reports
- Include user information when available
- Track request IDs for correlation
- Performance:
- Use async reporting when possible
- Don't block application flow for error reporting
- Monitor Bugsnag's impact on application performance
// Example of async error reporting public void reportErrorAsync(Exception error, Map<String, Object> metadata) { CompletableFuture.runAsync(() -> { try { bugsnag.notify(error, report -> { metadata.forEach((key, value) -> report.addToTab("Async", key, value)); }); } catch (Exception e) { logger.error("Failed to report error async", e); } }); } Conclusion
Bugsnag Java SDK provides:
- Comprehensive error monitoring and reporting
- Rich context for debugging production issues
- Breadcrumb tracking for understanding error paths
- Custom metadata for business-specific debugging
- Integration with Spring Boot and other frameworks
By implementing the patterns shown above, you can achieve comprehensive error monitoring, track user journeys, identify performance issues, and quickly diagnose and fix problems in production environments. The combination of automatic exception capture and manual instrumentation gives you complete visibility into your application's health and user experience.