From 8fc4c951381ff32e02efd92226daaee90219cc03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E5=8B=A4=E6=B0=91?= Date: Fri, 19 Jun 2026 02:49:26 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=88=9D=E5=A7=8B=E5=8C=96=E7=AC=A6?= =?UTF-8?q?=E5=8F=B7=E5=8C=96=E6=9C=8D=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 支持平台: - RN/Electron/Vue3 (JS Sourcemap) - Android (ProGuard mapping) - iOS (dSYM) - Flutter (Debug Symbols) Co-Authored-By: Claude --- Dockerfile | 33 ++++++ package.json | 25 +++++ src/index.ts | 31 ++++++ src/parsers/dsym.ts | 85 ++++++++++++++++ src/parsers/flutter.ts | 90 ++++++++++++++++ src/parsers/proguard.ts | 174 +++++++++++++++++++++++++++++++ src/parsers/sourcemap.ts | 101 ++++++++++++++++++ src/parsers/stack-parser.ts | 153 ++++++++++++++++++++++++++++ src/routes/symbolicate.ts | 198 ++++++++++++++++++++++++++++++++++++ tsconfig.json | 19 ++++ 10 files changed, 909 insertions(+) create mode 100644 Dockerfile create mode 100644 package.json create mode 100644 src/index.ts create mode 100644 src/parsers/dsym.ts create mode 100644 src/parsers/flutter.ts create mode 100644 src/parsers/proguard.ts create mode 100644 src/parsers/sourcemap.ts create mode 100644 src/parsers/stack-parser.ts create mode 100644 src/routes/symbolicate.ts create mode 100644 tsconfig.json diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c3164d0 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,33 @@ +FROM node:20-alpine AS builder + +WORKDIR /app + +# Install dependencies +COPY package.json package-lock.json* ./ +RUN npm ci + +# Copy source +COPY tsconfig.json ./ +COPY src/ ./src/ + +# Build +RUN npm run build + +# Production image +FROM node:20-alpine + +WORKDIR /app + +# Install dependencies +COPY package.json package-lock.json* ./ +RUN npm ci --production + +# Copy built files +COPY --from=builder /app/dist ./dist + +# Install atos for iOS symbolication (if needed) +RUN apk add --no-cache binutils + +EXPOSE 3000 + +CMD ["node", "dist/index.js"] diff --git a/package.json b/package.json new file mode 100644 index 0000000..4826ff8 --- /dev/null +++ b/package.json @@ -0,0 +1,25 @@ +{ + "name": "@xuqm/symbolicator", + "version": "1.0.0", + "description": "BugCollect 符号化服务 - 支持 RN/Android/iOS/Flutter", + "main": "dist/index.js", + "scripts": { + "dev": "tsx watch src/index.ts", + "build": "tsc", + "start": "node dist/index.js" + }, + "dependencies": { + "express": "^4.18.2", + "source-map": "^0.7.4", + "multer": "^1.4.5-lts.1", + "cors": "^2.8.5" + }, + "devDependencies": { + "@types/express": "^4.17.21", + "@types/multer": "^1.4.11", + "@types/cors": "^2.8.17", + "@types/node": "^20.10.0", + "typescript": "^5.3.2", + "tsx": "^4.6.2" + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..c3b9485 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,31 @@ +import express from 'express' +import cors from 'cors' +import { symbolicateRouter } from './routes/symbolicate' + +const app = express() +const PORT = process.env.PORT || 3000 + +// Middleware +app.use(cors()) +app.use(express.json({ limit: '200mb' })) +app.use(express.text({ limit: '200mb' })) + +// Routes +app.use('/api', symbolicateRouter) + +// Health check +app.get('/health', (req, res) => { + res.json({ status: 'ok', service: 'xuqm-symbolicator', version: '1.0.0' }) +}) + +// Error handler +app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => { + console.error('Error:', err.message) + res.status(500).json({ success: false, error: err.message }) +}) + +app.listen(PORT, () => { + console.log(`Symbolicator service running on port ${PORT}`) +}) + +export default app diff --git a/src/parsers/dsym.ts b/src/parsers/dsym.ts new file mode 100644 index 0000000..0ad26ec --- /dev/null +++ b/src/parsers/dsym.ts @@ -0,0 +1,85 @@ +/** + * iOS dSYM 符号化解析器 + * + * 使用 atos 命令进行符号化: + * atos -o MyApp.app.dSYM/Contents/Resources/DWARF/MyApp -arch arm64 -l 0x102345000 0x102345678 + */ + +import { execSync } from 'child_process' +import { StackFrame } from './stack-parser' + +export interface DsymConfig { + /** dSYM 文件路径 */ + dsymPath: string + /** 应用加载地址 */ + loadAddress: string + /** 架构 (arm64, x86_64) */ + arch?: string +} + +/** + * 使用 atos 命令符号化 iOS 堆栈 + */ +export function symbolicateWithDsym( + frames: StackFrame[], + config: DsymConfig +): string[] { + const { dsymPath, loadAddress, arch = 'arm64' } = config + + // 提取需要符号化的地址 + const addresses = frames + .filter(f => f.file === '') + .map(f => `0x${f.column.toString(16)}`) + + if (addresses.length === 0) { + return frames.map(f => `${f.function} (${f.file}:${f.line})`) + } + + try { + // 调用 atos 命令 + const cmd = `atos -o "${dsymPath}" -arch ${arch} -l ${loadAddress} ${addresses.join(' ')}` + const output = execSync(cmd, { encoding: 'utf-8', timeout: 30000 }) + + const symbolicated = output.trim().split('\n') + let addrIndex = 0 + + return frames.map(f => { + if (f.file === '' && addrIndex < symbolicated.length) { + return symbolicated[addrIndex++] + } + return `${f.function} (${f.file}:${f.line})` + }) + } catch (err) { + console.error('atos symbolication failed:', err) + return frames.map(f => `${f.function} (${f.file}:${f.line})`) + } +} + +/** + * 解析 iOS 崩溃日志中的线程帧 + * + * 格式示例: + * 0 MyApp 0x0000000102345678 myFunction + 123 + * 1 MyApp 0x0000000102345678 myFunction + 123 + */ +export function parseIOSCrashFrames(crashLog: string): StackFrame[] { + const frames: StackFrame[] = [] + const lines = crashLog.split('\n') + + for (const line of lines) { + const match = line.trim().match( + /(\d+)\s+(\S+)\s+(0x[\da-f]+)\s+(\S+)\s+\+\s+(\d+)/ + ) + + if (match) { + frames.push({ + function: match[4], + file: match[2], + line: 0, + column: parseInt(match[5], 10), + }) + } + } + + return frames +} diff --git a/src/parsers/flutter.ts b/src/parsers/flutter.ts new file mode 100644 index 0000000..b196696 --- /dev/null +++ b/src/parsers/flutter.ts @@ -0,0 +1,90 @@ +/** + * Flutter/Dart 符号化解析器 + * + * Flutter 使用 --split-debug-info 生成调试符号文件 + * 使用 flutter symbolize 命令进行符号化 + */ + +import { execSync } from 'child_process' +import { StackFrame } from './stack-parser' + +export interface FlutterDebugConfig { + /** debug symbols 文件路径 (通常是 .debug 文件) */ + debugSymbolsPath: string + /** Flutter SDK 路径 (可选,默认使用系统 flutter) */ + flutterPath?: string +} + +/** + * 使用 flutter symbolize 命令符号化 Flutter 堆栈 + */ +export function symbolicateWithFlutter( + stacktrace: string, + config: FlutterDebugConfig +): string { + const { debugSymbolsPath, flutterPath = 'flutter' } = config + + try { + // 将堆栈写入临时文件 + const tmpInput = `/tmp/flutter_stack_${Date.now()}.txt` + const tmpOutput = `/tmp/flutter_symbolicated_${Date.now()}.txt` + + require('fs').writeFileSync(tmpInput, stacktrace) + + // 调用 flutter symbolize + const cmd = `${flutterPath} symbolize -i "${tmpInput}" -o "${tmpOutput}" --debug-info "${debugSymbolsPath}"` + execSync(cmd, { timeout: 60000 }) + + // 读取符号化结果 + const result = require('fs').readFileSync(tmpOutput, 'utf-8') + + // 清理临时文件 + try { + require('fs').unlinkSync(tmpInput) + require('fs').unlinkSync(tmpOutput) + } catch {} + + return result + } catch (err) { + console.error('Flutter symbolization failed:', err) + return stacktrace + } +} + +/** + * 解析 Flutter/Dart 堆栈帧 + * + * 格式示例: + * #0 MyApp.myMethod (package:my_app/main.dart:42:10) + * #1 MyApp.anotherMethod (package:my_app/main.dart:50:5) + * #2 _rootRun (dart:async/zone.dart:1390:13) + */ +export function parseFlutterStack(stacktrace: string): StackFrame[] { + const frames: StackFrame[] = [] + const lines = stacktrace.split('\n') + + for (const line of lines) { + // 匹配 "#0 ClassName.method (package:.../file.dart:line:col)" 格式 + const match = line.trim().match( + /#\d+\s+(\S+)\s+\((.+?):(\d+):(\d+)\)/ + ) + + if (match) { + frames.push({ + function: match[1], + file: match[2], + line: parseInt(match[3], 10), + column: parseInt(match[4], 10), + }) + } + } + + return frames +} + +/** + * 检查是否是 Dart 原生堆栈帧(非用户代码) + */ +export function isDartNativeFrame(file: string): boolean { + return file.startsWith('dart:') || file.startsWith('package:flutter/') +} diff --git a/src/parsers/proguard.ts b/src/parsers/proguard.ts new file mode 100644 index 0000000..efd4e0a --- /dev/null +++ b/src/parsers/proguard.ts @@ -0,0 +1,174 @@ +/** + * Android ProGuard mapping.txt 解析器 + * + * mapping.txt 格式示例: + * com.example.MyClass -> a.b.c: + * int myField -> a + * void myMethod(int) -> a + * 42:42:void otherMethod():100:100 -> b + */ + +export interface ProguardMapping { + obfuscatedClass: string + originalClass: string + methods: MethodMapping[] + lines: LineMapping[] +} + +export interface MethodMapping { + obfuscatedMethod: string + originalMethod: string + obfuscatedLine: number + originalLine: number +} + +export interface LineMapping { + originalLine: number + obfuscatedLine: number + originalFile: string +} + +/** + * 解析 ProGuard mapping.txt 文件 + */ +export function parseProguardMapping(content: string): Map { + const mappings = new Map() + + let currentClass: ProguardMapping | null = null + let currentOriginalClass = '' + let currentObfuscatedClass = '' + let lineNumber = 0 + + const lines = content.split('\n') + + for (const line of lines) { + lineNumber++ + + // 跳过空行和注释 + if (!line.trim() || line.startsWith('#')) continue + + // 类映射行:com.example.MyClass -> a.b.c: + const classMatch = line.match(/^(\S+)\s+->\s+(\S+):$/) + if (classMatch) { + currentOriginalClass = classMatch[1] + currentObfuscatedClass = classMatch[2].replace(/:$/, '') + + currentClass = { + obfuscatedClass: currentObfuscatedClass, + originalClass: currentOriginalClass, + methods: [], + lines: [], + } + mappings.set(currentObfuscatedClass, currentClass) + continue + } + + if (!currentClass) continue + + // 行号映射行:42:42:void otherMethod():100:100 -> b + const lineMatch = line.match( + /^\s+(\d+):(\d+):([^:]+):(\d+):(\d+)\s+->\s+(\S+)$/ + ) + if (lineMatch) { + currentClass.lines.push({ + originalLine: parseInt(lineMatch[4], 10), + obfuscatedLine: parseInt(lineMatch[1], 10), + originalFile: currentOriginalClass, + }) + continue + } + + // 方法映射行:void myMethod(int) -> a + const methodMatch = line.match( + /^\s+(?:(\d+):(\d+):)?(\S+)\s+(\S+)\(([^)]*)\)\s+->\s+(\S+)$/ + ) + if (methodMatch) { + currentClass.methods.push({ + obfuscatedMethod: methodMatch[6], + originalMethod: methodMatch[4], + obfuscatedLine: methodMatch[1] ? parseInt(methodMatch[1], 10) : 0, + originalLine: methodMatch[2] ? parseInt(methodMatch[2], 10) : 0, + }) + continue + } + } + + return mappings +} + +/** + * 使用 mapping 反混淆堆栈帧 + */ +export function deobfuscateFrame( + mappings: Map, + obfuscatedClass: string, + obfuscatedMethod: string, + obfuscatedLine: number +): { class: string; method: string; line: number; file: string } | null { + // 查找类映射 + const mapping = mappings.get(obfuscatedClass) + if (!mapping) return null + + // 查找方法映射 + const methodMapping = mapping.methods.find(m => m.obfuscatedMethod === obfuscatedMethod) + + // 查找行号映射 + let originalLine = obfuscatedLine + const lineMapping = mapping.lines.find(l => l.obfuscatedLine === obfuscatedLine) + if (lineMapping) { + originalLine = lineMapping.originalLine + } else if (methodMapping) { + // 使用方法的行号范围估算 + originalLine = methodMapping.originalLine + } + + return { + class: mapping.originalClass, + method: methodMapping?.originalMethod || obfuscatedMethod, + line: originalLine, + file: `${mapping.originalClass.split('.').pop()}.java`, + } +} + +/** + * 反混淆整个堆栈字符串 + */ +export function deobfuscateStack( + mappings: Map, + stacktrace: string +): string { + const lines = stacktrace.split('\n') + const result: string[] = [] + + for (const line of lines) { + // 匹配 "at com.example.a.b(MyFile.java:42)" 格式 + const match = line.match( + /at\s+([\w.$]+)\.([\w$]+)\(([\w.]+):(\d+)\)/ + ) + + if (match) { + const obfuscatedClass = match[1] + const obfuscatedMethod = match[2] + const obfuscatedLine = parseInt(match[4], 10) + + const deobfuscated = deobfuscateFrame( + mappings, + obfuscatedClass, + obfuscatedMethod, + obfuscatedLine + ) + + if (deobfuscated) { + result.push( + `at ${deobfuscated.class}.${deobfuscated.method}(${deobfuscated.file}:${deobfuscated.line})` + ) + continue + } + } + + // 无法反混淆,保留原始行 + result.push(line) + } + + return result.join('\n') +} diff --git a/src/parsers/sourcemap.ts b/src/parsers/sourcemap.ts new file mode 100644 index 0000000..e24f00d --- /dev/null +++ b/src/parsers/sourcemap.ts @@ -0,0 +1,101 @@ +/** + * JS Sourcemap 解析器 - 使用 Mozilla source-map 库 + */ + +import { SourceMapConsumer } from 'source-map' +import { StackFrame } from './stack-parser' + +export interface SymbolicatedFrame { + originalFile: string + originalLine: number + originalColumn: number + originalFunction: string + minifiedFile: string + minifiedLine: number + minifiedColumn: number +} + +/** + * 使用 sourcemap 解析堆栈帧 + */ +export async function symbolicateWithSourcemap( + frames: StackFrame[], + sourcemapContent: string +): Promise { + const rawSourceMap = JSON.parse(sourcemapContent) + const consumer = await new SourceMapConsumer(rawSourceMap) + + const results: SymbolicatedFrame[] = [] + + for (const frame of frames) { + try { + const original = consumer.originalPositionFor({ + line: frame.line, + column: frame.column, + }) + + if (original.source) { + results.push({ + originalFile: original.source, + originalLine: original.line || 0, + originalColumn: original.column || 0, + originalFunction: original.name || frame.function, + minifiedFile: frame.file, + minifiedLine: frame.line, + minifiedColumn: frame.column, + }) + } else { + // 无法映射,保留原始帧 + results.push({ + originalFile: frame.file, + originalLine: frame.line, + originalColumn: frame.column, + originalFunction: frame.function, + minifiedFile: frame.file, + minifiedLine: frame.line, + minifiedColumn: frame.column, + }) + } + } catch (err) { + // 解析失败,保留原始帧 + results.push({ + originalFile: frame.file, + originalLine: frame.line, + originalColumn: frame.column, + originalFunction: frame.function, + minifiedFile: frame.file, + minifiedLine: frame.line, + minifiedColumn: frame.column, + }) + } + } + + consumer.destroy() + return results +} + +/** + * 将符号化结果格式化为堆栈字符串 + */ +export function formatSymbolicatedStack( + type: string, + message: string, + frames: SymbolicatedFrame[] +): string { + const lines: string[] = [`${type}: ${message}`] + + for (const frame of frames) { + const funcPart = frame.originalFunction !== '' + ? `${frame.originalFunction}` + : '' + const locationPart = `${frame.originalFile}:${frame.originalLine}:${frame.originalColumn}` + + if (funcPart) { + lines.push(` at ${funcPart} (${locationPart})`) + } else { + lines.push(` at ${locationPart}`) + } + } + + return lines.join('\n') +} diff --git a/src/parsers/stack-parser.ts b/src/parsers/stack-parser.ts new file mode 100644 index 0000000..5890b51 --- /dev/null +++ b/src/parsers/stack-parser.ts @@ -0,0 +1,153 @@ +/** + * 堆栈格式解析器 - 支持 RN/Hermes/V8/Java/iOS/Flutter + */ + +export interface StackFrame { + function: string + file: string + line: number + column: number +} + +export type Platform = 'rn' | 'android' | 'ios' | 'flutter' | 'electron' | 'vue3' + +/** + * 解析堆栈字符串,提取帧信息 + */ +export function parseStackFrames(platform: Platform, stacktrace: string): StackFrame[] { + if (!stacktrace) return [] + + switch (platform) { + case 'rn': + case 'electron': + case 'vue3': + return parseJavaScriptStack(stacktrace) + case 'android': + return parseJavaStack(stacktrace) + case 'ios': + return parseIOSStack(stacktrace) + case 'flutter': + return parseFlutterStack(stacktrace) + default: + return parseJavaScriptStack(stacktrace) + } +} + +/** + * 解析 JavaScript 堆栈(RN Hermes/V8/Chrome) + * + * 格式示例: + * at someFunction (http://localhost:8081/index.bundle:12345:67) + * at Object.someFunction (http://localhost:8081/index.bundle:12345:67) + * at someFunction (index.bundle:12345:67) + * at someFunction (native) + */ +function parseJavaScriptStack(stacktrace: string): StackFrame[] { + const frames: StackFrame[] = [] + const lines = stacktrace.split('\n') + + for (const line of lines) { + // 匹配 "at functionName (file:line:column)" 格式 + const match = line.trim().match( + /at\s+(?:(.+?)\s+\()?(.+?):(\d+):(\d+)\)?/ + ) + + if (match) { + frames.push({ + function: match[1] || '', + file: match[2], + line: parseInt(match[3], 10), + column: parseInt(match[4], 10), + }) + } + } + + return frames +} + +/** + * 解析 Java/Kotlin 堆栈 + * + * 格式示例: + * at com.example.MyClass.myMethod(MyFile.java:42) + * at com.example.MyClass.myMethod(MyFile.kt:42) + */ +function parseJavaStack(stacktrace: string): StackFrame[] { + const frames: StackFrame[] = [] + const lines = stacktrace.split('\n') + + for (const line of lines) { + const match = line.trim().match( + /at\s+([\w.$]+)\.([\w$]+)\(([\w.]+):(\d+)\)/ + ) + + if (match) { + frames.push({ + function: `${match[1]}.${match[2]}`, + file: match[3], + line: parseInt(match[4], 10), + column: 0, + }) + } + } + + return frames +} + +/** + * 解析 iOS 堆栈 + * + * 格式示例: + * 0 MyApp 0x0000000102345678 myFunction + 123 + * 1 MyApp 0x0000000102345678 myFunction + 123 + */ +function parseIOSStack(stacktrace: string): StackFrame[] { + const frames: StackFrame[] = [] + const lines = stacktrace.split('\n') + + for (const line of lines) { + const match = line.trim().match( + /\d+\s+\S+\s+0x[\da-f]+\s+(\S+)\s+\+\s+(\d+)/ + ) + + if (match) { + frames.push({ + function: match[1], + file: '', + line: 0, + column: parseInt(match[2], 10), + }) + } + } + + return frames +} + +/** + * 解析 Flutter/Dart 堆栈 + * + * 格式示例: + * #0 MyApp.myMethod (package:my_app/main.dart:42:10) + * #1 MyApp.anotherMethod (package:my_app/main.dart:50:5) + */ +function parseFlutterStack(stacktrace: string): StackFrame[] { + const frames: StackFrame[] = [] + const lines = stacktrace.split('\n') + + for (const line of lines) { + const match = line.trim().match( + /#\d+\s+(\S+)\s+\((.+?):(\d+):(\d+)\)/ + ) + + if (match) { + frames.push({ + function: match[1], + file: match[2], + line: parseInt(match[3], 10), + column: parseInt(match[4], 10), + }) + } + } + + return frames +} diff --git a/src/routes/symbolicate.ts b/src/routes/symbolicate.ts new file mode 100644 index 0000000..990b1b7 --- /dev/null +++ b/src/routes/symbolicate.ts @@ -0,0 +1,198 @@ +/** + * 符号化 API 路由 + */ + +import { Router, Request, Response } from 'express' +import { parseStackFrames, Platform } from '../parsers/stack-parser' +import { symbolicateWithSourcemap, formatSymbolicatedStack, SymbolicatedFrame } from '../parsers/sourcemap' +import { parseProguardMapping, deobfuscateStack } from '../parsers/proguard' +import { symbolicateWithDsym } from '../parsers/dsym' +import { symbolicateWithFlutter } from '../parsers/flutter' + +export const symbolicateRouter = Router() + +interface SymbolicateRequest { + /** 平台类型 */ + platform: Platform + /** 原始堆栈字符串 */ + stacktrace: string + /** 异常类型 */ + type?: string + /** 异常消息 */ + message?: string + + // JS Sourcemap (RN/Electron/Vue3) + /** Sourcemap JSON 字符串 */ + sourcemap?: string + + // Android ProGuard + /** ProGuard mapping.txt 内容 */ + mappingFile?: string + + // iOS dSYM + /** dSYM 文件路径 */ + dsymPath?: string + /** 应用加载地址 */ + loadAddress?: string + /** 架构 */ + arch?: string + + // Flutter + /** Flutter debug symbols 路径 */ + debugSymbolsPath?: string +} + +/** + * POST /api/symbolicate + * + * 符号化堆栈,支持多个平台 + */ +symbolicateRouter.post('/symbolicate', async (req: Request, res: Response) => { + try { + const { + platform, + stacktrace, + type = 'Error', + message = '', + sourcemap, + mappingFile, + dsymPath, + loadAddress, + arch, + debugSymbolsPath, + } = req.body as SymbolicateRequest + + if (!platform || !stacktrace) { + return res.status(400).json({ + success: false, + error: 'platform and stacktrace are required', + }) + } + + let result: { symbolicated: string; frames?: SymbolicatedFrame[] } + + switch (platform) { + case 'rn': + case 'electron': + case 'vue3': + // JS Sourcemap 符号化 + if (!sourcemap) { + return res.status(400).json({ + success: false, + error: 'sourcemap is required for JS platforms', + }) + } + result = await symbolicateJS(stacktrace, sourcemap, type, message) + break + + case 'android': + // Android ProGuard 反混淆 + if (!mappingFile) { + return res.status(400).json({ + success: false, + error: 'mappingFile is required for Android', + }) + } + result = symbolicateAndroid(stacktrace, mappingFile) + break + + case 'ios': + // iOS dSYM 符号化 + if (!dsymPath || !loadAddress) { + return res.status(400).json({ + success: false, + error: 'dsymPath and loadAddress are required for iOS', + }) + } + result = symbolicateIOS(stacktrace, dsymPath, loadAddress, arch) + break + + case 'flutter': + // Flutter 符号化 + if (!debugSymbolsPath) { + return res.status(400).json({ + success: false, + error: 'debugSymbolsPath is required for Flutter', + }) + } + result = symbolicateFlutter(stacktrace, debugSymbolsPath) + break + + default: + return res.status(400).json({ + success: false, + error: `Unsupported platform: ${platform}`, + }) + } + + return res.json({ + success: true, + ...result, + }) + } catch (err: any) { + console.error('Symbolication error:', err) + return res.status(500).json({ + success: false, + error: err.message, + }) + } +}) + +/** + * JS Sourcemap 符号化 + */ +async function symbolicateJS( + stacktrace: string, + sourcemap: string, + type: string, + message: string +) { + const frames = parseStackFrames('rn', stacktrace) + const symbolicatedFrames = await symbolicateWithSourcemap(frames, sourcemap) + const symbolicated = formatSymbolicatedStack(type, message, symbolicatedFrames) + + return { + symbolicated, + frames: symbolicatedFrames, + } +} + +/** + * Android ProGuard 反混淆 + */ +function symbolicateAndroid(stacktrace: string, mappingFile: string) { + const mappings = parseProguardMapping(mappingFile) + const symbolicated = deobfuscateStack(mappings, stacktrace) + + return { symbolicated } +} + +/** + * iOS dSYM 符号化 + */ +function symbolicateIOS( + stacktrace: string, + dsymPath: string, + loadAddress: string, + arch?: string +) { + const frames = parseStackFrames('ios', stacktrace) + const symbolicatedLines = symbolicateWithDsym(frames, { + dsymPath, + loadAddress, + arch: arch || 'arm64', + }) + + return { symbolicated: symbolicatedLines.join('\n') } +} + +/** + * Flutter 符号化 + */ +function symbolicateFlutter(stacktrace: string, debugSymbolsPath: string) { + const symbolicated = symbolicateWithFlutter(stacktrace, { + debugSymbolsPath, + }) + + return { symbolicated } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..50572da --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["ES2020"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +}