第2章 埋点状态定义与类型

2.1 状态的边界定义

在 Jetpack Compose 埋点体系中,埋点状态是对 UI 交互行为的原子化建模。与其将每一次埋点视为一次离散的网络请求,不如将状态视为 UI 事件的持久化快照。三种核心状态——曝光、点击、停留——构成了用户行为数据流的源头。

状态类型 语义 触发时机 生命周期特征 典型场景
曝光 可见性报告 组件首次进入视口且稳定存在 ≥ 300ms 一次性触发,不可逆 首页推荐流卡片、广告位、列表 item
点击 意图确认 用户手指抬起且仍在组件边界内 瞬时事件,携带组件元数据 按钮、跳转链接、切换开关
停留 注意力时长 组件从可见变为不可见,或主动上报结束 有开始和结束两个子状态,持续累积 阅读文章、视频播放页、表单填写

2.2 状态的生命周期与转换

每一种埋点状态从诞生到消费,都经历三个核心阶段:检测、确认、上报。在 Compose 声明式框架下,状态的检测依赖于组合键(key)与视觉树(LayoutCoordinates)的联合计算。

  1. 检测阶段:通过 Modifier.onGloballyPositioned 获取组件实际边界,结合 isVisible 与 isFullyVisible 判断曝光条件。
  2. 确认阶段:引入防抖窗口(Debounce),避免快速滚动导致的假曝光;点击必须区分手势时长与移动距离,排除误触。
  3. 上报阶段:将状态序列化为埋点协议对象,通过信任链路发送至数据分析平台,同时更新本地状态缓存。

状态转换约束:曝光必须先于点击发生;停留的开始必须在曝光确认之后;不允许出现无曝光基础的停留或点击。

埋点状态生命周期与转换模型 埋点状态生命周期与转换模型 页面打开 视图可见性检测 曝光状态 可见 ≥ 300ms 点击状态 抬起在边界内 停留开始 打时间戳 停留结束 离开或关闭 状态收集器 序列化 + 上报 说明 点击必须基于已曝光的组件 停留时长 = 结束 - 开始 三者互斥但可组合

2.3 组件职责分离

在架构上,三种状态不应由同一个 Composable 函数内联处理。职责应按照“状态机”与“埋点协议”两层分离:

  • 状态机层:只负责状态检测与生命周期管理,输出布尔型或枚举型结果(如 isExposed、onClick、stayDuration)。
  • 埋点协议层:接收状态机的输出,拼接业务元数据(页面名、组件ID、扩展参数),组装成标准协议格式。
// 状态定义协议示例
sealed class TrackEvent {
    data class Exposure(
        val pageId: String,
        val componentId: String,
        val timestamp: Long
    ) : TrackEvent()

    data class Click(
        val pageId: String,
        val componentId: String,
        val targetUrl: String?,
        val timestamp: Long
    ) : TrackEvent()

    data class Stay(
        val pageId: String,
        val componentId: String,
        val durationMs: Long,
        val startTime: Long,
        val endTime: Long
    ) : TrackEvent()
}

最佳实践:状态机的输出应与 Composable 的重组解耦。推荐将 TrackEvent 的构建放在 ViewModel 层完成,Composable 只负责传递生命周期事件的原始信号。

2.4 风险点与选型权衡

定义埋点状态时,最常见的风险集中在以下三个维度:

风险点 具体表现 缓解方案
状态遗漏 快速滑动导致曝光未被检测到;点击事件被系统手势拦截 引入可见性阈值与手势冲突判断;使用 pointerInput 自定义手势
内存泄漏 停留开始后组件被回收但未上报结束 绑定 DisposableEffect 在组件生命周期结束时强制上报
重复上报 状态因重组被多次触发 为每个状态生成唯一 UUID;上报后更新本地缓存避免二次写入

2.5 落地步骤与验收标准

将三种状态定义落实为可执行的代码模块,建议按以下步骤推进:

  1. 定协议:确定 TrackEvent 的数据结构,包含必选字段与扩展字段。
  2. 写检测器:为曝光、点击、停留分别创建 Modifier 扩展函数,内嵌状态机。
  3. 接存储:在 ViewModel 中维护一个 MutableStateFlow<TrackEvent>,用于汇集所有上报。
  4. 联调验证:使用埋点测试工具(如自定义日志面板)验证三种状态是否按预期触发。
// 停留状态检测器示例 Modifier
fun Modifier.trackStay(
    pageId: String,
    componentId: String,
    onStayComplete: (TrackEvent.Stay) -> Unit
): Modifier = composed {
    var startTime by remember { mutableLongStateOf(0L) }
    var isVisible by remember { mutableStateOf(false) }
    DisposableEffect(Unit) {
        onDispose {
            if (isVisible && startTime > 0) {
                val endTime = System.currentTimeMillis()
                onStayComplete(
                    TrackEvent.Stay(
                        pageId = pageId,
                        componentId = componentId,
                        durationMs = endTime - startTime,
                        startTime = startTime,
                        endTime = endTime
                    )
                )
            }
        }
    }
    this.onGloballyPositioned { coordinates ->
        isVisible = coordinates.isVisible()
        if (isVisible && startTime == 0L) {
            startTime = System.currentTimeMillis()
        }
    }
}

验收标准案例:

  • 快速滚动列表时,只有停留超过 300ms 的 item 才会触发曝光;
  • 点击按钮后,埋点日志中同一组件 ID 的曝光与点击事件先后顺序严格一致;
  • 用户退出页面时,停留状态自动结束并上报,无悬空状态。

2.6 选型权衡与信任链路

在状态定义层做选型时,需要在 编译期强校验 与 运行期灵活性 之间找到平衡:

  • 枚举合一:将曝光、点击、停留定义为同一 sealed class 的子类,便于在 Collector 端统一处理,但新增状态类型需要修改多处 pattern match。
  • 独立接口:每个状态单独抽象为接口(如 ExposureTracker、ClickTracker、StayTracker),扩展性好但上层组合变复杂。