用户行为数据是产品迭代的基石。想知道哪个按钮最受欢迎?哪个页面停留时间最长?瀑布流的卡片曝光率如何?这些都需要埋点技术。传统做法是在每个事件回调里写一堆上报代码,维护成本高还容易遗漏。HarmonyOS提供了UIObserver全局监听能力,配合setOnVisibleAreaApproximateChange属性,能实现无侵入式的埋点方案。
image.png

埋点数据绑定策略

埋点的第一步是给组件打标签。不是简单的ID命名,而是通过customProperty属性注入埋点数据。比如Button组件ID设为"button-1",同时把埋点数据挂在key-value里,key是组件ID,value是业务自定义的数据结构。推荐把这些数据统一定义在DataResource中,按照Page名+组件名+索引的方式组织,方便后续扩展。

// 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' }  
  }  
}

这样设计的好处是埋点数据和业务代码解耦,改数据不用改组件代码,新增页面也能快速复用。

点击埋点:全局监听替代分散回调

点击埋点最容易实现。UIObserver提供了willClick和didClick两种监听方式,前者在点击事件触发前回调,后者在触发后回调。区别不大,选willClick就行,能更早拿到数据。

在EntryAbility中注册监听,回调里拿到FrameNode节点对象。FrameNode能直接获取组件ID、所在页面信息、组件大小位置等属性,还能通过getCustomProperty提取之前绑定的埋点数据。拿到数据后调用hiAppEvent.write()写入本地文件,参数值只能是number、string、boolean及数组类型。

Button('Click Tracing Point - Single Component')  
  .width('100%')  
  .id('button-1')  
  .customProperty('button-1', DataResource['Index']['button-1'])  
  .onClick(() => {  
    hilog.info(0x0000, 'ApplicationTrack', '%{public}s', 'btn');  
  })

// EntryAbility.ets - 全局监听注册  
uiContext.getUIObserver()?.on('willClick', (_event: ClickEvent, node?: FrameNode) => {  
  const clickCallback = CallbackManager.getInstance().getClickCallback();  
  clickCallback(node, uiContext);  
})

生命周期结束时记得调用off()取消监听,在onWindowStageDestroy()里处理。不然监听器一直存在,浪费资源。

滑动埋点:监听容器滚动事件

点击埋点只能追踪单一操作,滑动埋点能捕捉连续行为。UIObserver的on(‘scrollEvent’)监听组件滑动,回调参数ScrollEventInfo包含组件ID、滑动事件类型、滑动偏移量等信息。但有个限制:回调的ID值只能精确到外层容器,无法精确定位到内层的每个Item。如果要追踪瀑布流每个卡片的具体曝光情况,得用专门的曝光埋点方案。

// WaterFlowPage.ets - WaterFlow容器监听  
WaterFlow() {  
  LazyForEach(this.dataSource, (item: number, index: number) => {  
    FlowItem() {  
      WaterFlowCard({ item: item, index: index })  
    }  
  }, (item: number) => item.toString())  
}  
.id('WaterFlow-1')  
.onScrollStart(() => {  
  hilog.info(0x0000, 'ApplicationTrack', '%{public}s', 'scroll start');  
})  
.onScrollStop(() => {  
  hilog.info(0x0000, 'ApplicationTrack', '%{public}s', 'scroll stop');  
})

曝光埋点:虚拟树结构追踪组件可见性

曝光埋点最复杂,需要监测每个组件的出现与消失。瀑布流场景下,如果某个Item出现超过500ms算一次有效曝光,用户滑动过程中要实时计算曝光比例。

解决方案是自定义TrackNode"钩子"组件,用onDidBuild()生命周期注入组件信息。核心逻辑分三步:一是通过TrackManager把当前组件与TrackShadow对象绑定存入Map;二是调用setOnVisibleAreaApproximateChange()监听可视区域变化,ratio参数设0.0、0.5、1.0代表不同曝光阈值;三是追溯父节点构建虚拟树,子组件集合childIds记录在父节点的TrackShadow里。

// TrackNode.ets - 钩子组件核心逻辑  
onDidBuild(): void {  
  let uid = this.getUniqueId();  
  let node: FrameNode | null = this.getUIContext().getFrameNodeByUniqueId(uid);

  this.trackShadow.node = node;  
  this.trackShadow.id = node?.getId();  
  TrackManager.get().addTrack(this.trackShadow.id, this.trackShadow);

  node?.commonEvent.setOnVisibleAreaApproximateChange(  
    { ratios: [0, 0.5, 1], expectedUpdateInterval: 500 },  
    (ratioInc: boolean, ratio: number) => {  
      this.trackShadow.visibleRatio = ratio;  
    });

  // 向上追溯父节点,构建虚拟树  
  let parent: FrameNode | null = node?.getParent();  
  while (parent !== null) {  
    let parentTrack = TrackManager.get().getTrackById(parent?.getId());  
    if (parentTrack !== undefined) {  
      parentTrack.childIds.add(this.trackShadow.id);  
      this.trackShadow.parentId = parentTrack.id;  
      break;  
    }  
    parent = parent.getParent();  
  }  
}

TrackManager封装了增删查导出方法,导出时从根节点递归输出所有子组件的曝光比例。TrackShadow对象包含FrameNode、track、childIds、parentId和visibleRatio等字段,完整记录组件的曝光状态。

应用时用TrackNode包裹WaterFlow和FlowItem,传递包含id的track对象。滚动时能监听每个Item的曝光比例,还能追溯到根节点统计所有子组件的曝光数据。

页面埋点:路由监听与性能采集

页面埋点分两部分:监听页面切换和采集加载性能。HarmonyOS有Navigation和Router两种路由方案,UIObserver分别提供对应监听接口。

Navigation路由用on(‘navDestinationSwitch’)监听页面切换,回调参数info包含context、from、to和operation字段,标识来源页和去向页。还能用on(‘navDestinationUpdate’)监听页面显示隐藏状态。

Router路由用on(‘routerPageUpdate’)监听,调用pushPath()从A跳到B时触发三次回调:第一次B页面ABOUT_TO_APPEAR即将显示,第二次A页面ON_PAGE_HIDE隐藏,第三次B页面ON_PAGE_SHOW显示。回调参数包含页面名称、路径、状态、唯一标识pageId等信息。

页面加载性能通过on(‘willDraw’)和on(‘didLayout’)监听,前者记录首帧绘制开始时间,后者记录布局完成结束时间,差值就是加载耗时。监听要在页面aboutToAppear生命周期注册。

// NavigationPage.ets - 性能监听注册  
aboutToAppear(): void {  
  const uiContext = this.getUIContext();  
  uiContext.getUIObserver().on('willDraw', () => {  
    this.startTime = Date.now();  
  })  
  uiContext.getUIObserver().on('didLayout', () => {  
    this.endTime = Date.now();  
  })  
}

数据上传:观察者模式触发上报

埋点数据先写入本地文件,攒够一定数量再批量上传。hiAppEvent的addWatcher()方法添加观察者,设置触发条件:比如事件size超过1000字节、row超过10行、timeOut超过1秒。满足条件触发onTrigger回调,回调里调用http.request发起网络请求,把EXAMPLE_URL换成服务器地址就行。

// EntryAbility.ets - 数据上报  
hiAppEvent.addWatcher({  
  name: 'watcher1',  
  appEventFilters: [  
    {  
      domain: 'test_domain',  
      eventTypes: [hiAppEvent.EventType.FAULT, hiAppEvent.EventType.BEHAVIOR]  
    }  
  ],  
  triggerCondition: {  
    row: 10,  
    size: 1000,  
    timeOut: 1  
  },  
  onTrigger: onTrigger  
})

这种批量上报策略能减少网络请求频次,节省带宽成本。

注意事项

滑动容器的子组件曝光监听有个坑:setOnVisibleAreaApproximateChange在组件未变化时不会触发回调。瀑布流场景下,某个Item已经达到500ms阈值,但用户不再滑动或直接退出应用,这次曝光不会被记录。解决方案是在组件销毁时强制上报一次曝光数据。

曝光埋点推荐在List、Grid、Swiper、WaterFlow等滑动容器中使用,静态页面组件曝光监听意义不大。

埋点数据上传要根据业务场景调整触发条件,频繁上报浪费资源,积攒太多可能导致数据丢失。建议row设为10-20,size设为1000-5000字节,timeOut设为1-2秒。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐