feat: 集成符号化服务 - 实现 triggerSymbolicationAsync

- 注入 SourcemapService 到 LogService
- 实现 triggerSymbolicationAsync 调用符号化服务
- 支持 RN/Android/iOS/Flutter 堆栈符号化
- 新增 symbolicator.url 配置

Co-Authored-By: Claude <noreply@anthropic.com>
这个提交包含在:
XuqmGroup 2026-06-19 02:50:03 +08:00
父节点 064e3e3223
当前提交 a8ad342336

查看文件

@ -31,19 +31,25 @@ public class LogService {
private final LogIssueUserRepository issueUserRepository;
private final LogEventRepository eventRepository;
private final WebhookService webhookService;
private final SourcemapService sourcemapService;
private final ObjectMapper objectMapper;
@org.springframework.beans.factory.annotation.Value("${symbolicator.url:http://xuqm-symbolicator:3000}")
private String symbolicatorUrl;
public LogService(LogIssueRepository issueRepository,
LogIssueEventRepository issueEventRepository,
LogIssueUserRepository issueUserRepository,
LogEventRepository eventRepository,
WebhookService webhookService,
SourcemapService sourcemapService,
ObjectMapper objectMapper) {
this.issueRepository = issueRepository;
this.issueEventRepository = issueEventRepository;
this.issueUserRepository = issueUserRepository;
this.eventRepository = eventRepository;
this.webhookService = webhookService;
this.sourcemapService = sourcemapService;
this.objectMapper = objectMapper;
}
@ -149,6 +155,79 @@ public class LogService {
@Async
void triggerSymbolicationAsync(Long issueId, String appKey, String platform, String release) {
log.debug("Symbolication triggered issueId={} appKey={} platform={} release={}", issueId, appKey, platform, release);
// 查找 sourcemap 文件
String storageKey = sourcemapService.findSourceMap(appKey, platform, release, "index");
if (storageKey == null) {
log.debug("No sourcemap found for appKey={} platform={} release={}", appKey, platform, release);
return;
}
// 查找事件
LogIssueEventEntity event = issueEventRepository.findById(issueId).orElse(null);
if (event == null || event.getStack() == null || event.getStack().isBlank()) {
return;
}
try {
// 读取 sourcemap 内容
String sourcemapContent = sourcemapService.readSourceMapContent(storageKey);
if (sourcemapContent == null) {
log.warn("Failed to read sourcemap content for storageKey={}", storageKey);
return;
}
// 确定平台类型
String symbolicatePlatform = mapPlatform(platform);
// 调用符号化服务
String symbolicated = callSymbolicator(symbolicatePlatform, event.getStack(), sourcemapContent);
if (symbolicated != null && !symbolicated.equals(event.getStack())) {
event.setStackSymbolicated(symbolicated);
issueEventRepository.save(event);
log.info("Symbolicated issueId={} platform={}", issueId, platform);
}
} catch (Exception e) {
log.error("Symbolication failed for issueId={}: {}", issueId, e.getMessage(), e);
}
}
private String mapPlatform(String platform) {
if (platform == null) return "rn";
return switch (platform.toLowerCase()) {
case "android" -> "android";
case "ios" -> "ios";
case "flutter" -> "flutter";
default -> "rn"; // RN/Electron/Vue3 都使用 JS sourcemap
};
}
private String callSymbolicator(String platform, String stacktrace, String sourcemapContent) {
try {
org.springframework.web.client.RestTemplate restTemplate = new org.springframework.web.client.RestTemplate();
java.util.Map<String, Object> request = java.util.Map.of(
"platform", platform,
"stacktrace", stacktrace,
"sourcemap", sourcemapContent
);
org.springframework.http.HttpHeaders headers = new org.springframework.http.HttpHeaders();
headers.setContentType(org.springframework.http.MediaType.APPLICATION_JSON);
org.springframework.http.HttpEntity<java.util.Map<String, Object>> entity = new org.springframework.http.HttpEntity<>(request, headers);
java.util.Map response = restTemplate.postForObject(
symbolicatorUrl + "/api/symbolicate",
entity,
java.util.Map.class
);
if (response != null && Boolean.TRUE.equals(response.get("success"))) {
return (String) response.get("symbolicated");
}
} catch (Exception e) {
log.error("Symbolicator service call failed: {}", e.getMessage());
}
return null;
}
@Transactional