鸿蒙 应用埋点:点击、曝光与页面埋点
本文介绍了移动应用埋点技术的实现方案,主要包括三种埋点类型:点击埋点(通过UIObserver监听点击事件)、曝光埋点(使用setOnVisibleAreaApproximateChange监听组件可视区域变化)和页面埋点(监听页面切换事件)。详细阐述了每种埋点的实现方法,包括数据绑定、事件监听、回调处理和上报流程。方案采用全局无感监听方式,通过封装TrackNode组件管理曝光数据,利用hiAp
·
一、埋点
埋点是指将信息采集程序和原本的功能代码结合起来,针对特定用户行为收集、处理和发送一些信息,用来跟踪应用使用情况。包括访问数、访客数、停留时长、页面浏览数和跳出率。
| 场景 | 说明 |
|---|---|
| 点击统计 | 统计组件点击频率,分析用户偏好 |
| 滑动监听 | 计算滑动偏移量和曝光比例 |
| 页面切换 | 统计停留时间、来源页和目标页 |
| 性能分析 | 计算加载各节点耗时,针对性优化 |
二、埋点分类
| 埋点类型 | 行为属性 | 说明 |
|---|---|---|
| 点击埋点 | 主动行为 | 用户在任意区域的一次单击(icon、图片等) |
| 曝光埋点 | 被动行为 | 统计页面局部区域是否被有效浏览(瀑布流卡片曝光比例和时长) |
| 页面埋点 | - | 统计停留时间、加载性能、跳转来源/去向 |
三、整体方案
整体方案使用全局无感监听能力实现埋点功能:
-
UIObserver:全局监听点击事件、滑动事件、页面切换
-
setOnVisibleAreaApproximateChange:监听组件可视区域变化(曝光埋点)
3.1 UIObserver提供的事件
| 事件 | 说明 |
|---|---|
on('willClick') |
点击事件指令下发前触发 |
on('didClick') |
点击事件指令下发后触发 |
on('scrollEvent') |
组件滑动开始/结束回调 |
on('navDestinationSwitch') |
Navigation页面切换监听 |
on('routerPageUpdate') |
Router页面切换监听 |
on('willDraw') |
首帧绘制前 |
on('didLayout') |
完成绘制时 |
四、绑定埋点数据
4.1 组件绑定ID和埋点数据
Button('Click Tracing Point - Single Component')
.width('100%')
.id('button-1')
.fontWeight(FontWeight.Bold)
.customProperty('button-1', DataResource['Index']['button-1'])
.onClick(() => {
hilog.info(0x0000, 'ApplicationTrack', 'btn');
})
4.2 封装埋点数据资源
// DataResource.ets
export const DataResource: Record<string, Record<string, DataResourceType>> = {
'Index': {
'button-1': { id: 'button-1' },
'button-2': { id: 'button-2' }
},
'Page2': {
'component-1': { id: 'text-2' }
}
}
export interface DataResourceType {
id: string
}
五、点击埋点实现
5.1 在EntryAbility中注册点击监听
// EntryAbility.ets
uiContext.getUIObserver()?.on('willClick', (_event: ClickEvent, node?: FrameNode) => {
const clickCallback = CallbackManager.getInstance().getClickCallback();
clickCallback(node, uiContext);
})
5.2 处理点击回调
// CallbackManager.ets
public getClickCallback() {
return (node: FrameNode | undefined, uiContext: UIContext) => {
const uniqueId = node?.getUniqueId();
const ID = node?.getId();
const pageInfo = uiContext.getPageInfoByUniqueId(uniqueId);
const trackData = node?.getCustomProperty(ID);
let eventParams: Record<string, string | number> = {
'component_id': ID ?? '',
'pageInfo': JSON.stringify(pageInfo ?? {}),
'trackData': JSON.stringify(trackData ?? {})
};
hiAppEvent.write({
domain: 'test_domain',
name: 'test_event',
eventType: hiAppEvent.EventType.FAULT,
params: eventParams
}, (err: BusinessError) => {
// 处理回调
});
};
}
5.3 取消监听
// onWindowStageDestroy
uiContext?.getUIObserver().off('willClick');
六、曝光埋点实现
6.1 自定义TrackNode钩子组件
TrackNode组件需支持:
-
嵌套子组件
-
组件ID值注入
-
注册监听事件
// TrackNode.ets
@Builder
function TrackNode(
{ track }: { track?: Track },
content: () => void
) {
TrackNodeComponent({ track: track }, content)
}
@Component
struct TrackNodeComponent {
@ObjectLink track: Track;
onDidBuild() {
let uid = this.getUniqueId();
let node: FrameNode | null = this.getUIContext().getFrameNodeByUniqueId(uid);
// 1. 将组件与TrackShadow对象绑定
TrackManager.get().addTrack(this.trackShadow.id, this.trackShadow);
// 2. 监听可视区域变化(设置0、0.5、1比率,500ms更新间隔)
node?.commonEvent.setOnVisibleAreaApproximateChange(
{ ratios: [0, 0.5, 1], expectedUpdateInterval: 500 },
(ratioInc: boolean, ratio: number) => {
this.trackShadow.visibleRatio = ratio;
}
);
// 3. 向上追溯父节点,建立父子关系
// ...
}
aboutToDisappear() {
TrackManager.get().removeTrack(this.trackShadow.id);
}
build() {
// 包裹子组件
content();
}
}
6.2 TrackManager曝光数据管理
export class TrackManager {
private trackMap: Map<string, TrackShadow> = new Map();
private rootTrack: TrackShadow | null = null;
addTrack(id: string, track: TrackShadow): void {
this.trackMap.set(id, track);
}
removeTrack(id: string): void {
this.trackMap.delete(id);
}
getTrackById(id: string): TrackShadow | undefined {
return this.trackMap.get(id);
}
dump(): void {
this.rootTrack?.dump(0); // 递归输出所有子组件的曝光比例
}
}
6.3 使用TrackNode包裹组件
// WaterFlowPage.ets
TrackNode({ track: new Track().id('WaterFlow-1') }) {
WaterFlow() {
LazyForEach(this.dataSource, (item: number, index: number) => {
FlowItem() {
TrackNode({ track: new Track().id(`flowItem_${index}`) }) {
WaterFlowCard({ item: item, index: index })
.id(`flowItem_${index}`)
}
}
})
}
}
七、页面埋点实现
7.1 Navigation路由监听
// EntryAbility.ets
uiContext.getUIObserver().on('navDestinationSwitch', (info) => {
const switchCallback = CallbackManager.getInstance().getSwitchCallback();
switchCallback(info);
})
回调参数:
| 字段 | 说明 |
|---|---|
context |
页面上下文信息 |
from |
来源页 |
to |
去向页 |
operation |
页面操作类型 |
7.2 Router路由监听
// EntryAbility.ets
uiContext.getUIObserver().on('routerPageUpdate', (info) => {
const switchCallback = CallbackManager.getInstance().getSwitchCallback();
switchCallback(info);
})
回调参数:
| 字段 | 说明 |
|---|---|
context |
页面上下文信息 |
index |
触发页面在路由栈中的位置 |
name |
触发页面名称 |
path |
触发页面路径 |
state |
页面状态(ABOUT_TO_APPEAR/ON_PAGE_HIDE/ON_PAGE_SHOW) |
pageId |
页面唯一标识 |
7.3 页面加载性能监听
aboutToAppear(): void {
const uiContext = this.getUIContext();
uiContext.getUIObserver().on('willDraw', () => {
this.startTime = Date.now(); // 首帧开始
})
uiContext.getUIObserver().on('didLayout', () => {
this.endTime = Date.now(); // 绘制完成
})
}
八、埋点数据上传
通过hiAppEvent.addWatcher()添加订阅事件观察者,在onTrigger回调中实现数据上报。
const onTrigger = CallbackManager.getInstance().getOnTrigger();
hiAppEvent.addWatcher({
name: 'watcher1',
appEventFilters: [
{
domain: 'test_domain',
eventTypes: [hiAppEvent.EventType.FAULT, hiAppEvent.EventType.BEHAVIOR]
}
],
triggerCondition: {
row: 10, // 10条事件
size: 1000, // 或1000字节
timeOut: 1 // 或1秒
},
onTrigger: onTrigger
})
九、三种埋点对比
| 埋点类型 | 实现方式 | 核心API |
|---|---|---|
| 点击埋点 | UIObserver点击监听 + hiAppEvent.write | on('willClick') + getCustomProperty |
| 曝光埋点 | setOnVisibleAreaApproximateChange监听 + TrackNode封装 | setOnVisibleAreaApproximateChange + 自定义TrackManager |
| 页面埋点 | UIObserver页面切换监听 | on('navDestinationSwitch') / on('routerPageUpdate') |
数据上报流程
埋点数据写入(hiAppEvent.write)
↓
本地事件文件存储
↓
addWatcher订阅(设置触发条件)
↓
onTrigger触发
↓
HTTP请求上报服务器
注意事项
-
hiAppEvent.write参数:params的值只能是number、string、boolean及数组类型
-
曝光埋点限制:组件未发生变化时不触发回调(如Item达到500ms后用户不再滑动,不会被记录)
-
scrollEvent限制:id值仅能精确到外层组件,无法精确到内层子组件(曝光比例需用setOnVisibleAreaApproximateChange)
更多推荐




所有评论(0)