本文介绍接入用户体验监控 Android SDK 后,如何验证各监控能力的数据上报是否成功。
前提条件
崩溃(
crashPluginConfig)、ANR(anrPluginConfig)、卡顿(lagPluginConfig)、启动(launchPluginConfig)、网络(httpPluginConfig)、挣扎检测(strugglePluginConfig)、设备信息(devicePluginConfig)默认开启。用户行为(
behaviorPluginConfig)、Session Replay(replayPluginConfig)默认关闭,需显式开启。验证期间建议设置
.logLevel(TDEM.LEVEL_DEBUG),方便观察上报请求与运行日志。日志输出开关由 SDK 内部固定开启,logLevel 只控制输出级别下限,默认 WARN。步骤1:检查上报请求
初始化后,SDK 会发出下列请求(Session Replay 默认关闭,开启后才有回放上报)。HTTP 2xx / 204 均视为成功:
端点 | 内容 |
POST {url}/api/v1/config | 远程配置,仅在返回启用时才挂载各监控插件 |
POST {url}/api/v1/collect/events | 标准事件(崩溃 / ANR / 卡顿 / 启动 / 网络 / 行为 / 挣扎 / 自定义事件等) |
POST {url}/api/v1/collect/replay | Session Replay 录屏数据 |
url 填站点根地址即可,SDK 会自行拼接路径,无需手动补 /api/v1。步骤2:验证各监控能力
崩溃监控
初始化后,可在页面中主动触发 Java 异常来验证:
// 触发未捕获的 Java 异常throw RuntimeException("tdem verify crash")
Native 崩溃(SIGSEGV / SIGABRT)通过 Signal Handler 捕获,可在 demo 中触发验证。
ANR 监控
主线程 sleep 或死循环可触发 SIGQUIT ANR 检测:
Thread.sleep(10_000) // 主线程阻塞触发 ANR
卡顿监控
主线程执行长时间任务,超过
lagThresholdMs(默认1000ms)即可触发卡顿上报。启动监控
启动监控默认开启,通过 ASM 编译期插桩实现,早于 SDK 初始化执行,不依赖
TDEM.init 的调用时机。启动类型共6种:
first_start:首次安装。upgrade_start:版本升级后首次启动。cold_start:进程被杀后重新启动。warm_start:从后台切回且停留超过阈值。preheat_start:从后台切回且停留不超过阈值,即热启动。prepare_start:进程由 Service / BroadcastReceiver / ContentProvider 触发。后台停留阈值由
warmStartThresholdMs 控制,默认 180000ms。默认在首个 Activity resume 后自动封口并上报事件
app_launch。若配置 .launchPluginConfig(LaunchPluginConfig(manualEndEnabled = true)),则改为等待业务调用 TDEM.endLaunch() 后才封口。首次安装后的第一次启动不采集:启动监控的开关通过 SharedPreferences 持久化,第一次启动时开关尚未写入,SDK 初始化成功写入后,第二次及后续启动才正式开启。验证启动数据需要至少冷启动两次。
验证方式:冷启动 App 后观察 Logcat 中的启动日志。
网络监控
网络监控默认开启,SDK 自动监听 OkHttp3 / HttpURLConnection / SSE 请求并上报耗时与错误。可通过配置
slowThresholdMs 验证慢请求上报。大模型 SSE 体验监控
大模型应用普遍用 SSE(Server-Sent Events)流式返回内容,普通 HTTP 请求指标看不出「首字慢」、「长时间不吐字」、「流中断」这类问题。SDK 在网络监控之上对 SSE 流单独采集,上报
sse_call 事件,控制台据此计算 token 吞吐率、首次 token 耗时、错误率、卡死率、流畅度等指标。该能力默认开启,不需要额外接入。接入前提:业务侧使用 OkHttp 的
EventSource(okhttp-sse)发起 SSE 请求,并传入 EventSourceListener。SDK 在编译期对 okhttp3.internal.sse.RealEventSource 的构造过程插桩,同时把业务中继承 okhttp3.sse.EventSourceListener 的监听器挂到 TDEMEventSourceListener 下。用其他方式自行拼接、手工解析流式响应的写法不采集。开关:是否采集由编译期
tdem { instrumentSSE = true } 决定,默认 true;关闭后不再插桩,也就不会产生 sse_call。运行期 httpPluginConfig.sseEnabled 默认 true,当前版本仅用于插件初始化时的状态日志,不参与采集判定。停滞阈值:
httpPluginConfig.sseStallThresholdMs,默认60000ms。超过该时长没有新消息,本次连接按停滞处理并上报 stalled。模型标识:
TDEMConfiguration.Builder.llmVersion(...) 设置的值随事件上报为 model_version;未设置时上报 unknown。resource_type 为 sse 的 sse_call 事件,字段分三组:连接:
http_method、http_status、outcome(success / http_error / transport_error / cancelled / stalled)、server_ip、model_version。网络耗时:
duration、dns_time、tcp_time、ssl_time、request_time、response_time、ttfb。流式特征:
message_count(消息条数)、time_to_first_message(首条消息耗时)、max_message_gap(最大消息间隔,消息数不少于 2 时才有)、last_message_offset(最后一条消息相对请求发起的时间偏移),以及 6 档间隔计数 gap_lt_500_count / gap_500_999_count / gap_1000_2999_count / gap_3000_9999_count / gap_10000_59999_count / gap_ge_60000_count。验证方式:在控制台 大模型体验 查看 token 吞吐率、首次 token 耗时、错误率、卡死率、流畅度,维度分布中的模型版本与
llmVersion 对应;或直接确认设备已产生 sse_call 上报。移动拨测
移动拨测从真实设备侧发起网络探测,用于定位域名或端口在特定网络环境下的可达性、丢包与路由问题。SDK 已内置该能力并默认开启,不需要额外接入或配置,探测任务由控制台下发、客户端拉取后执行,结果以
dial_probe(连通性探测)和 dial_mtr(路由追踪)事件上报。2. 在左侧菜单栏中选择 移动拨测。
3. 单击 新建任务,填写任务名称、类型(TCP / ICMP)、执行策略与探测地址后保存。
任务保存后不会立即生效,客户端每 10 分钟左右拉取一次任务,最迟一个拉取周期后按新定义执行。同一设备同时最多执行 3 个不同拨测目标,超出部分会被跳过。
行为监控
开启行为监控后,SDK 自动采集以下行为事件:页面(
page_view)、触摸(click / double_tap / long_press / scroll / pinch_zoom)、按键与系统(key_press / screen_toggle / screen_rotation)、输入与表单(text_input / form_focus / form_blur / form_change)。val config = TDEMConfiguration.Builder(this).id("your-project-key").url("https://dem.rumt-zh.com").behaviorPluginConfig(BehaviorPluginConfig(enabled = true)).build()TDEM.init(config)
import android.app.Application;import com.tencent.tdem.TDEM;import com.tencent.tdem.TDEMConfiguration;import com.tencent.tdem.behavior.BehaviorPluginConfig;TDEMConfiguration config = new TDEMConfiguration.Builder(this).id("your-project-key").url("https://dem.rumt-zh.com").behaviorPluginConfig(new BehaviorPluginConfig(true)).build();TDEM.init(config);
手动逻辑页可通过
startPage / leavePage(LIFO)声明:TDEM.getInstance()?.startPage("checkout")// ... 页面逻辑 ...TDEM.getInstance()?.leavePage()
import com.tencent.tdem.TDEM;TDEM.getInstance().startPage("checkout");// ... 页面逻辑 ...TDEM.getInstance().leavePage();
Compose 监控
Compose 的两类能力都要先引入
com.tencent.tdem:tdem-behavior-compose 这个必需依赖(版本与 tdem 一致),再按需开启,且都依赖行为监控已打开(behaviorPluginConfig(enabled = true))。本节示例均为 Kotlin。
@Composable 函数只能由 Kotlin 编译器产出,下列调用点都位于 Composable 作用域内,SDK 也未提供等价的 Java 入口;Java 项目使用本能力时,这部分代码同样需要写在 Kotlin 文件中。页面流转。两条路径任选其一,效果等价;同时启用也不会重复上报。
路径一:代码中显式包裹。适合不改动构建配置的项目,在创建
NavHostController 时调用 withTDEMPageTracking() 即可:import androidx.compose.runtime.Composableimport androidx.navigation.compose.rememberNavControllerimport com.tencent.tdem.behavior.compose.withTDEMPageTracking@Composablefun AppNavHost() {val navController = rememberNavController().withTDEMPageTracking()// NavHost(navController = navController, ...)}
路径二:编译期自动插桩。在模块级
build.gradle 中开启,插件会在每个 rememberNavController() 调用之后自动插入同一句 withTDEMPageTracking(),业务代码无需改动:// 模块级 build.gradletdem {instrumentComposeNavigation = true}
切换 Navigation destination 后,SDK 会把 destination 的 route 作为当前自动页面并上报一条
page_view,page_source 为 compose;destination 没有 route 时回退取资源的 entry name。组件标签。引入 Kotlin 编译器插件后,
@Composable 函数会自动获得以函数名为值的标签;需要覆盖时手动指定。import androidx.compose.material3.Buttonimport androidx.compose.material3.Textimport com.tencent.tdem.behavior.compose.tdemTagButton(modifier = Modifier.tdemTag("buy_button")) {Text("购买")}
点击该 Button 后,上报的
click 事件中 element_id / target_element 即为 buy_button。注意:
开启了
instrumentComposeNavigation 却没引入 tdem-behavior-compose 时,插桩后的字节码在运行期找不到目标类,会抛 NoClassDefFoundError(构建期不报错)。只引入
tdem-behavior-compose、不引入 Kotlin 编译器插件时,自动标签不生效;此时只能手动调用 tdemTag。TDEMTag 与 Compose 原生的 testTag 同级读取,若已有 UI 测试标签,可直接复用 Modifier.testTag(),效果一致。挣扎检测
挣扎检测默认开启,产出5类子事件:
rage_click:同一元素1秒内连续点击5次。dead_click:点击后3秒内既无页面跳转也无接口调用,判定为无响应。error_click:点击后3秒内出现接口错误或 JS / Promise 错误。long_focus_time:表单控件持续聚焦超过20秒。back_forward:10秒内连续返回3次。规则阈值内部写死,接入方不可修改。可通过
clickRulesEnabled / formRulesEnabled / navigationRulesEnabled 分别关闭点击类、表单类与返回类规则。需要注意的是,默认配置下通常只有
back_forward 能被触发。rage_click / dead_click / error_click 依赖点击事件,long_focus_time 依赖表单聚焦事件,而这些事件全部由用户行为监控(behaviorPluginConfig)产生,行为监控默认关闭,因此这4类子事件会静默失效。要完整验证挣扎检测,需要同时开启行为监控:val config = TDEMConfiguration.Builder(this).id("your-project-key").url("https://dem.rumt-zh.com").behaviorPluginConfig(BehaviorPluginConfig(enabled = true)) // 必需:提供点击 / 表单事件源.strugglePluginConfig(StrugglePluginConfig(enabled = true)).build()TDEM.init(config)
import android.app.Application;import com.tencent.tdem.TDEM;import com.tencent.tdem.TDEMConfiguration;import com.tencent.tdem.behavior.BehaviorPluginConfig;import com.tencent.tdem.struggle.StrugglePluginConfig;TDEMConfiguration config = new TDEMConfiguration.Builder(this).id("your-project-key").url("https://dem.rumt-zh.com").behaviorPluginConfig(new BehaviorPluginConfig(true)) // 必需:提供点击 / 表单事件源.strugglePluginConfig(new StrugglePluginConfig()).build();TDEM.init(config);
back_forward 消费的是页面事件,而页面事件有不受行为监控开关影响的来源(startPage / setPage / leavePage),因此它单独可用。验证方式:连点同一按钮5次以上触发
rage_click,或在10秒内连续返回3次触发 back_forward,随后观察 Logcat 中的挣扎日志。会话回放
Session Replay 默认关闭,需在初始化时显式开启。Kotlin 侧可直接用命名参数(
ReplayConfig(enabled = true));Java 侧需传满 9 个参数,且 quality / screenshotStrategy 两个枚举类型位于内部包、类名被混淆,跨版本可能变化,不便直接依赖时可在 Kotlin 文件中构造后再传入:val config = TDEMConfiguration.Builder(this).id("your-project-key").url("https://dem.rumt-zh.com").replayPluginConfig(ReplayConfig(enabled = true)).build()TDEM.init(config)
import android.app.Application;import com.tencent.tdem.TDEM;import com.tencent.tdem.TDEMConfiguration;import com.tencent.tdem.replay.ReplayConfig;import com.tencent.tdem.internal.z7; // quality 枚举(当前版本位于内部包、类名被混淆)import com.tencent.tdem.internal.f9; // screenshotStrategy 枚举(同上)import java.util.Collections;// Java 没有 Kotlin 默认参数,必须按顺序传满 9 个参数TDEMConfiguration config = new TDEMConfiguration.Builder(this).id("your-project-key").url("https://dem.rumt-zh.com").replayPluginConfig(new ReplayConfig(true, // enabled0.1, // sessionSampleRate,默认 0.11.0, // onErrorSampleRate,默认 1.0z7.d, // quality = LOW(默认值)false, // maskAllText,默认 falsefalse, // maskAllImages,默认 falsef9.a, // screenshotStrategy = PIXEL_COPY(默认值)ReplayConfig.k, // maskViewClasses,默认遮罩集Collections.<String>emptySet())) // unmaskViewClasses.build();TDEM.init(config);
采样是双通道:
sessionSampleRate(默认0.1)决定常规会话是否被录制;未命中时若 onErrorSampleRate(默认1.0)大于0,SDK 会走滚动缓冲策略,在发生错误时冲刷并上报。控制台下发的远程配置
replay_sample_rate(0 - 100)会覆盖本地 sessionSampleRate。因此“开启了回放却没看到数据”不一定是接入失败,需要依次确认:本地 enabled 已开启、控制台远程配置已启用回放、且会话被采样命中。验证方式:开启后操作 App 若干秒,观察 Logcat 中的会话回放日志。
设备信息
设备信息采集默认开启,SDK 会在每条事件的上下文中附带下列设备字段:
os / device_type / device_model / app_version / sdk_version / network_type / screen_resolution / language / carrier / storage_free_mb / memory_free_mb验证方式:在控制台打开任一事件的详情,确认事件上下文中的上述字段已填充。
步骤3:主动上报(可选)
除自动采集外,SDK 提供下列主动上报接口,均通过
TDEM.getInstance() 获取实例后调用。val tdem = TDEM.getInstance()// 自定义事件tdem?.track("order_submit",tags = mapOf("channel" to "app"),properties = mapOf("amount" to 99),)// 自定义测速:可直接传耗时,或 start / end 成对使用tdem?.measure("api_latency", 320, tags = mapOf("api" to "/user/info"))tdem?.startMeasure("render")val duration = tdem?.endMeasure("render") // 返回耗时(ms);未调用 startMeasure 时返回 -1// 日志与异常:用于上报业务已捕获、未导致崩溃的异常tdem?.captureMessage("用户完成注册", level = "info")try {// 业务代码} catch (e: Exception) {tdem?.captureException(e, tags = mapOf("module" to "payment"))}
import com.tencent.tdem.TDEM;import java.util.HashMap;import java.util.Map;TDEM tdem = TDEM.getInstance();// 自定义事件Map<String, String> tags = new HashMap<>();tags.put("channel", "app");Map<String, Object> properties = new HashMap<>();properties.put("amount", 99);tdem.track("order_submit", tags, properties);// 自定义测速:可直接传耗时,或 start / end 成对使用Map<String, String> measureTags = new HashMap<>();measureTags.put("api", "/user/info");tdem.measure("api_latency", 320, measureTags, new HashMap<String, Object>());tdem.startMeasure("render");long duration = tdem.endMeasure("render"); // 返回耗时(ms);未调用 startMeasure 时返回 -1// 日志与异常:用于上报业务已捕获、未导致崩溃的异常tdem.captureMessage("用户完成注册", "info", null);try {// 业务代码} catch (Exception e) {Map<String, Object> crashTags = new HashMap<>();crashTags.put("module", "payment");tdem.captureException(e, crashTags);}
用户标识、设备标识与全局标签:
tdem?.setUser("user-456") // 设置用户标识,用于用户维度聚合tdem?.clearUser() // 清除用户标识tdem?.setDeviceId("device-abc") // 设置设备标识,由宿主提供tdem?.clearDeviceId() // 清除设备标识,回落 not_settdem?.setTags(mapOf("role" to "admin", "team" to "dev")) // 设置 / 追加标签tdem?.removeTags(listOf("team")) // 移除指定标签tdem?.clearTags() // 清空所有标签
import com.tencent.tdem.TDEM;import java.util.Arrays;import java.util.HashMap;import java.util.Map;TDEM tdem = TDEM.getInstance();tdem.setUser("user-456"); // 设置用户标识,用于用户维度聚合tdem.clearUser(); // 清除用户标识tdem.setDeviceId("device-abc"); // 设置设备标识,由宿主提供tdem.clearDeviceId(); // 清除设备标识,回落 not_setMap<String, Object> tags = new HashMap<>();tags.put("role", "admin");tags.put("team", "dev");tdem.setTags(tags); // 设置 / 追加标签tdem.removeTags(Arrays.asList("team")); // 移除指定标签tdem.clearTags(); // 清空所有标签
步骤4:检查数据上报
在 Logcat 中观察 SDK 日志。全模块日志统一使用
TDEM-Android 作为 tag,模块名只出现在日志正文中:adb logcat -s TDEM-Android:D# 只看某个模块(如网络)的日志adb logcat -s TDEM-Android:D | grep HttpPlugin