Article
When exceptions occur in a Java web application, generic error reports often leave developers guessing: "Which user was affected?" and "What were they trying to do?" By integrating Airbrake with Spring Security's UserDetails, you can automatically enrich error reports with user context, transforming vague errors into actionable, user-aware incidents that dramatically accelerate debugging and resolution.
What is Airbrake and Why Integrate with UserDetails?
Airbrake is an error monitoring and performance tracking service that captures exceptions from applications and provides detailed diagnostic information. Spring Security's UserDetails interface represents the core user information that the security framework uses throughout the authentication process.
Integrating these two systems means that whenever an exception occurs, your Airbrake reports will automatically include:
- The authenticated user's ID and username
- Their roles and permissions
- Tenant/organization context (in multi-tenant apps)
- Any other user-specific metadata you choose to include
This context is invaluable for answering critical questions during incident investigation.
Setting Up Basic Airbrake Integration in Spring Boot
First, let's establish the basic Airbrake configuration in a Spring Boot application.
1. Add Dependencies (Maven):
<dependency> <groupId>io.airbrake</groupId> <artifactId>airbrake-java</artifactId> <version>3.0.0</version> </dependency>
2. Configure Airbrake in application.properties:
# Airbrake configuration airbrake.project-id=YOUR_PROJECT_ID airbrake.project-key=YOUR_PROJECT_KEY airbrake.environment=production
3. Create Airbrake Configuration Class:
@Configuration public class AirbrakeConfig { @Value("${airbrake.project-id}") private String projectId; @Value("${airbrake.project-key}") private String projectKey; @Value("${airbrake.environment:development}") private String environment; @Bean public Notifier airbrakeNotifier() { AirbrakeConfig config = new AirbrakeConfig(projectKey, projectId); config.setEnvironment(environment); return new AirbrakeNotifier(config); } } Implementing UserDetails Integration with Airbrake
The key to successful integration is creating a custom NoticeBuilder that extracts user information from the Spring Security context and attaches it to Airbrake reports.
1. Custom UserDetails Implementation (Enhanced):
public class CustomUserDetails implements UserDetails { private Long id; private String username; private String email; private String password; private Collection<? extends GrantedAuthority> authorities; private String tenantId; private String department; // Constructors, getters, and standard UserDetails methods @Override public Collection<? extends GrantedAuthority> getAuthorities() { return authorities; } @Override public String getPassword() { return password; } @Override public String getUsername() { return username; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } // Custom getters public Long getId() { return id; } public String getEmail() { return email; } public String getTenantId() { return tenantId; } public String getDepartment() { return department; } } 2. Custom Airbrake Notice Builder with UserDetails Support:
@Component public class UserAwareNoticeBuilder { private final Notifier airbrakeNotifier; public UserAwareNoticeBuilder(Notifier airbrakeNotifier) { this.airbrakeNotifier = airbrakeNotifier; } public void notify(Throwable throwable, HttpServletRequest request) { Notice notice = buildNotice(throwable, request); airbrakeNotifier.notify(notice); } private Notice buildNotice(Throwable throwable, HttpServletRequest request) { Notice notice = airbrakeNotifier.newNotice(throwable); // Add user context from Spring Security addUserContext(notice); // Add request context if (request != null) { addRequestContext(notice, request); } return notice; } private void addUserContext(Notice notice) { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication != null && authentication.isAuthenticated() && !(authentication.getPrincipal() instanceof String)) { CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal(); // Create user context for Airbrake Map<String, Object> userContext = new HashMap<>(); userContext.put("id", userDetails.getId()); userContext.put("username", userDetails.getUsername()); userContext.put("email", userDetails.getEmail()); userContext.put("tenant_id", userDetails.getTenantId()); userContext.put("department", userDetails.getDepartment()); // Add roles/authorities List<String> roles = userDetails.getAuthorities().stream() .map(GrantedAuthority::getAuthority) .collect(Collectors.toList()); userContext.put("roles", roles); notice.setContext("user", userContext); } } private void addRequestContext(Notice notice, HttpServletRequest request) { Map<String, Object> requestContext = new HashMap<>(); requestContext.put("method", request.getMethod()); requestContext.put("url", request.getRequestURL().toString()); requestContext.put("query_string", request.getQueryString()); requestContext.put("user_agent", request.getHeader("User-Agent")); requestContext.put("ip", getClientIpAddress(request)); notice.setContext("http", requestContext); } private String getClientIpAddress(HttpServletRequest request) { String xfHeader = request.getHeader("X-Forwarded-For"); if (xfHeader != null) { return xfHeader.split(",")[0]; } return request.getRemoteAddr(); } } 3. Global Exception Handler with Airbrake Integration:
@ControllerAdvice public class GlobalExceptionHandler { private final UserAwareNoticeBuilder airbrakeNotifier; public GlobalExceptionHandler(UserAwareNoticeBuilder airbrakeNotifier) { this.airbrakeNotifier = airbrakeNotifier; } @ExceptionHandler(Exception.class) public ResponseEntity<Map<String, Object>> handleException( Exception ex, HttpServletRequest request) { // Notify Airbrake with user context airbrakeNotifier.notify(ex, request); // Return user-friendly error response Map<String, Object> response = new HashMap<>(); response.put("error", "An unexpected error occurred"); response.put("timestamp", Instant.now()); response.put("path", request.getRequestURI()); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body(response); } @ExceptionHandler(AccessDeniedException.class) public ResponseEntity<Map<String, Object>> handleAccessDenied( AccessDeniedException ex, HttpServletRequest request) { // Log access denied attempts with user context Authentication auth = SecurityContextHolder.getContext().getAuthentication(); if (auth != null) { CustomUserDetails userDetails = (CustomUserDetails) auth.getPrincipal(); Map<String, Object> securityContext = new HashMap<>(); securityContext.put("user_id", userDetails.getId()); securityContext.put("attempted_path", request.getRequestURI()); securityContext.put("user_roles", userDetails.getAuthorities()); ex.setContext("security", securityContext); } airbrakeNotifier.notify(ex, request); Map<String, Object> response = new HashMap<>(); response.put("error", "Access denied"); response.put("timestamp", Instant.now()); return ResponseEntity.status(HttpStatus.FORBIDDEN).body(response); } } Advanced Integration: Custom Filters for Deeper Context
For even richer context, you can create filters that capture additional user activity.
1. User Activity Tracking Filter:
@Component public class UserActivityFilter implements Filter { private static final ThreadLocal<Map<String, Object>> userActivityContext = new ThreadLocal<>(); @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { try { // Initialize user context for this request userActivityContext.set(new HashMap<>()); // Capture user activity if authenticated Authentication auth = SecurityContextHolder.getContext().getAuthentication(); if (auth != null && auth.isAuthenticated() && !(auth.getPrincipal() instanceof String)) { CustomUserDetails userDetails = (CustomUserDetails) auth.getPrincipal(); Map<String, Object> activity = userActivityContext.get(); activity.put("user_id", userDetails.getId()); activity.put("login_time", getLoginTime(auth)); activity.put("session_id", request.getServletContext().getSessionCookieConfig()); } chain.doFilter(request, response); } finally { userActivityContext.remove(); } } public static Map<String, Object> getCurrentUserActivity() { return userActivityContext.get(); } private Instant getLoginTime(Authentication authentication) { // Extract login time from authentication details if available if (authentication.getDetails() instanceof WebAuthenticationDetails) { // You might store login time in session or other mechanism } return Instant.now(); // Fallback } } 2. Enhanced Airbrake Integration with Activity Context:
@Component public class EnhancedAirbrakeService { public void notifyWithActivity(Throwable throwable, String feature, Map<String, Object> customParams) { Notice notice = buildEnhancedNotice(throwable, feature, customParams); // Send to Airbrake } private Notice buildEnhancedNotice(Throwable throwable, String feature, Map<String, Object> customParams) { Notice notice = // ... build basic notice // Add user activity context Map<String, Object> activity = UserActivityFilter.getCurrentUserActivity(); if (activity != null) { notice.setContext("user_activity", activity); } // Add feature context notice.setContext("feature", feature); // Add custom parameters if (customParams != null) { notice.setContext("custom", customParams); } return notice; } } Testing the Integration
Create tests to verify that user context is properly captured:
@SpringBootTest @AutoConfigureTestDatabase class AirbrakeUserIntegrationTest { @Autowired private UserAwareNoticeBuilder airbrakeNotifier; @Mock private HttpServletRequest mockRequest; @Test void whenExceptionOccursWithAuthenticatedUser_thenUserContextIsCaptured() { // Given CustomUserDetails userDetails = createTestUserDetails(); Authentication auth = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(auth); when(mockRequest.getRequestURL()).thenReturn(new StringBuffer("/api/test")); when(mockRequest.getMethod()).thenReturn("GET"); // When TestException ex = new TestException("Test error"); airbrakeNotifier.notify(ex, mockRequest); // Then - Verify through Airbrake API or mock that user context was included // This would typically verify through a mocked Airbrake client } private CustomUserDetails createTestUserDetails() { return new CustomUserDetails( 123L, "john_doe", "[email protected]", "encrypted_password", List.of(new SimpleGrantedAuthority("ROLE_USER")), "tenant_abc", "Engineering" ); } static class TestException extends RuntimeException { TestException(String message) { super(message); } } } Best Practices for Production
- Sanitize Sensitive Data: Ensure no PII or sensitive information is sent to Airbrake.
- Use Environment-Specific Configuration: Different configurations for dev, staging, and production.
- Rate Limiting: Implement rate limiting to prevent overwhelming Airbrake during error storms.
- Context Enrichment: Continuously add relevant business context to error reports.
- Monitor Airbrake Performance: Ensure the integration isn't impacting application performance.
Conclusion
Integrating Airbrake with Spring Security's UserDetails transforms your error monitoring from generic alerts to context-rich incidents. By automatically attaching user identity, roles, and activity context to every error report, you empower your development team to:
- Quickly identify affected users and their permissions
- Understand the security context of access-related errors
- Correlate errors with specific user segments or tenants
- Prioritize fixes based on user impact
This integration turns error monitoring from a reactive debugging tool into a proactive system health dashboard that understands both your technical infrastructure and your business context.