Bugsnag Java SDK: Comprehensive Error Monitoring and Reporting

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

  1. Context is Key: Always provide meaningful context with errors
  2. Proper Severity: Use appropriate severity levels (ERROR, WARNING, INFO)
  3. Breadcrumbs: Leave breadcrumbs to trace event flow
  4. User Sessions: Track user sessions for better error correlation
  5. Avoid Duplicates: Use grouping hashes to prevent duplicate errors
  6. 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.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper