Shake for Bug Reports in Java: Comprehensive Implementation

Introduction to Shake Bug Reporting

Shake is a bug reporting SDK that allows users to report issues by shaking their device or through manual triggers. It captures screenshots, device info, logs, and user feedback. This implementation brings similar functionality to Java applications.


System Architecture Overview

Shake Bug Reporting System
├── Trigger Mechanisms
│   ├ - Shake Detection (Mobile)
│   ├ - Keyboard Shortcuts (Desktop)
│   ├ - Manual UI Trigger
│   └ - Automated Error Detection
├── Data Collection
│   ├ - Screenshots & Screen Recording
│   ├ - Application State
│   ├ - System Information
│   ├ - Log Files
│   └ - User Input
├── Report Processing
│   ├ - Data Compression
│   ├ - Privacy Filtering
│   ├ - Report Formatting
│   └ - Attachment Management
└── Delivery & Integration
├ - Local Storage
├ - Email Integration
├ - API Upload (JIRA, GitHub)
├ - Slack/Teams Webhooks
â”” - Analytics Integration

Core Implementation

1. Maven Dependencies

<properties>
<spring.boot.version>2.7.0</spring.boot.version>
<jackson.version>2.14.2</jackson.version>
<aws.java.sdk.version>2.20.56</aws.java.sdk.version>
<thumbnailator.version>0.4.19</thumbnailator.version>
</properties>
<dependencies>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring.boot.version}</version>
</dependency>
<!-- JSON Processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<!-- Image Processing -->
<dependency>
<groupId>net.coobird</groupId>
<artifactId>thumbnailator</artifactId>
<version>${thumbnailator.version}</version>
</dependency>
<!-- File Upload -->
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.5</version>
</dependency>
<!-- Email Support -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
<version>${spring.boot.version}</version>
</dependency>
<!-- HTTP Client -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
<version>${spring.boot.version}</version>
</dependency>
<!-- Compression -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
<version>1.23.0</version>
</dependency>
<!-- Logging -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
<version>${spring.boot.version}</version>
</dependency>
</dependencies>

2. Core Configuration

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.List;
@Component
@ConfigurationProperties(prefix = "shake.bug-report")
public class ShakeConfig {
private boolean enabled = true;
private String triggerMethod = "MANUAL"; // SHAKE, KEYBOARD, MANUAL
private int shakeSensitivity = 5;
private String screenshotQuality = "HIGH"; // LOW, MEDIUM, HIGH
private int maxAttachmentSize = 10; // MB
private boolean captureLogs = true;
private boolean captureScreenshot = true;
private boolean captureSystemInfo = true;
private List<String> excludedFields = Arrays.asList("password", "token", "secret");
private String storageType = "LOCAL"; // LOCAL, S3, DATABASE
private String reportFormat = "JSON"; // JSON, XML, HTML
// Notification settings
private boolean emailNotification = false;
private String emailRecipients;
private boolean slackNotification = false;
private String slackWebhookUrl;
private boolean jiraIntegration = false;
private String jiraProjectKey;
// Getters and setters
public boolean isEnabled() { return enabled; }
public void setEnabled(boolean enabled) { this.enabled = enabled; }
public String getTriggerMethod() { return triggerMethod; }
public void setTriggerMethod(String triggerMethod) { this.triggerMethod = triggerMethod; }
public int getShakeSensitivity() { return shakeSensitivity; }
public void setShakeSensitivity(int shakeSensitivity) { this.shakeSensitivity = shakeSensitivity; }
public String getScreenshotQuality() { return screenshotQuality; }
public void setScreenshotQuality(String screenshotQuality) { this.screenshotQuality = screenshotQuality; }
public int getMaxAttachmentSize() { return maxAttachmentSize; }
public void setMaxAttachmentSize(int maxAttachmentSize) { this.maxAttachmentSize = maxAttachmentSize; }
public boolean isCaptureLogs() { return captureLogs; }
public void setCaptureLogs(boolean captureLogs) { this.captureLogs = captureLogs; }
public boolean isCaptureScreenshot() { return captureScreenshot; }
public void setCaptureScreenshot(boolean captureScreenshot) { this.captureScreenshot = captureScreenshot; }
public boolean isCaptureSystemInfo() { return captureSystemInfo; }
public void setCaptureSystemInfo(boolean captureSystemInfo) { this.captureSystemInfo = captureSystemInfo; }
public List<String> getExcludedFields() { return excludedFields; }
public void setExcludedFields(List<String> excludedFields) { this.excludedFields = excludedFields; }
public String getStorageType() { return storageType; }
public void setStorageType(String storageType) { this.storageType = storageType; }
public String getReportFormat() { return reportFormat; }
public void setReportFormat(String reportFormat) { this.reportFormat = reportFormat; }
public boolean isEmailNotification() { return emailNotification; }
public void setEmailNotification(boolean emailNotification) { this.emailNotification = emailNotification; }
public String getEmailRecipients() { return emailRecipients; }
public void setEmailRecipients(String emailRecipients) { this.emailRecipients = emailRecipients; }
public boolean isSlackNotification() { return slackNotification; }
public void setSlackNotification(boolean slackNotification) { this.slackNotification = slackNotification; }
public String getSlackWebhookUrl() { return slackWebhookUrl; }
public void setSlackWebhookUrl(String slackWebhookUrl) { this.slackWebhookUrl = slackWebhookUrl; }
public boolean isJiraIntegration() { return jiraIntegration; }
public void setJiraIntegration(boolean jiraIntegration) { this.jiraIntegration = jiraIntegration; }
public String getJiraProjectKey() { return jiraProjectKey; }
public void setJiraProjectKey(String jiraProjectKey) { this.jiraProjectKey = jiraProjectKey; }
}

3. Core Bug Report Service

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.*;
import java.lang.management.ManagementFactory;
import java.lang.management.RuntimeMXBean;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
@Service
public class ShakeBugReportService {
private static final Logger logger = LoggerFactory.getLogger(ShakeBugReportService.class);
private final ShakeConfig config;
private final BugReportStorageService storageService;
private final NotificationService notificationService;
private final DataSanitizationService sanitizationService;
private final List<BugReportListener> listeners = new CopyOnWriteArrayList<>();
private final Map<String, BugReport> pendingReports = new ConcurrentHashMap<>();
// Shake detection variables (for mobile/desktop apps)
private long lastShakeTime = 0;
private final double SHAKE_THRESHOLD = 2.5;
private final int SHAKE_COOLDOWN_MS = 2000;
public ShakeBugReportService(ShakeConfig config, 
BugReportStorageService storageService,
NotificationService notificationService,
DataSanitizationService sanitizationService) {
this.config = config;
this.storageService = storageService;
this.notificationService = notificationService;
this.sanitizationService = sanitizationService;
}
@PostConstruct
public void init() {
if (config.isEnabled()) {
logger.info("Shake bug reporting service initialized");
setupShakeDetection();
} else {
logger.info("Shake bug reporting service is disabled");
}
}
/**
* Main method to trigger bug report creation
*/
public BugReport createBugReport(BugReportRequest request) {
if (!config.isEnabled()) {
throw new IllegalStateException("Bug reporting is disabled");
}
logger.info("Creating bug report for user: {}", request.getUserId());
BugReport report = new BugReport();
report.setId(generateReportId());
report.setTimestamp(LocalDateTime.now());
report.setUserId(request.getUserId());
report.setUserEmail(request.getUserEmail());
report.setTitle(request.getTitle());
report.setDescription(request.getDescription());
report.setSeverity(request.getSeverity());
report.setCategory(request.getCategory());
try {
// Collect various types of diagnostic data
collectSystemInformation(report);
collectApplicationState(report);
collectLogFiles(report);
if (config.isCaptureScreenshot() && request.isIncludeScreenshot()) {
captureScreenshot(report);
}
if (request.getAttachments() != null) {
processAttachments(report, request.getAttachments());
}
// Sanitize sensitive data
sanitizationService.sanitizeReport(report);
// Store the report
String storagePath = storageService.storeReport(report);
report.setStoragePath(storagePath);
// Notify listeners
notifyListeners(report);
// Send notifications
sendNotifications(report);
logger.info("Bug report created successfully: {}", report.getId());
} catch (Exception e) {
logger.error("Failed to create bug report", e);
report.setError("Failed to create report: " + e.getMessage());
}
return report;
}
/**
* Shake detection for mobile devices (conceptual)
*/
public void onShakeDetected(double accelerationX, double accelerationY, double accelerationZ) {
if (!config.isEnabled() || !"SHAKE".equals(config.getTriggerMethod())) {
return;
}
long currentTime = System.currentTimeMillis();
if (currentTime - lastShakeTime < SHAKE_COOLDOWN_MS) {
return; // Cooldown period
}
// Calculate total acceleration
double totalAcceleration = Math.sqrt(
accelerationX * accelerationX + 
accelerationY * accelerationY + 
accelerationZ * accelerationZ
);
if (totalAcceleration > SHAKE_THRESHOLD * config.getShakeSensitivity()) {
lastShakeTime = currentTime;
triggerShakeReport();
}
}
/**
* Manual trigger via keyboard shortcut
*/
public void onKeyboardShortcut() {
if (!config.isEnabled() || !"KEYBOARD".equals(config.getTriggerMethod())) {
return;
}
triggerManualReport();
}
/**
* Programmatic trigger for automated error reporting
*/
public BugReport reportError(Throwable error, String context, Map<String, Object> additionalData) {
BugReportRequest request = new BugReportRequest();
request.setTitle("Automated Error Report: " + error.getClass().getSimpleName());
request.setDescription("Error: " + error.getMessage() + "\nContext: " + context);
request.setSeverity(BugSeverity.HIGH);
request.setCategory("AUTOMATED_ERROR");
request.setIncludeScreenshot(true);
request.setAdditionalData(additionalData);
// Add stack trace to attachments
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
error.printStackTrace(pw);
Map<String, String> attachments = new HashMap<>();
attachments.put("stacktrace.txt", sw.toString());
request.setAttachments(attachments);
return createBugReport(request);
}
private void triggerShakeReport() {
logger.debug("Shake detected, triggering bug report");
BugReportRequest request = new BugReportRequest();
request.setTitle("Shake Report - " + LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME));
request.setDescription("Bug report triggered by device shake");
request.setSeverity(BugSeverity.MEDIUM);
request.setCategory("SHAKE_TRIGGER");
request.setIncludeScreenshot(true);
// This would typically show a dialog in a UI application
pendingReports.put("shake_" + System.currentTimeMillis(), createBugReport(request));
}
private void triggerManualReport() {
logger.debug("Manual trigger for bug report");
BugReportRequest request = new BugReportRequest();
request.setTitle("Manual Report - " + LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME));
request.setDescription("Bug report triggered manually");
request.setSeverity(BugSeverity.MEDIUM);
request.setCategory("MANUAL_TRIGGER");
request.setIncludeScreenshot(true);
pendingReports.put("manual_" + System.currentTimeMillis(), createBugReport(request));
}
private void collectSystemInformation(BugReport report) {
if (!config.isCaptureSystemInfo()) {
return;
}
SystemInfo systemInfo = new SystemInfo();
// Java Runtime Information
systemInfo.setJavaVersion(System.getProperty("java.version"));
systemInfo.setJavaVendor(System.getProperty("java.vendor"));
systemInfo.setJavaHome(System.getProperty("java.home"));
// OS Information
systemInfo.setOsName(System.getProperty("os.name"));
systemInfo.setOsVersion(System.getProperty("os.version"));
systemInfo.setOsArchitecture(System.getProperty("os.arch"));
// User Information
systemInfo.setUserName(System.getProperty("user.name"));
systemInfo.setUserHome(System.getProperty("user.home"));
systemInfo.setUserTimezone(TimeZone.getDefault().getID());
// Runtime Information
RuntimeMXBean runtimeMxBean = ManagementFactory.getRuntimeMXBean();
systemInfo.setJvmUptime(runtimeMxBean.getUptime());
systemInfo.setJvmInputArguments(runtimeMxBean.getInputArguments());
// Memory Information
Runtime runtime = Runtime.getRuntime();
systemInfo.setTotalMemory(runtime.totalMemory());
systemInfo.setFreeMemory(runtime.freeMemory());
systemInfo.setMaxMemory(runtime.maxMemory());
systemInfo.setAvailableProcessors(runtime.availableProcessors());
// Application-specific information
systemInfo.setApplicationVersion(getApplicationVersion());
systemInfo.setEnvironment(System.getProperty("spring.profiles.active", "default"));
report.setSystemInfo(systemInfo);
}
private void collectApplicationState(BugReport report) {
ApplicationState appState = new ApplicationState();
// Thread Information
ThreadMXBean threadBean = ManagementFactory.getThreadMXBean();
appState.setThreadCount(threadBean.getThreadCount());
appState.setDaemonThreadCount(threadBean.getDaemonThreadCount());
// Memory Pool Information
List<MemoryPoolInfo> memoryPools = new ArrayList<>();
for (MemoryPoolMXBean pool : ManagementFactory.getMemoryPoolMXBeans()) {
MemoryPoolInfo poolInfo = new MemoryPoolInfo();
poolInfo.setName(pool.getName());
poolInfo.setType(pool.getType().name());
poolInfo.setUsage(pool.getUsage().getUsed());
poolInfo.setMax(pool.getUsage().getMax());
memoryPools.add(poolInfo);
}
appState.setMemoryPools(memoryPools);
// Garbage Collection Information
List<GcInfo> gcInfos = new ArrayList<>();
for (GarbageCollectorMXBean gc : ManagementFactory.getGarbageCollectorMXBeans()) {
GcInfo gcInfo = new GcInfo();
gcInfo.setName(gc.getName());
gcInfo.setCollectionCount(gc.getCollectionCount());
gcInfo.setCollectionTime(gc.getCollectionTime());
gcInfos.add(gcInfo);
}
appState.setGarbageCollectors(gcInfos);
report.setApplicationState(appState);
}
private void collectLogFiles(BugReport report) {
if (!config.isCaptureLogs()) {
return;
}
try {
LogCollection logCollection = new LogCollection();
// Capture recent application logs
String recentLogs = captureRecentLogs();
logCollection.setRecentLogs(recentLogs);
// Capture log files
Map<String, String> logFiles = captureLogFiles();
logCollection.setLogFiles(logFiles);
report.setLogCollection(logCollection);
} catch (Exception e) {
logger.warn("Failed to capture log files", e);
}
}
private void captureScreenshot(BugReport report) {
if (!GraphicsEnvironment.isHeadless()) {
try {
Robot robot = new Robot();
Rectangle screenRect = new Rectangle(Toolkit.getDefaultToolkit().getScreenSize());
BufferedImage screenshot = robot.createScreenCapture(screenRect);
// Compress image based on quality setting
BufferedImage compressedImage = compressImage(screenshot);
// Convert to bytes
ByteArrayOutputStream baos = new ByteArrayOutputStream();
javax.imageio.ImageIO.write(compressedImage, "jpg", baos);
byte[] imageBytes = baos.toByteArray();
report.setScreenshot(imageBytes);
report.setScreenshotFormat("jpg");
} catch (Exception e) {
logger.warn("Failed to capture screenshot", e);
}
}
}
private BufferedImage compressImage(BufferedImage original) {
String quality = config.getScreenshotQuality();
double scaleFactor;
switch (quality) {
case "LOW":
scaleFactor = 0.3;
break;
case "MEDIUM":
scaleFactor = 0.6;
break;
case "HIGH":
default:
scaleFactor = 0.8;
break;
}
int newWidth = (int) (original.getWidth() * scaleFactor);
int newHeight = (int) (original.getHeight() * scaleFactor);
BufferedImage resized = new BufferedImage(newWidth, newHeight, original.getType());
Graphics2D g = resized.createGraphics();
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g.drawImage(original, 0, 0, newWidth, newHeight, null);
g.dispose();
return resized;
}
private void processAttachments(BugReport report, Map<String, String> attachments) {
if (attachments == null || attachments.isEmpty()) {
return;
}
Map<String, byte[]> processedAttachments = new HashMap<>();
for (Map.Entry<String, String> entry : attachments.entrySet()) {
try {
byte[] data = entry.getValue().getBytes();
// Check size limit
if (data.length > config.getMaxAttachmentSize() * 1024 * 1024) {
logger.warn("Attachment {} exceeds size limit, skipping", entry.getKey());
continue;
}
processedAttachments.put(entry.getKey(), data);
} catch (Exception e) {
logger.warn("Failed to process attachment: {}", entry.getKey(), e);
}
}
report.setAttachments(processedAttachments);
}
private String captureRecentLogs() {
// Implementation depends on your logging framework
// This is a simplified version
return "Recent logs capture would be implemented here";
}
private Map<String, String> captureLogFiles() {
Map<String, String> logFiles = new HashMap<>();
// Look for log files in common locations
String[] logLocations = {
"logs/application.log",
"var/log/myapp/application.log",
"./application.log"
};
for (String location : logLocations) {
try {
Path logPath = Paths.get(location);
if (Files.exists(logPath)) {
String content = new String(Files.readAllBytes(logPath));
logFiles.put(location, content);
}
} catch (Exception e) {
logger.debug("Could not read log file: {}", location);
}
}
return logFiles;
}
private String getApplicationVersion() {
try {
Package pkg = getClass().getPackage();
return pkg.getImplementationVersion() != null ? 
pkg.getImplementationVersion() : "unknown";
} catch (Exception e) {
return "unknown";
}
}
private String generateReportId() {
return "BR-" + System.currentTimeMillis() + "-" + UUID.randomUUID().toString().substring(0, 8);
}
private void setupShakeDetection() {
// This would set up accelerometer listeners in a mobile app
// For server applications, this might not be applicable
logger.debug("Shake detection setup completed");
}
private void notifyListeners(BugReport report) {
for (BugReportListener listener : listeners) {
try {
listener.onBugReportCreated(report);
} catch (Exception e) {
logger.error("Error notifying listener", e);
}
}
}
private void sendNotifications(BugReport report) {
if (config.isEmailNotification()) {
notificationService.sendEmailNotification(report);
}
if (config.isSlackNotification()) {
notificationService.sendSlackNotification(report);
}
if (config.isJiraIntegration()) {
notificationService.createJiraIssue(report);
}
}
// Public API methods
public void addListener(BugReportListener listener) {
listeners.add(listener);
}
public void removeListener(BugReportListener listener) {
listeners.remove(listener);
}
public List<BugReport> getPendingReports() {
return new ArrayList<>(pendingReports.values());
}
public BugReport getReport(String reportId) {
return storageService.loadReport(reportId);
}
public List<BugReport> getRecentReports(int count) {
return storageService.getRecentReports(count);
}
}

4. Data Models

// Main bug report class
public class BugReport {
private String id;
private LocalDateTime timestamp;
private String userId;
private String userEmail;
private String title;
private String description;
private BugSeverity severity;
private String category;
private SystemInfo systemInfo;
private ApplicationState applicationState;
private LogCollection logCollection;
private byte[] screenshot;
private String screenshotFormat;
private Map<String, byte[]> attachments;
private String storagePath;
private String error;
private Map<String, Object> metadata = new HashMap<>();
// Getters and setters
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public LocalDateTime getTimestamp() { return timestamp; }
public void setTimestamp(LocalDateTime timestamp) { this.timestamp = timestamp; }
public String getUserId() { return userId; }
public void setUserId(String userId) { this.userId = userId; }
public String getUserEmail() { return userEmail; }
public void setUserEmail(String userEmail) { this.userEmail = userEmail; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public BugSeverity getSeverity() { return severity; }
public void setSeverity(BugSeverity severity) { this.severity = severity; }
public String getCategory() { return category; }
public void setCategory(String category) { this.category = category; }
public SystemInfo getSystemInfo() { return systemInfo; }
public void setSystemInfo(SystemInfo systemInfo) { this.systemInfo = systemInfo; }
public ApplicationState getApplicationState() { return applicationState; }
public void setApplicationState(ApplicationState applicationState) { this.applicationState = applicationState; }
public LogCollection getLogCollection() { return logCollection; }
public void setLogCollection(LogCollection logCollection) { this.logCollection = logCollection; }
public byte[] getScreenshot() { return screenshot; }
public void setScreenshot(byte[] screenshot) { this.screenshot = screenshot; }
public String getScreenshotFormat() { return screenshotFormat; }
public void setScreenshotFormat(String screenshotFormat) { this.screenshotFormat = screenshotFormat; }
public Map<String, byte[]> getAttachments() { return attachments; }
public void setAttachments(Map<String, byte[]> attachments) { this.attachments = attachments; }
public String getStoragePath() { return storagePath; }
public void setStoragePath(String storagePath) { this.storagePath = storagePath; }
public String getError() { return error; }
public void setError(String error) { this.error = error; }
public Map<String, Object> getMetadata() { return metadata; }
public void setMetadata(Map<String, Object> metadata) { this.metadata = metadata; }
}
// Request DTO
public class BugReportRequest {
private String userId;
private String userEmail;
private String title;
private String description;
private BugSeverity severity = BugSeverity.MEDIUM;
private String category = "GENERAL";
private boolean includeScreenshot = true;
private Map<String, String> attachments;
private Map<String, Object> additionalData;
// Getters and setters
public String getUserId() { return userId; }
public void setUserId(String userId) { this.userId = userId; }
public String getUserEmail() { return userEmail; }
public void setUserEmail(String userEmail) { this.userEmail = userEmail; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public BugSeverity getSeverity() { return severity; }
public void setSeverity(BugSeverity severity) { this.severity = severity; }
public String getCategory() { return category; }
public void setCategory(String category) { this.category = category; }
public boolean isIncludeScreenshot() { return includeScreenshot; }
public void setIncludeScreenshot(boolean includeScreenshot) { this.includeScreenshot = includeScreenshot; }
public Map<String, String> getAttachments() { return attachments; }
public void setAttachments(Map<String, String> attachments) { this.attachments = attachments; }
public Map<String, Object> getAdditionalData() { return additionalData; }
public void setAdditionalData(Map<String, Object> additionalData) { this.additionalData = additionalData; }
}
// Supporting classes
public enum BugSeverity {
LOW, MEDIUM, HIGH, CRITICAL
}
public class SystemInfo {
private String javaVersion;
private String javaVendor;
private String javaHome;
private String osName;
private String osVersion;
private String osArchitecture;
private String userName;
private String userHome;
private String userTimezone;
private long jvmUptime;
private List<String> jvmInputArguments;
private long totalMemory;
private long freeMemory;
private long maxMemory;
private int availableProcessors;
private String applicationVersion;
private String environment;
// Getters and setters...
}
public class ApplicationState {
private int threadCount;
private int daemonThreadCount;
private List<MemoryPoolInfo> memoryPools;
private List<GcInfo> garbageCollectors;
// Getters and setters...
}
public class LogCollection {
private String recentLogs;
private Map<String, String> logFiles;
// Getters and setters...
}

5. Storage Service

import org.springframework.stereotype.Service;
import java.io.*;
import java.nio.file.*;
import java.util.*;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
@Service
public class BugReportStorageService {
private static final String STORAGE_BASE = "bug-reports";
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
public String storeReport(BugReport report) {
String reportPath = generateReportPath(report);
try {
Path reportDir = Paths.get(STORAGE_BASE, reportPath);
Files.createDirectories(reportDir);
// Store main report as JSON
String reportJson = serializeReport(report);
Files.write(reportDir.resolve("report.json"), reportJson.getBytes());
// Store screenshot if present
if (report.getScreenshot() != null) {
Files.write(reportDir.resolve("screenshot." + report.getScreenshotFormat()), 
report.getScreenshot());
}
// Store attachments
if (report.getAttachments() != null) {
Path attachmentsDir = reportDir.resolve("attachments");
Files.createDirectories(attachmentsDir);
for (Map.Entry<String, byte[]> attachment : report.getAttachments().entrySet()) {
Files.write(attachmentsDir.resolve(attachment.getKey()), attachment.getValue());
}
}
// Create a zip archive for easy download
createZipArchive(reportDir, report.getId());
return reportPath;
} catch (Exception e) {
throw new RuntimeException("Failed to store bug report", e);
}
}
public BugReport loadReport(String reportId) {
try {
// Find report directory
Path reportDir = findReportDirectory(reportId);
if (reportDir == null) {
return null;
}
Path reportFile = reportDir.resolve("report.json");
String reportJson = new String(Files.readAllBytes(reportFile));
return deserializeReport(reportJson);
} catch (Exception e) {
throw new RuntimeException("Failed to load bug report: " + reportId, e);
}
}
public List<BugReport> getRecentReports(int count) {
try {
Path baseDir = Paths.get(STORAGE_BASE);
if (!Files.exists(baseDir)) {
return Collections.emptyList();
}
return Files.walk(baseDir)
.filter(path -> path.resolve("report.json").toFile().exists())
.sorted((p1, p2) -> {
try {
return Files.getLastModifiedTime(p2.resolve("report.json"))
.compareTo(Files.getLastModifiedTime(p1.resolve("report.json")));
} catch (IOException e) {
return 0;
}
})
.limit(count)
.map(path -> {
try {
String reportJson = new String(Files.readAllBytes(path.resolve("report.json")));
return deserializeReport(reportJson);
} catch (Exception e) {
return null;
}
})
.filter(Objects::nonNull)
.collect(Collectors.toList());
} catch (Exception e) {
throw new RuntimeException("Failed to get recent reports", e);
}
}
public byte[] getReportArchive(String reportId) {
try {
Path reportDir = findReportDirectory(reportId);
if (reportDir == null) {
return null;
}
Path zipFile = reportDir.resolve(reportId + ".zip");
if (Files.exists(zipFile)) {
return Files.readAllBytes(zipFile);
}
return null;
} catch (Exception e) {
throw new RuntimeException("Failed to get report archive", e);
}
}
private String generateReportPath(BugReport report) {
String dateStr = report.getTimestamp().format(DATE_FORMATTER);
return dateStr + "/" + report.getId();
}
private Path findReportDirectory(String reportId) throws IOException {
Path baseDir = Paths.get(STORAGE_BASE);
if (!Files.exists(baseDir)) {
return null;
}
return Files.walk(baseDir)
.filter(path -> path.getFileName().toString().equals(reportId))
.findFirst()
.orElse(null);
}
private String serializeReport(BugReport report) {
// Using Jackson ObjectMapper in real implementation
// Simplified for example
return "JSON representation of bug report";
}
private BugReport deserializeReport(String reportJson) {
// Using Jackson ObjectMapper in real implementation
// Simplified for example
return new BugReport();
}
private void createZipArchive(Path reportDir, String reportId) throws IOException {
Path zipFile = reportDir.resolve(reportId + ".zip");
try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile.toFile()))) {
Files.walk(reportDir)
.filter(path -> !Files.isDirectory(path) && !path.getFileName().toString().endsWith(".zip"))
.forEach(path -> {
try {
String entryName = reportDir.relativize(path).toString();
zos.putNextEntry(new ZipEntry(entryName));
Files.copy(path, zos);
zos.closeEntry();
} catch (IOException e) {
throw new UncheckedIOException(e);
}
});
}
}
}

6. REST API Controllers

import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/bug-reports")
public class BugReportController {
private final ShakeBugReportService bugReportService;
private final BugReportStorageService storageService;
public BugReportController(ShakeBugReportService bugReportService,
BugReportStorageService storageService) {
this.bugReportService = bugReportService;
this.storageService = storageService;
}
@PostMapping
public ResponseEntity<BugReport> createBugReport(@RequestBody BugReportRequest request) {
try {
BugReport report = bugReportService.createBugReport(request);
return ResponseEntity.ok(report);
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
@PostMapping("/with-attachments")
public ResponseEntity<BugReport> createBugReportWithAttachments(
@RequestPart("request") BugReportRequest request,
@RequestPart(value = "attachments", required = false) MultipartFile[] attachments) {
try {
// Process file attachments
if (attachments != null && attachments.length > 0) {
Map<String, String> attachmentMap = new HashMap<>();
for (MultipartFile file : attachments) {
if (!file.isEmpty()) {
attachmentMap.put(file.getOriginalFilename(), 
new String(file.getBytes()));
}
}
request.setAttachments(attachmentMap);
}
BugReport report = bugReportService.createBugReport(request);
return ResponseEntity.ok(report);
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
@GetMapping("/{reportId}")
public ResponseEntity<BugReport> getBugReport(@PathVariable String reportId) {
BugReport report = bugReportService.getReport(reportId);
if (report == null) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(report);
}
@GetMapping("/{reportId}/download")
public ResponseEntity<byte[]> downloadBugReport(@PathVariable String reportId) {
byte[] archive = storageService.getReportArchive(reportId);
if (archive == null) {
return ResponseEntity.notFound().build();
}
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
headers.setContentDisposition(ContentDisposition.attachment()
.filename(reportId + ".zip")
.build());
return new ResponseEntity<>(archive, headers, HttpStatus.OK);
}
@GetMapping
public ResponseEntity<List<BugReport>> getRecentBugReports(
@RequestParam(defaultValue = "10") int count) {
List<BugReport> reports = bugReportService.getRecentReports(count);
return ResponseEntity.ok(reports);
}
@PostMapping("/{reportId}/reproduce")
public ResponseEntity<String> markAsReproducible(@PathVariable String reportId) {
// Mark report as reproducible
return ResponseEntity.ok("Report marked as reproducible");
}
@PostMapping("/trigger/shake")
public ResponseEntity<String> triggerShakeReport() {
bugReportService.onShakeDetected(3.0, 3.0, 3.0); // Simulate shake
return ResponseEntity.ok("Shake report triggered");
}
@PostMapping("/trigger/manual")
public ResponseEntity<String> triggerManualReport() {
bugReportService.onKeyboardShortcut();
return ResponseEntity.ok("Manual report triggered");
}
@PostMapping("/error")
public ResponseEntity<BugReport> reportError(@RequestBody ErrorReportRequest errorRequest) {
try {
BugReport report = bugReportService.reportError(
errorRequest.getError(), 
errorRequest.getContext(), 
errorRequest.getAdditionalData()
);
return ResponseEntity.ok(report);
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
}
class ErrorReportRequest {
private Throwable error;
private String context;
private Map<String, Object> additionalData;
// Getters and setters...
}

7. Notification Service

import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
@Service
public class NotificationService {
private final ShakeConfig config;
private final JavaMailSender mailSender;
private final WebClient webClient;
public NotificationService(ShakeConfig config, 
JavaMailSender mailSender,
WebClient.Builder webClientBuilder) {
this.config = config;
this.mailSender = mailSender;
this.webClient = webClientBuilder.build();
}
public void sendEmailNotification(BugReport report) {
if (!config.isEmailNotification() || config.getEmailRecipients() == null) {
return;
}
try {
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true);
helper.setTo(config.getEmailRecipients().split(","));
helper.setSubject("Bug Report: " + report.getTitle());
helper.setText(buildEmailContent(report), true);
// Attach screenshot if available
if (report.getScreenshot() != null) {
helper.addAttachment("screenshot.jpg", 
new ByteArrayResource(report.getScreenshot()));
}
mailSender.send(message);
} catch (Exception e) {
logger.error("Failed to send email notification", e);
}
}
public void sendSlackNotification(BugReport report) {
if (!config.isSlackNotification() || config.getSlackWebhookUrl() == null) {
return;
}
try {
Map<String, Object> slackMessage = new HashMap<>();
slackMessage.put("text", "New Bug Report: " + report.getTitle());
List<Map<String, Object>> attachments = new ArrayList<>();
Map<String, Object> attachment = new HashMap<>();
attachment.put("color", getSeverityColor(report.getSeverity()));
attachment.put("title", report.getTitle());
attachment.put("text", report.getDescription());
attachment.put("fields", createSlackFields(report));
attachments.add(attachment);
slackMessage.put("attachments", attachments);
webClient.post()
.uri(config.getSlackWebhookUrl())
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(slackMessage)
.retrieve()
.bodyToMono(String.class)
.block();
} catch (Exception e) {
logger.error("Failed to send Slack notification", e);
}
}
public void createJiraIssue(BugReport report) {
if (!config.isJiraIntegration()) {
return;
}
try {
Map<String, Object> jiraIssue = new HashMap<>();
jiraIssue.put("fields", createJiraFields(report));
// This would make actual JIRA API call
// webClient.post()...
} catch (Exception e) {
logger.error("Failed to create JIRA issue", e);
}
}
private String buildEmailContent(BugReport report) {
return String.format("""
<h2>Bug Report: %s</h2>
<p><strong>Description:</strong> %s</p>
<p><strong>Severity:</strong> %s</p>
<p><strong>User:</strong> %s (%s)</p>
<p><strong>Timestamp:</strong> %s</p>
<p><strong>System Info:</strong> %s %s, Java %s</p>
<hr>
<p><a href="%s">View full report</a></p>
""", 
report.getTitle(),
report.getDescription(),
report.getSeverity(),
report.getUserId(),
report.getUserEmail(),
report.getTimestamp(),
report.getSystemInfo().getOsName(),
report.getSystemInfo().getOsVersion(),
report.getSystemInfo().getJavaVersion(),
generateReportUrl(report.getId())
);
}
private List<Map<String, Object>> createSlackFields(BugReport report) {
List<Map<String, Object>> fields = new ArrayList<>();
addSlackField(fields, "Severity", report.getSeverity().toString(), true);
addSlackField(fields, "User", report.getUserId(), true);
addSlackField(fields, "Environment", report.getSystemInfo().getEnvironment(), true);
addSlackField(fields, "Java Version", report.getSystemInfo().getJavaVersion(), true);
return fields;
}
private void addSlackField(List<Map<String, Object>> fields, String title, String value, boolean shortField) {
Map<String, Object> field = new HashMap<>();
field.put("title", title);
field.put("value", value);
field.put("short", shortField);
fields.add(field);
}
private Map<String, Object> createJiraFields(BugReport report) {
Map<String, Object> fields = new HashMap<>();
fields.put("project", Map.of("key", config.getJiraProjectKey()));
fields.put("summary", "Bug Report: " + report.getTitle());
fields.put("description", buildJiraDescription(report));
fields.put("issuetype", Map.of("name", "Bug"));
return fields;
}
private String buildJiraDescription(BugReport report) {
return String.format("""
*Description:*
{quote}%s{quote}
*System Information:*
* OS: %s %s
* Java: %s
* User: %s
* Environment: %s
{color:red}Automatically generated bug report{color}
""",
report.getDescription(),
report.getSystemInfo().getOsName(),
report.getSystemInfo().getOsVersion(),
report.getSystemInfo().getJavaVersion(),
report.getUserId(),
report.getSystemInfo().getEnvironment()
);
}
private String getSeverityColor(BugSeverity severity) {
switch (severity) {
case CRITICAL: return "#FF0000";
case HIGH: return "#FF6B00";
case MEDIUM: return "#FFC400";
case LOW: return "#00FF00";
default: return "#CCCCCC";
}
}
private String generateReportUrl(String reportId) {
return "/api/bug-reports/" + reportId;
}
}

8. Application Configuration

# application.yml
shake:
bug-report:
enabled: true
trigger-method: MANUAL # SHAKE, KEYBOARD, MANUAL
shake-sensitivity: 5
screenshot-quality: HIGH
max-attachment-size: 10
capture-logs: true
capture-screenshot: true
capture-system-info: true
excluded-fields:
- "password"
- "token"
- "secret"
storage-type: LOCAL
report-format: JSON
# Notifications
email-notification: false
email-recipients: "[email protected]"
slack-notification: false
slack-webhook-url: ${SLACK_WEBHOOK_URL}
jira-integration: false
jira-project-key: "BUG"
# Email configuration (if using email notifications)
spring:
mail:
host: smtp.gmail.com
port: 587
username: ${EMAIL_USERNAME}
password: ${EMAIL_PASSWORD}
properties:
mail:
smtp:
auth: true
starttls:
enable: true
# Logging configuration
logging:
file:
name: logs/application.log
level:
com.example.shake: DEBUG

Best Practices

1. Privacy and Security

@Component
public class DataSanitizationService {
public void sanitizeReport(BugReport report) {
// Remove sensitive data from system info
if (report.getSystemInfo() != null) {
report.getSystemInfo().setUserName("[REDACTED]");
report.getSystemInfo().setUserHome("[REDACTED]");
}
// Sanitize logs
if (report.getLogCollection() != null) {
report.getLogCollection().setRecentLogs(
sanitizeString(report.getLogCollection().getRecentLogs())
);
}
// Sanitize description
report.setDescription(sanitizeString(report.getDescription()));
}
private String sanitizeString(String input) {
if (input == null) return null;
// Remove credit card numbers
input = input.replaceAll("\\b\\d{4}[ -]?\\d{4}[ -]?\\d{4}[ -]?\\d{4}\\b", "[CREDIT_CARD]");
// Remove email addresses
input = input.replaceAll("\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Z|a-z]{2,}\\b", "[EMAIL]");
// Remove other sensitive patterns
for (String pattern : config.getExcludedFields()) {
input = input.replaceAll("(?i)" + pattern + "\\s*[:=]\\s*[^\\s]+", pattern + "=[REDACTED]");
}
return input;
}
}

2. Performance Optimization

@Component
public class PerformanceOptimizer {
@Async
public void processReportAsync(BugReport report) {
// Heavy processing in background thread
compressAttachments(report);
generateAnalytics(report);
updateDashboard(report);
}
private void compressAttachments(BugReport report) {
if (report.getAttachments() != null) {
report.getAttachments().replaceAll((key, value) -> 
compressData(value)
);
}
}
}

Conclusion

This comprehensive Shake-like bug reporting implementation provides:

  • Multiple trigger mechanisms (shake, keyboard, manual)
  • Comprehensive data collection (screenshots, logs, system info)
  • Flexible storage options (local, cloud, database)
  • Multiple notification channels (email, Slack, JIRA)
  • Privacy and security with data sanitization
  • REST API for integration
  • Async processing for performance

Key benefits:

  • Easy bug reporting for users and developers
  • Rich context with screenshots and system information
  • Flexible integration with existing tools
  • Privacy-focused with data sanitization
  • Production-ready with proper error handling

This system enables efficient bug reporting and triaging, helping development teams quickly identify and resolve issues with comprehensive context and diagnostic information.

Leave a Reply

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


Macro Nepal Helper