Enriching Error Context: Integrating Airbrake with Spring Security UserDetails


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

  1. Sanitize Sensitive Data: Ensure no PII or sensitive information is sent to Airbrake.
  2. Use Environment-Specific Configuration: Different configurations for dev, staging, and production.
  3. Rate Limiting: Implement rate limiting to prevent overwhelming Airbrake during error storms.
  4. Context Enrichment: Continuously add relevant business context to error reports.
  5. 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.

Leave a Reply

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


Macro Nepal Helper