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 平台。
这种设计带来三个关键收益:
- 进程隔离:Core 保持无网络依赖,可脱离 Desktop 独立运行(例如开发调试场景),其生命周期数据不因进程退出而丢失;
- 崩溃可观测:Core 崩溃(未捕获异常)时异常对象已被序列化进 WAL,Desktop 下次消费 WAL 时将其还原为
SentryEvent补报,时间戳保留为崩溃发生时刻而非上报时刻; - 优雅降级:DSN 为空时 SDK 完全禁用,WAL 会被直接清理删除,不产生任何网络流量。
SentryHelper 同时维护两个语义不同的布尔标志:
sdkAvailable:SDK 是否成功初始化(DSN 有效且类库存在);enable:用户遥测开关。setEnable()中enable && sdkAvailable的短路设计保证 SDK 不可用时开关永远无法打开。
Architecture
架构要点解读:
- 单向数据流: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 的内部结构(私有方法分组与数据聚合类型)如下:
WalData 是核心的聚合中间结构:collectWalRecords() 单次遍历 WAL 记录流,把同一文件中的心跳、异常、配置、系统信息分别归位——心跳只保留最后一条用于会话判定,而全部心跳保留在 modelHeartbeats 列表中用于逐条上报性能指标。
Core Flow
SDK 初始化与外部配置覆盖
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)
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
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 消费主流程
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
逐行解读这段核心逻辑的三重守卫:
!sdkAvailable→ 保留 WAL 文件。SDK 不可用是暂时性状态(可能是本轮构建未配置 DSN),此时保留 WAL 以便未来版本消费,不销毁数据。!enable→ 删除 WAL 文件。用户明确关闭遥测属于永久性意愿,此时必须清理本地 WAL,避免数据无限堆积(注意这里与上一条的语义差异)。- 进程存活检查。WAL 文件按 PID 命名,
WalReader.parsePid(file)提取 PID 后通过 Java 9+ 的ProcessHandle.of(pid).isAlive判断该 Core 进程是否仍在运行。存活的进程还在持续写入自己的 WAL,此时消费会产生不完整/错误的会话结论,因此跳过。 - try-with-resources + 失败重试。单个 WAL 文件
IOException时仅continue到下一个文件,不删除该文件,下次启动时会重试("will retry later")。消费成功的文件立即删除,保证 at-most-once 语义。
会话级别判定算法(reportSession)
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 == true | INFO | 最后一条心跳时间戳 | Core 正常调用退出钩子写入了停止标记 |
| 心跳无停止标记(进程被杀) | WARN | 最后一条心跳时间戳 | 强杀/断电等非正常退出,心跳停在半路 |
设计意图:Sentry 平台上 ERROR 会触发告警,WARN 用于观察崩溃疑似场景,INFO 是正常会话基线。这个分类让"用户强杀进程"和"代码抛异常"在数据面板上天然分离。
注意 event.setTimestamp(new Date(...)) 的细节:异常事件的时间戳被显式回填为 WAL 帧头时间戳,而不是 SDK 默认的上报时间。这保证 Sentry 面板上崩溃事件出现在发生时刻,跨进程重放不会造成时间轴失真。
Usage Examples
模型会话上报(model_session)
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)
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
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_used | Gauge | BYTE | character_asset | Core 进程堆内存占用 |
core.cpu.process_usage | Gauge | RATIO | character_asset | Core 进程 CPU 占比(可能为 null,判空后跳过) |
core.render.callback_time | Distribution | MILLISECOND | character_asset, width, height, pixels | 平均单帧渲染耗时 |
core.render.callback_time_ppx | Distribution | 无(比值) | 同上 | 每像素渲染耗时,衡量渲染效率与分辨率无关的归一化指标 |
core.render.fps | Distribution | 无 | 同上 | 帧率 |
reportDistribution 中的 if (!Double.isFinite(value)) return; 是重要的边界防御:libGDX 采集到的 NaN/Infinity(例如除零或采样窗口为零)不会传给 SDK,避免污染 Sentry 端的聚合统计。
配置快照上报(CONFIG)
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)
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.dsn | string (系统属性) / 环境变量 SENTRY_DSN | ""(SDK 优雅禁用) | Sentry 项目 DSN。经 setEnableExternalConfiguration(true) 启用外部覆盖;空值时 SDK 不激活、不产生网络流量 |
sentry.environment | string (系统属性) | "dev" | 遥测环境标记。发布安装包经 jpackage 注入 production |
enable(SentryHelper.setEnable) | boolean | false | 用户级遥测开关,实际生效需 enable && sdkAvailable |
sdkAvailable(运行时派生) | boolean | Sentry.isEnabled() | SDK 初始化结果,决定 WAL 是保留还是删除 |
Sentry SDK 初始化选项(Sentry.init 内联设置)
| 选项 | 类型 | 设定值 | 设计意图 |
|---|---|---|---|
setDsn | string | "" | 先禁用,等外部配置覆盖 |
setEnableExternalConfiguration | boolean | true | 允许 -Dsentry.dsn / SENTRY_DSN 生效 |
setEnvironment | string | "dev" | 默认开发环境 |
setSendDefaultPii | boolean | true | 发送默认个人识别信息(IP 等),用于 Sentry 端去重与影响面统计 |
setTracesSampleRate | double | 1.0 | 全量采样性能追踪 |
getLogs().setEnabled | boolean | true | 启用 Logs 通道(MODEL_SESSION 等事件依赖此通道) |
setRelease | string | "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.isAlive | continue 跳过该文件 | 该 WAL 留待进程退出后的下次启动 |
| 单个 WAL 文件 IO 损坏 | IOException | 记 warn + continue,不删除文件 | 下次启动重试 |
| WAL 文件删除失败 | file.delete() == false | 记 warn | 文件残留,下次会重复上报该 WAL(需要人工清理) |
| 指标值 NaN / Infinity | Double.isFinite 检查 | 静默丢弃该次 distribution | 不污染 Sentry 聚合 |
| 心跳解码失败 | catch (IOException ignored) | 跳过该条记录 | 部分数据缺失但会话判定不中断 |
| CPU 采样为 null | processCpuRatio() != 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私有方法。WalDatarecord 需同步扩展字段。这是该类最自然的扩展路径,全部集中在collectWalRecords单方法内。 - 新增指标:
reportGauge/reportDistribution已是通用封装(名称、值、单位、时间戳、属性五元组),直接复用即可,注意传入有限 double 值。 - 切换环境标记:无需改代码,
-Dsentry.environment=<name>即可,配合构建脚本注入。 - 上报通道选择:会话/配置/系统信息走 Logs 通道,性能走 Metrics 通道,崩溃走 Events 通道,反馈走 Feedback 通道——四通道已在类内形成清晰分层,新增数据时按此约定选型。
Related Links
- SentryHelper.java — 本页核心实现
- desktop/build.gradle — DSN 构建期注入
- docs/Telemetry.md — 遥测架构与隐私政策总览
- Sentry Java SDK — SDK 官方文档(外部)