埋点是指将信息采集程序和原本的功能代码结合起来,针对特定用户行为收集、处理和发送一些信息,用来跟踪应用使用情况。包括访问数、访客数、停留时长、页面浏览数和跳出率。以下是几种常见业务场景:
按照用户行为不同,埋点可以分为点击埋点、曝光埋点以及页面埋点等。
应用事件打点方法hiAppEvent.write(),可将AppEventInfo类型事件进行存储,并通过callback方式实现异步回调。若上报失败,建议采用缓存机制进行后续处理;或重新调用hiappEvent.write埋点,待下次回调时再处理。
接下来会从(1)组件动态绑定埋点数据;(2)点击埋点方案;(3)曝光埋点方案;(4)页面埋点方案四部分介绍。整体方案使用全局无感监听能力Class (UIObserver)和setOnVisibleAreaApproximateChange属性实现埋点功能。

首先,为需要埋点的组件指定对应的ID值和埋点数据。例如,Button组件的ID可以设为“button-1”,并通过customProperty属性设置key和value,其中key为组件ID,value为埋点数据。为了方便获取,可以将埋点数据统一定义在DataResource中。
// entry\src\main\ets\pages\ClickPage.ets
Button($r('app.string.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', '%{public}s', 'btn');
}) DataResource根据Page名、组件名和索引封装,Page名为最外层key,组件名+索引为里层key。value值根据实际业务配置。
// entry\src\main\ets\viewModel\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
} 配置完埋点数据并绑定组件后,在EntryAbility中注册点击事件监听,在事件回调中获取触发节点。Class (UIObserver)一共提供了两种监听事件:
这两种方法都能在用户点击组件时触发回调。本示例使用 `willClick` 监听方式,下面介绍具体实现方案。
实现步骤
// entry\src\main\ets\pages\ClickPage.ets
Row() {
Text($r('app.string.click_tracing_point_composite_component'))
// ...
Image($r('app.media.ic_public_arrow_right'))
// ...
}
// ...
.id('button-2')
.margin({ top: 12 })
.customProperty('button-2', DataResource['Index']['button-2'])
.onClick(() => {
hilog.info(0x0000, 'ApplicationTrack', '%{public}s', 'row');
})
Row() {
// entry\src\main\ets\pages\ClickPage.ets
Button($r('app.string.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', '%{public}s', 'btn');
})
} // entry\src\main\ets\entryability\EntryAbility.ets
uiContext.getUIObserver()?.on('willClick', (_event: ClickEvent, node?: FrameNode) => {
const clickCallback = CallbackManager.getInstance().getClickCallback();
clickCallback(node, uiContext);
}) // entry\src\main\ets\viewModel\CallBackManager.ets
/**
* Obtains the ClickCallback callback.
*
*/
public getClickCallback() {
return (node: FrameNode | undefined, uiContext: UIContext) => {
hilog.info(0x0000, 'ApplicationTrack', '%{public}s', `FrameNode: ${node}`);
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) => {
if (err) {
hilog.error(0x0000, 'CallBackManager', '%{public}s', `code: ${err.code}, message: ${err.message}`);
return;
}
hilog.info(0x0000, 'ApplicationTrack', '%{public}s', `getClickCallback, success to write event`);
});
};
} hiAppEvent.write({
domain: 'test_domain',
name: 'test_event',
eventType: hiAppEvent.EventType.FAULT,
params: eventParams
}, (err: BusinessError) => {
if (err) {
hilog.error(0x0000, 'CallBackManager', '%{public}s', `code: ${err.code}, message: ${err.message}`);
return;
}
hilog.info(0x0000, 'ApplicationTrack', '%{public}s', `getClickCallback, success to write event`);
}); // entry\src\main\ets\entryability\EntryAbility.ets
onWindowStageDestroy(): void {
const uiContext: UIContext | undefined = AppStorage.get('uiContext');
uiContext?.getUIObserver().off('willClick');
uiContext?.getUIObserver().off('scrollEvent');
uiContext?.getUIObserver().off('navDestinationSwitch');
uiContext?.getUIObserver().off('routerPageUpdate');
// Main window is destroyed, release UI related resources
hilog.info(0x0000, 'ApplicationTrack', '%{public}s', 'Ability onWindowStageDestroy');
} 除了点击事件外,UIObserver还可以通过on('scrollEvent')监听组件滑动。在滑动开始与结束触发回调并得到滑动偏移量。以瀑布流为例,对WaterFlow和FlowItem设置组件ID。
// entry\src\main\ets\pages\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}`)
}
}
// ...
}, (item: number) => item.toString())
}
.id('WaterFlow-1')
// ...
.onReachStart(() => {
hilog.info(0x0000, 'ApplicationTrack', '%{public}s', 'waterFlow reach start');
})
.onScrollStart(() => {
hilog.info(0x0000, 'ApplicationTrack', '%{public}s', 'waterFlow scroll start');
})
.onScrollStop(() => {
hilog.info(0x0000, 'ApplicationTrack', '%{public}s', 'waterFlow scroll stop');
})
.onScrollFrameBegin((offset: number, state: ScrollState) => {
hilog.info(0x0000, 'ApplicationTrack', '%{public}s', `waterFlow scrollFrameBegin offset: ${offset}`);
hilog.info(0x0000, 'ApplicationTrack', '%{public}s',
`waterFlow scrollFrameBegin state: ${state.toString()}`);
return { offsetRemain: offset };
})
} 接着在EntryAbility里统一注册scrollEvent的事件监听,在回调中获取ScrollEventInfo信息,包括id、uniqueId、scrollEvent以及offset。
// entry\src\main\ets\entryability\EntryAbility.ets
uiContext.getUIObserver()
.on('scrollEvent', (info) => CallbackManager.getInstance().getScrollEvent(info))
在scrollEvent监听事件中,回调参数的id值仅能精确到外层组件WaterFlow,无法精确到内层FlowItem。 若要在滑动过程中获取各Item组件的曝光比例,请参考第三小节的曝光埋点。
曝光埋点需要监测页面中每个组件的出现与消失。例如,当用户滑动瀑布流时,如果某个项目出现的时长超过500毫秒,则记录为一次有效曝光。为避免在每个页面注入冗长代码,建议使用自定义的“埋点钩子”组件进行封装。以下是具体实现步骤。
实现步骤
(1)调用TrackManager的addTrack()方法,将当前组件与TrackShadow对象绑定。
(2)通过setOnVisibleAreaApproximateChange()监听埋点组件的可视区域的变化;其中ratio值可以自定义设置,比如本示例设置了0.0、0.5、1.0。
(3)根据当前组件获取其父节点,判断父节点是否有埋点钩子。如果没有,继续往上追溯,直到父节点为null。如果有,则在父节点的子组件集合中添加当前节点。
在aboutToDisappear()生命周期中,调用TrackManager的removeTrack()方法删除当前组件信息。
// entry\src\main\ets\viewModel\TrackNode.ets
// onDidBuild Life Cycle.
onDidBuild(): void {
// Construct the virtual tree of the tracing point.
// The obtained node is the root node of the current page (row in the test case).
let uid = this.getUniqueId();
let node: FrameNode | null = this.getUIContext().getFrameNodeByUniqueId(uid);
hilog.info(0x0000, 'ApplicationTrack', '%{public}s', `Track onDidBuild node:${node?.getNodeType()}`);
if (node === null) {
return;
}
this.trackShadow.node = node;
this.trackShadow.id = node?.getId();
this.trackShadow.track = this.track;
TrackManager.get().addTrack(this.trackShadow.id, this.trackShadow);
// The setOnVisibleAreaApproximateChange monitors and records the visible area of the tracing point component.
node?.commonEvent.setOnVisibleAreaApproximateChange(
{ ratios: [0, 0.5, 1], expectedUpdateInterval: 500 },
(ratioInc: boolean, ratio: number) => {
const areaChangeCb = CallbackManager.getInstance().getAreaChangeCallback();
areaChangeCb(node, ratio);
this.trackShadow.visibleRatio = ratio;
hilog.info(0x0000, 'ApplicationTrack', '%{public}s', `ratioInc: ${ratioInc}`);
hilog.info(0x0000, 'ApplicationTrack', '%{public}s', `ratio: ${ratio}`);
});
let parent: FrameNode | null = node?.getParent();
hilog.info(0x0000, 'ApplicationTrack', '%{public}s', `Parent getId: ${parent?.getId()}`);
let attachTrackToParent: (parent: FrameNode | null) => boolean =
(parent: FrameNode | null) => {
while (parent !== null) {
let parentTrack = TrackManager.get().getTrackById(parent?.getId());
if (parentTrack !== undefined) {
parentTrack.childIds.add(this.trackShadow.id);
this.trackShadow.parentId = parentTrack.id;
return true;
}
parent = parent.getParent();
}
return false;
};
let attached = attachTrackToParent(parent);
if (!attached) {
node?.commonEvent.setOnAppear(() => {
let attached = attachTrackToParent(parent);
if (attached) {
hilog.info(0x0000, 'ApplicationTrack', '%{public}s', `Track lazy attached: ${this.trackShadow.id}`);
}
});
}
} // entry\src\main\ets\viewModel\TrackNode.ets
/**
* Tracing point data operation class
*/
export class TrackManager {
static instance: TrackManager;
private trackMap: Map<string, TrackShadow> = new Map();
private rootTrack: TrackShadow | null = null;
static get(): TrackManager {
if (TrackManager.instance !== undefined) {
return TrackManager.instance;
}
TrackManager.instance = new TrackManager();
return TrackManager.instance;
}
addTrack(id: string, track: TrackShadow): void {
if (this.trackMap.size === 0) {
this.rootTrack = track;
}
hilog.info(0x0000, 'ApplicationTrack', '%{public}s', `Track add id: ${id}`);
this.trackMap.set(id, track);
}
removeTrack(id: string): void {
let current = this.getTrackById(id);
if (current !== undefined) {
this.trackMap.delete(id);
let parent = this.getTrackById(current?.parentId);
parent?.childIds.delete(id);
}
}
getTrackById(id: string): TrackShadow | undefined {
return this.trackMap.get(id);
}
dump(): void {
this.rootTrack?.dump(0);
}
} // entry\src\main\ets\viewModel\TrackNode.ets
export class Track {
public areaPercent: number = 0;
public trackId: string = '';
constructor() {
}
id(newId: string): Track {
this.trackId = newId;
return this;
}
}
/**
* Tracing point data.
*/
export class TrackShadow {
public node: FrameNode | null = null;
public id: string = '';
public track: Track | null = null;
public childIds: Set<string> = new Set();
public parentId: string = '';
public visibleRect: common2D.Rect = {
left: 0,
top: 0,
right: 0,
bottom: 0
};
public visibleRatio: number = 0;
// Output the information about the tracing point tree through global dump.
dump(depth: number = 0): void {
hilog.info(0x0000, 'ApplicationTrack', '%{public}s', `Track Dp: ${depth}`);
hilog.info(0x0000, 'ApplicationTrack', '%{public}s', `AreaPer: ${this.track?.areaPercent}`);
hilog.info(0x0000, 'ApplicationTrack', '%{public}s', `VisibleRatio: ${this.visibleRatio}`);
this.childIds.forEach((value: string) => {
TrackManager.get().getTrackById(value)?.dump(depth + 1);
});
}
} // entry\src\main\ets\pages\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}`)
}
}
// ...
}, (item: number) => item.toString())
}
.id('WaterFlow-1')
// ...
.onReachStart(() => {
hilog.info(0x0000, 'ApplicationTrack', '%{public}s', 'waterFlow reach start');
})
.onScrollStart(() => {
hilog.info(0x0000, 'ApplicationTrack', '%{public}s', 'waterFlow scroll start');
})
.onScrollStop(() => {
hilog.info(0x0000, 'ApplicationTrack', '%{public}s', 'waterFlow scroll stop');
})
.onScrollFrameBegin((offset: number, state: ScrollState) => {
hilog.info(0x0000, 'ApplicationTrack', '%{public}s', `waterFlow scrollFrameBegin offset: ${offset}`);
hilog.info(0x0000, 'ApplicationTrack', '%{public}s',
`waterFlow scrollFrameBegin state: ${state.toString()}`);
return { offsetRemain: offset };
})
} 滚动瀑布流时,不仅可以监听每个Item的曝光比例,还可以追溯到根节点,统计根节点中每个子组件的曝光比例。
页面埋点在本示例中分为两类:监听页面切换和采集页面加载性能。以下将从Navigation和Router两种路由方案进行讲解。
Navigation路由:
针对Navigation方案,UIObserver提供`navDestinationSwitch`事件,用于监听页面切换,并支持在回调中获取当前页面的切换信息。在`EntryAbility`中统一注册`UIObserver`的`navDestinationSwitch`事件监听。
// entry\src\main\ets\entryability\EntryAbility.ets
uiContext.getUIObserver().on('navDestinationSwitch', (info) => {
const switchCallback = CallbackManager.getInstance().getSwitchCallback();
switchCallback(info);
}) 回调函数中的info包含context、from、to和operation,用于标识页面的来源和去向。
字段 | 类型 | 含义 |
|---|---|---|
context | UIContext | 页面上下文信息 |
from | NavDestinationInfo | NavBar | 来源页 |
to | NavDestinationInfo | NavBar | 去向页 |
operation | 页面操作 |
此外还可以通过UIObserver的on("navDestinationUpdate")事件监听页面的显示与隐藏,回调传参中包含页面名称、状态信息以及页面的唯一标识ID。
Router路由:
针对Router路由方案,UIObserver提供了on('routerPageUpdate')监听事件,在页面切换过程中触发相应回调。
// entry\src\main\ets\entryability\EntryAbility.ets
uiContext.getUIObserver().on('routerPageUpdate', (info) => {
const switchCallback = CallbackManager.getInstance().getSwitchCallback();
switchCallback(info);
}) 比如调用pushPath()从A页面跳转到B页面时,该回调会被触发三次:第一次触发的页面名称为PageB,页面状态为ABOUT_TO_APPEAR即将显示;第二次触发的页面名称为PageA,页面状态为ON_PAGE_HIDE页面隐藏;第三次触发的页面名称为PageB,页面状态为ON_PAGE_SHOW页面显示。回调传参同样包含页面上下文、触发事件的页面名称等。
字段名 | 类型 | 含义 |
|---|---|---|
context | UIContext | 页面上下文信息 |
index | number | 触发页面在路由栈中的位置 |
name | String | 触发页面名称 |
path | String | 触发页面路径 |
state | 页面状态 | |
pageId | String | 页面唯一标识 |
页面加载性能:
页面加载性能可以通过计算首帧绘制与绘制结束的时间差来判断。UIObserver同样提供了on("willDraw")事件和on("didLayout")事件,可以在首帧监听中记录初始时间,在完成绘制时记录结束时间。此事件监听需要在页面中注册,Navigation与Router路由相同,本示例以Navigation为例。在aboutToAppear注册on("willDraw")和on("didLayout")事件。
import { hilog } from '@kit.PerformanceAnalysisKit';
import { router } from '@kit.ArkUI';
@Entry
@Component
struct NavigationPage {
// ...
aboutToAppear(): void {
const uiContext = this.getUIContext();
// Registering a Listening Event
uiContext.getUIObserver().on('willDraw', () => {
this.startTime = Date.now();
})
uiContext.getUIObserver().on('didLayout', () => {
this.endTime = Date.now();
})
}
// ...
getResourceString(resource: Resource): string {
let resourceString: string = '';
try {
resourceString = this.getUIContext().getHostContext()!.resourceManager.getStringSync(resource.id);
} catch (error) {
hilog.error(0x0000, '[getResourceString]', `getResourceString err: ${JSON.stringify(error)}`);
}
return resourceString;
}
// ...
build() {
Navigation(this.pageInfos) {
Column() {
Button('pushPath', { stateEffect: true, type: ButtonType.Capsule })
.width('100%')
.onClick(() => {
// Put the NavDestination page information specified by name into the stack.
this.pageInfos.pushPath({ name: 'pageOne' });
})
Button('Use interception', { stateEffect: true, type: ButtonType.Capsule })
// ...
.onClick(() => {
this.isUseInterception = !this.isUseInterception;
if (this.isUseInterception) {
// Register Interceptor.
this.registerInterception();
} else {
// Do not use interceptors.
this.pageInfos.setInterception(undefined);
}
})
Button($r('app.string.back'), { stateEffect: true, type: ButtonType.Capsule })
// ...
.onClick(() => {
this.getUIContext().getRouter().back();
})
}
// ...
}
// ...
}
} // entry\src\main\ets\entryability\EntryAbility.ets
const onTrigger = CallbackManager.getInstance().getOnTrigger();
hiAppEvent.addWatcher({
name: 'watcher1',
appEventFilters: [
{
domain: 'test_domain',
eventTypes: [hiAppEvent.EventType.FAULT, hiAppEvent.EventType.BEHAVIOR]
}
],
triggerCondition: {
row: 10,
size: 1000,
timeOut: 1
},
onTrigger: onTrigger
})
hilog.info(0x0000, 'ApplicationTrack', '%{public}s', 'Succeeded in loading the content.'); 本文从绑定埋点数据入手,介绍三种埋点的开发实现:点击、曝光和页面埋点。最后调用hiAppEvent的addWatcher()方法添加订阅对象和onTrigger()回调,在回调中实现数据上报逻辑。