Repository Wiki
isHarryh/Ark-Pets

Sentry 错误上报

SentryHelper 是 Desktop(JavaFX 启动器)进程中封装 Sentry Java SDK 的静态工具类,负责遥测开关管理、SDK 初始化、WAL 遥测日志重放上报、会话/性能指标/配置快照上报,以及用户日志反馈提交。它是 ArkPets 双进程遥测架构中唯一与 Sentry 云端通信的出口。

Purpose and Scope

本页覆盖 Sentry 错误上报子系统的完整实现,包括:

  • SentryHelper 的初始化流程、DSN 注入机制与可用性降级策略;
  • WAL(Write-Ahead Log)文件的消费与重放逻辑,即 Core 进程崩溃/异常退出事件如何被事后补报;
  • 会话上报的级别判定算法(ERROR / WARN / INFO)与异常事件的补捕获;
  • Core 性能指标(Gauge / Distribution)、配置快照、系统信息的上报细节;
  • 用户日志文件反馈通道。

有意留给兄弟页面的内容:

  • WAL 文件的写入端(编码器 WalCoreHeartbeatCodec、WalExceptionCodec、WalConfigCodec、WalSystemInfoCodec 等及 HeartbeatSession 会话管理)属于 Core 侧遥测数据采集,见兄弟页面;
  • 遥测总体架构、隐私政策与数据范围说明见项目文档 docs/Telemetry.md;
  • 桌宠渲染与性能数据采集(而非上报)逻辑不属于本页范围。

Overview

ArkPets 由两个独立 JVM 进程构成:Desktop(JavaFX 启动器)与 Core(libGDX 桌宠渲染)。遥测架构的核心约束是:只有 Desktop 与 Sentry 通信,Core 不具备网络通信功能。Core 只把遥测数据追加写入本地 WAL 文件,由 Desktop 在下次启动时通过 SentryHelper.consumePendingWal() 重放并上报到 Sentry 平台。

这种设计带来三个关键收益:

  1. 进程隔离:Core 保持无网络依赖,可脱离 Desktop 独立运行(例如开发调试场景),其生命周期数据不因进程退出而丢失;
  2. 崩溃可观测:Core 崩溃(未捕获异常)时异常对象已被序列化进 WAL,Desktop 下次消费 WAL 时将其还原为 SentryEvent 补报,时间戳保留为崩溃发生时刻而非上报时刻;
  3. 优雅降级:DSN 为空时 SDK 完全禁用,WAL 会被直接清理删除,不产生任何网络流量。

SentryHelper 同时维护两个语义不同的布尔标志:

  • sdkAvailable:SDK 是否成功初始化(DSN 有效且类库存在);
  • enable:用户遥测开关。setEnable() 中 enable && sdkAvailable 的短路设计保证 SDK 不可用时开关永远无法打开。

Architecture

Loading diagram...

架构要点解读:

  • 单向数据流:Core → 本地 WAL → Desktop SentryHelper → Sentry SDK → Sentry 云端。Core 到 Sentry 之间不存在任何直接路径。
  • Desktop 自身会话不走 WAL:Desktop 进程自己的会话通过 HeartbeatSession 直接管理(beginDesktopSession() / endDesktopSession()),因为 Desktop 自身拥有网络能力,无需落盘中转。
  • SDK 初始化即网关:SentryHelper.init() 中先以空 DSN 初始化(禁用状态),再依赖 setEnableExternalConfiguration(true) 让外部 -Dsentry.dsn 或 SENTRY_DSN 环境变量覆盖。这是整个遥测链路的总开关。

SentryHelper 的内部结构(私有方法分组与数据聚合类型)如下:

Loading diagram...

WalData 是核心的聚合中间结构:collectWalRecords() 单次遍历 WAL 记录流,把同一文件中的心跳、异常、配置、系统信息分别归位——心跳只保留最后一条用于会话判定,而全部心跳保留在 modelHeartbeats 列表中用于逐条上报性能指标。

Core Flow

SDK 初始化与外部配置覆盖

java
1public static void init() { 2 try { 3 Sentry.init(options -> { 4 // An empty DSN will disable the SDK gracefully. 5 options.setDsn(""); 6 // A non-empty value from external configuration will override it. 7 // Set -Dsentry.dsn Java option or SENTRY_DSN environment variable to customize DSN. 8 options.setEnableExternalConfiguration(true); 9 10 // Set -Dsentry.environment=production in releases to switch telemetry environment. 11 // If not changed, "dev" will be used as the environment. 12 options.setEnvironment("dev"); 13 14 options.setSendDefaultPii(true); 15 options.setTracesSampleRate(1.0); 16 options.getLogs().setEnabled(true); 17 options.setRelease("arkpets@"+ Const.appVersion); 18 }); 19 } catch (Exception | LinkageError e) { 20 Logger.warn("Telemetry", "Failed to initialize the Sentry SDK, telemetry is unavailable. " + e); 21 } 22 sdkAvailable = Sentry.isEnabled(); 23 if (!sdkAvailable) 24 Logger.info("Telemetry", "Sentry SDK is not active due to missing, invalid or disabled configuration, telemetry is unavailable"); 25}

Source: SentryHelper.java

初始化的关键设计:

设计点意图
setDsn("") 先置空Sentry SDK 对空 DSN 的语义是优雅禁用而非报错,保证未配置 DSN 的构建(如源码自编译)不会崩溃
setEnableExternalConfiguration(true)允许 -Dsentry.dsn JVM 参数或 SENTRY_DSN 环境变量覆盖空 DSN,无需重新编译即可接入生产项目
setEnvironment("dev")开发环境为默认值;发布渠道通过 -Dsentry.environment=production 显式切换
catch (Exception | LinkageError)Sentry 类库缺失时抛出的 LinkageError(如 NoClassDefFoundError)也会被捕获,SDK 依赖被完全可选化
sdkAvailable = Sentry.isEnabled()初始化后立即查询 SDK 真实状态,作为后续所有上报的守卫条件

DSN 的构建期注入(build.gradle)

groovy
1def sentryDsn = (project.findProperty("SENTRY_DSN") ?: "").trim() 2 3// Runs the app without debug. 4tasks.register("run", JavaExec) { 5 // ... 6 jvmArgs += "-Dfile.encoding=UTF-8" 7 if (sentryDsn) 8 jvmArgs += "-Dsentry.dsn=${sentryDsn}" 9 // ... 10}

Source: build.gradle

groovy
1 '--java-options', '-Dfile.encoding=UTF-8', 2 '--java-options', '-Dsentry.environment=production' 3 ] 4 if (sentryDsn) { 5 commands << '--java-options' 6 commands << "-Dsentry.dsn=${sentryDsn}" 7 }

Source: build.gradle

构建层从 Gradle 属性 SENTRY_DSN(gradle.properties 或 -PSENTRY_DSN=...)读取 DSN,仅在非空时注入 JVM 参数,分别覆盖 run/debug 开发任务与 jpackage 安装包生成命令。production 环境标记只在安装包路径注入,因此源码运行天然标记为 dev。

WAL 消费主流程

java
1public static void consumePendingWal() { 2 if (!sdkAvailable) { 3 Logger.debug("Telemetry", "Sentry SDK unavailable, now keeping existing WAL files"); 4 return; 5 } 6 7 // If telemetry features were disabled, delete all WAL files and skip consuming. 8 if (!enable) { 9 Logger.debug("Telemetry", "Telemetry disabled, now deleting existing WAL files"); 10 for (File file : WalReader.listWalFiles()) 11 if (!file.delete()) 12 Logger.warn("Telemetry", "Failed to delete existing WAL file " + file.getName()); 13 return; 14 } 15 16 for (File file : WalReader.listWalFiles()) { 17 // Skip WAL file whose process is still alive 18 if (ProcessHandle.of(WalReader.parsePid(file)).map(ProcessHandle::isAlive).orElse(false)) 19 continue; 20 try (WalReader reader = WalReader.open(file)) { 21 Logger.debug("Telemetry", "Consuming WAL file " + file.getName()); 22 consumeWalRecords(reader.readAll()); 23 } catch (IOException e) { 24 Logger.warn("Telemetry", "Failed to consume WAL file " + file.getName() + ", will retry later"); 25 continue; 26 } 27 if (!file.delete()) 28 Logger.warn("Telemetry", "Failed to delete consumed WAL file " + file.getName()); 29 } 30}

Source: SentryHelper.java

逐行解读这段核心逻辑的三重守卫:

  1. !sdkAvailable → 保留 WAL 文件。SDK 不可用是暂时性状态(可能是本轮构建未配置 DSN),此时保留 WAL 以便未来版本消费,不销毁数据。
  2. !enable → 删除 WAL 文件。用户明确关闭遥测属于永久性意愿,此时必须清理本地 WAL,避免数据无限堆积(注意这里与上一条的语义差异)。
  3. 进程存活检查。WAL 文件按 PID 命名,WalReader.parsePid(file) 提取 PID 后通过 Java 9+ 的 ProcessHandle.of(pid).isAlive 判断该 Core 进程是否仍在运行。存活的进程还在持续写入自己的 WAL,此时消费会产生不完整/错误的会话结论,因此跳过。
  4. try-with-resources + 失败重试。单个 WAL 文件 IOException 时仅 continue 到下一个文件,不删除该文件,下次启动时会重试("will retry later")。消费成功的文件立即删除,保证 at-most-once 语义。

会话级别判定算法(reportSession)

java
1private static void reportSession(WalData data) { 2 WalCoreHeartbeatCodec.WalHeartbeatEvent lastModelHeartbeat = data.lastModelHeartbeat(); 3 if (lastModelHeartbeat != null) { 4 long endTimeMillis; 5 SentryLogLevel level; 6 if (data.exceptionRecord() != null) { 7 endTimeMillis = data.exceptionRecord().timestamp(); 8 level = SentryLogLevel.ERROR; 9 try { 10 Exception exception = WalExceptionCodec.INSTANCE.decode(data.exceptionRecord().payload()); 11 SentryEvent event = new SentryEvent(exception); 12 event.setTimestamp(new Date(data.exceptionRecord().timestamp())); 13 Sentry.captureEvent(event); 14 } catch (IOException ignored) { 15 } 16 } else if (lastModelHeartbeat.stopped()) { 17 endTimeMillis = data.lastModelHeartbeatTime(); 18 level = SentryLogLevel.INFO; 19 } else { 20 endTimeMillis = data.lastModelHeartbeatTime(); 21 level = SentryLogLevel.WARN; 22 } 23 reportModelSession(lastModelHeartbeat, endTimeMillis, level); 24 } else if (data.lastDesktopHeartbeat() != null) { 25 reportDesktopSession( 26 data.lastDesktopHeartbeat(), 27 data.lastDesktopHeartbeatTime(), 28 data.lastDesktopHeartbeat().stopped() ? SentryLogLevel.INFO : SentryLogLevel.WARN 29 ); 30 } 31}

Source: SentryHelper.java

这是整个子系统的判定核心,三分支状态机:

结束方式Sentry 级别会话终点时间判定依据
WAL 中存在异常记录ERROR异常记录的时间戳存在 WalExceptionCodec 记录即视为异常终止,同时还原 Exception 并 Sentry.captureEvent() 补捕获
心跳标记 stopped == trueINFO最后一条心跳时间戳Core 正常调用退出钩子写入了停止标记
心跳无停止标记(进程被杀)WARN最后一条心跳时间戳强杀/断电等非正常退出,心跳停在半路

设计意图:Sentry 平台上 ERROR 会触发告警,WARN 用于观察崩溃疑似场景,INFO 是正常会话基线。这个分类让"用户强杀进程"和"代码抛异常"在数据面板上天然分离。

Loading diagram...

注意 event.setTimestamp(new Date(...)) 的细节:异常事件的时间戳被显式回填为 WAL 帧头时间戳,而不是 SDK 默认的上报时间。这保证 Sentry 面板上崩溃事件出现在发生时刻,跨进程重放不会造成时间轴失真。

Usage Examples

模型会话上报(model_session)

java
1private static void reportModelSession(WalCoreHeartbeatCodec.WalHeartbeatEvent heartbeat, long endTimeMillis, SentryLogLevel level) { 2 if (!enable) return; 3 long durationSeconds = Math.max(0, (endTimeMillis - heartbeat.startTime()) / 1000); 4 Sentry.logger().log( 5 level, 6 SentryLogParameters.create( 7 new SentryLongDate(endTimeMillis * 1_000_000L), 8 SentryAttributes.of( 9 SentryAttribute.stringAttribute("core.character_asset", normalizeAsset(heartbeat.asset())), 10 SentryAttribute.integerAttribute("core.session_duration", (int) durationSeconds) 11 ) 12 ), 13 "MODEL_SESSION" 14 ); 15 Logger.debug("Telemetry", "Uploaded a MODEL_SESSION event"); 16}

Source: SentryHelper.java

使用 Sentry 的 Logs 通道(Sentry.logger().log())而非 Events 通道上报会话,携带两个属性:归一化的角色资源路径(normalizeAsset 把 Windows 反斜杠统一为 /,避免同一资源因路径分隔符分裂成两个维度值)和会话秒数。Math.max(0, ...) 防御性钳位避免时钟回拨产生负时长。

Core 性能指标上报(Metrics:Gauge + Distribution)

java
1private static void reportCorePerformance( 2 CorePerformanceSnapshot performance, 3 String asset, 4 long timestampMillis 5) { 6 if (!enable || performance == null) return; 7 8 SentryAttributes resourceAttributes = SentryAttributes.of( 9 SentryAttribute.stringAttribute("core.character_asset", normalizeAsset(asset)) 10 ); 11 reportGauge( 12 "core.memory.heap_used", 13 (double) performance.heapUsedBytes(), 14 MetricsUnit.Information.BYTE, 15 timestampMillis, 16 resourceAttributes 17 ); 18 if (performance.processCpuRatio() != null) { 19 reportGauge( 20 "core.cpu.process_usage", 21 performance.processCpuRatio(), 22 MetricsUnit.Fraction.RATIO, 23 timestampMillis, 24 resourceAttributes 25 ); 26 } 27 28 for (CorePerformanceSnapshot.RenderMetrics metrics : performance.renderMetrics()) { 29 SentryAttributes attributes = SentryAttributes.of( 30 SentryAttribute.stringAttribute("core.character_asset", normalizeAsset(asset)), 31 SentryAttribute.integerAttribute("core.render.width", metrics.width()), 32 SentryAttribute.integerAttribute("core.render.height", metrics.height()), 33 SentryAttribute.integerAttribute("core.render.pixels", (int) metrics.pixels()) 34 ); 35 reportDistribution( 36 "core.render.callback_time", 37 metrics.renderTimeAverageMillis(), 38 MetricsUnit.Duration.MILLISECOND, 39 timestampMillis, 40 attributes 41 ); 42 reportDistribution( 43 "core.render.callback_time_ppx", 44 metrics.renderTimeAveragePpx(), 45 null, 46 timestampMillis, 47 attributes 48 ); 49 50 reportDistribution("core.render.fps", metrics.fps(), null, timestampMillis, attributes); 51 } 52 Logger.debug("Telemetry", "Uploaded a core performance metrics"); 53}

Source: SentryHelper.java

java
1private static void reportDistribution( 2 String name, 3 double value, 4 String unit, 5 long timestampMillis, 6 SentryAttributes attributes 7) { 8 if (!Double.isFinite(value)) return; 9 Sentry.metrics().distribution( 10 name, 11 value, 12 unit, 13 SentryMetricsParameters.create(new SentryLongDate(timestampMillis * 1_000_000L), attributes) 14 ); 15}

Source: SentryHelper.java

指标体系一览:

指标名类型单位维度说明
core.memory.heap_usedGaugeBYTEcharacter_assetCore 进程堆内存占用
core.cpu.process_usageGaugeRATIOcharacter_assetCore 进程 CPU 占比(可能为 null,判空后跳过)
core.render.callback_timeDistributionMILLISECONDcharacter_asset, width, height, pixels平均单帧渲染耗时
core.render.callback_time_ppxDistribution无(比值)同上每像素渲染耗时,衡量渲染效率与分辨率无关的归一化指标
core.render.fpsDistribution无同上帧率

reportDistribution 中的 if (!Double.isFinite(value)) return; 是重要的边界防御:libGDX 采集到的 NaN/Infinity(例如除零或采样窗口为零)不会传给 SDK,避免污染 Sentry 端的聚合统计。

配置快照上报(CONFIG)

java
1private static SentryAttributes configAttributes(Map<String, Object> config) { 2 SentryAttributes attributes = SentryAttributes.of(); 3 for (Map.Entry<String, Object> entry : config.entrySet()) { 4 String key = "core_config." + entry.getKey(); 5 Object value = entry.getValue(); 6 if (value instanceof Boolean) { 7 attributes.add(SentryAttribute.booleanAttribute(key, (Boolean) value)); 8 } else if (value instanceof Integer) { 9 attributes.add(SentryAttribute.integerAttribute(key, (Integer) value)); 10 } else if (value instanceof Double) { 11 attributes.add(SentryAttribute.doubleAttribute(key, (Double) value)); 12 } else if (value instanceof String) { 13 attributes.add(SentryAttribute.stringAttribute(key, (String) value)); 14 } 15 } 16 return attributes; 17}

Source: SentryHelper.java

配置快照在 Core 启动时经反射一次性采集 ArkConfig 公有标量字段并作为独立 WAL 记录落盘,消费时按 Java 类型分派到对应的 Sentry 属性构造器,统一加 core_config. 前缀。设计意图:携带会话发生时的配置而非上报时的值,便于在 Sentry 面板上按配置分组定位问题(例如某配置组合下渲染指标异常)。

用户日志反馈提交(Feedback)

java
1public static boolean captureLogFeedback(List<String> fileList) { 2 if (!sdkAvailable) { 3 Logger.warn("Telemetry", "Sentry SDK unavailable, unable to upload the user log feedback"); 4 return false; 5 } 6 SentryId sentryId = Sentry.feedback().capture( 7 new Feedback("User uploaded ArkPets log files."), 8 Hint.withAttachments(fileList.stream().map(Attachment::new).toList()) 9 ); 10 if (SentryId.EMPTY_ID.equals(sentryId)) { 11 Logger.warn("Telemetry", "Failed to submit a user log feedback to the Sentry SDK"); 12 return false; 13 } 14 Logger.info("Telemetry", "Submitted a user log feedback, Sentry ID is " + sentryId); 15 return true; 16}

Source: SentryHelper.java

这是用户主动上传日志文件的通道(区别于前述自动遥测):把本地日志文件包装为 Sentry Attachment,通过 feedback().capture() 提交。注意它只检查 sdkAvailable 而不检查 enable——因为这是用户的显式授权动作,不应被自动遥测开关拦截。返回布尔值供 UI 层向用户展示提交结果,SentryId.EMPTY_ID 是 SDK 表示"提交被拒"的哨兵值。

Configuration Options

SentryHelper 运行时配置(JVM 参数 / 环境变量)

选项类型默认值说明
sentry.dsnstring (系统属性) / 环境变量 SENTRY_DSN""(SDK 优雅禁用)Sentry 项目 DSN。经 setEnableExternalConfiguration(true) 启用外部覆盖;空值时 SDK 不激活、不产生网络流量
sentry.environmentstring (系统属性)"dev"遥测环境标记。发布安装包经 jpackage 注入 production
enable(SentryHelper.setEnable)booleanfalse用户级遥测开关,实际生效需 enable && sdkAvailable
sdkAvailable(运行时派生)booleanSentry.isEnabled()SDK 初始化结果,决定 WAL 是保留还是删除

Sentry SDK 初始化选项(Sentry.init 内联设置)

选项类型设定值设计意图
setDsnstring""先禁用,等外部配置覆盖
setEnableExternalConfigurationbooleantrue允许 -Dsentry.dsn / SENTRY_DSN 生效
setEnvironmentstring"dev"默认开发环境
setSendDefaultPiibooleantrue发送默认个人识别信息(IP 等),用于 Sentry 端去重与影响面统计
setTracesSampleRatedouble1.0全量采样性能追踪
getLogs().setEnabledbooleantrue启用 Logs 通道(MODEL_SESSION 等事件依赖此通道)
setReleasestring"arkpets@" + Const.appVersion版本标记,按版本聚合问题

构建期配置(desktop/build.gradle)

选项类型说明
SENTRY_DSN (Gradle property)string构建时 DSN 注入源:gradle.properties 或 -PSENTRY_DSN=...;空值(trim 后)则完全不注入任何 sentry.dsn JVM 参数
run / debug 任务—开发运行时通过 jvmArgs += "-Dsentry.dsn=..." 注入
jpackage 安装包—通过 --java-options 注入 DSN 与 -Dsentry.environment=production

API Reference

所有方法均为 SentryHelper 类的静态方法,无实例状态。

init(): void

初始化 Sentry SDK。以空 DSN 调用 Sentry.init 并启用外部配置覆盖,随后将 Sentry.isEnabled() 结果写入 sdkAvailable。初始化失败(含 LinkageError)仅记录警告不抛出。

setEnable(boolean enable): void

设置用户遥测开关。实际赋值 enable && sdkAvailable——SDK 不可用时强制为 false,保证开关语义与 SDK 现实一致。

beginDesktopSession(): void

为 Desktop 进程自身创建 HeartbeatSession(使用 WalDesktopHeartbeatCodec.INSTANCE 与其事件工厂引用)。幂等性:直接覆盖 desktopSession 字段,重复调用会丢弃旧会话引用。

endDesktopSession(): void

结束 Desktop 会话,委托 desktopSession.finish();字段为 null 时安全空操作。

consumePendingWal(): void

扫描全部 WAL 文件并消费。三重守卫(SDK 可用性 / 用户开关 / 进程存活检查)见「Core Flow」一节。单文件 IOException 时保留文件下次重试,成功消费后删除文件。

captureLogFeedback(List<String> fileList): boolean

用户主动提交日志文件作为 Sentry Feedback 附件。

Parameters:

  • fileList (List<String>): 本地日志文件绝对路径列表,逐个包装为 Attachment

Returns: boolean — true 表示 SDK 返回了有效 SentryId;false 表示 SDK 不可用或提交被拒(SentryId.EMPTY_ID)

isEnable() / isSdkAvailable(): boolean

只读访问两个状态标志,供 UI 或其他模块查询遥测状态。

Failure Modes, Edge Cases & Concurrency

失败模式矩阵

失败场景检测方式处理行为后果
Sentry 类库缺失catch (Exception | LinkageError)记 warn,sdkAvailable = false遥测完全不可用,WAL 被保留
DSN 未配置 / 无效Sentry.isEnabled() == false记 info同上
用户关闭遥测(enable == false)显式守卫删除所有 WAL 文件无数据堆积,符合用户意愿
Core 进程仍存活ProcessHandle.isAlivecontinue 跳过该文件该 WAL 留待进程退出后的下次启动
单个 WAL 文件 IO 损坏IOException记 warn + continue,不删除文件下次启动重试
WAL 文件删除失败file.delete() == false记 warn文件残留,下次会重复上报该 WAL(需要人工清理)
指标值 NaN / InfinityDouble.isFinite 检查静默丢弃该次 distribution不污染 Sentry 聚合
心跳解码失败catch (IOException ignored)跳过该条记录部分数据缺失但会话判定不中断
CPU 采样为 nullprocessCpuRatio() != null 判空跳过 core.cpu.process_usage其余指标照常上报
反馈提交被拒SentryId.EMPTY_ID 比较返回 false 给 UI用户可见提交失败

边界条件

  • 时钟回拨:durationSeconds = Math.max(0, ...) 钳位,会话时长不会为负;
  • 路径分隔符差异:normalizeAsset 将 \ 替换为 /,Windows 与 Unix 下同一资源归一为同一维度值;
  • 时间戳回填:所有日志/指标/事件的时间戳均来自 WAL 帧头(new SentryLongDate(timestampMillis * 1_000_000L),毫秒转纳秒),保证数据代表事件发生时刻而非重放时刻;
  • 空 WAL / 无任何记录:collectWalRecords 返回全 null 的 WalData,reportSession 两个分支都不命中,静默跳过。

并发与一致性

  • at-most-once 语义:消费成功即删除文件,与"进程存活跳过"配合,同一 WAL 不会被两个 Desktop 实例并发消费(每个 Core PID 对应一个文件)。但"消费成功 + 删除失败"窗口内可能产生下次重复上报——代码接受这一弱一致,仅记 warn。
  • 静态可变状态:enable / sdkAvailable / desktopSession 为非 volatile 静态字段。实际调用序列是 DesktopLauncher 主流程的 init() → setEnable() → beginDesktopSession(),运行在 JavaFX Application Thread 上串行执行,因此不构成竞态;但如果未来从多线程调用(例如后台线程消费 WAL)需要外加同步。

Performance & Operational Notes

  • 启动时机:consumePendingWal() 在 Desktop 启动早期执行,WAL 重放是批量、一次性操作,collectWalRecords 单次遍历所有记录(O(n))完成归类,避免多遍扫描。
  • 网络异步性:Sentry Java SDK 的上报默认异步(内部队列 + 后台发送线程),Sentry.logger().log() / metrics() 调用不阻塞 UI 线程;即使 SDK 发送失败也有自身重试与缓存机制(SDK 层职责,不在本类)。
  • 数据体量控制:性能指标只上报每条心跳的性能快照(Distribution 支持服务端聚合),异常事件只在 WAL 含异常记录时上报一次,无重复事件风暴。
  • 运维排查入口:本地 Logger("Telemetry" tag)的 debug/warn/info 日志是诊断遥测链路的第一手材料——例如 "Consuming WAL file ..."、"will retry later"、"Sentry SDK unavailable" 等消息可直接对应到上述代码路径。

Extension Points

  • 新增遥测事件类型:新增一个 Wal*Codec(写入端)+ 在 collectWalRecords 中新增一个 else if 分支 + 新增一个 reportXxx 私有方法。WalData record 需同步扩展字段。这是该类最自然的扩展路径,全部集中在 collectWalRecords 单方法内。
  • 新增指标:reportGauge / reportDistribution 已是通用封装(名称、值、单位、时间戳、属性五元组),直接复用即可,注意传入有限 double 值。
  • 切换环境标记:无需改代码,-Dsentry.environment=<name> 即可,配合构建脚本注入。
  • 上报通道选择:会话/配置/系统信息走 Logs 通道,性能走 Metrics 通道,崩溃走 Events 通道,反馈走 Feedback 通道——四通道已在类内形成清晰分层,新增数据时按此约定选型。

Sources

(2 files)
desktop/src/cn/harryh/arkpets/utils