1. 背景
用户数据分析与埋点,在互联网产品的设计与迭代中是不可缺少的一部分,利用用户的行为规律、用户画像,能在很大程度上帮助团队制定合适的运营策略与产品方向。
随着产品的迭代与业务的发展,对业务团队的敏捷性与创新性提出了更高的要求,而通过大数据的手段在一定程度上可以帮助我们实现这个愿景,同时,良好的数据分析可以帮助我们进行更好更优的决策。
一般我们会采集的数据会包括用户的点击行为操作、页面浏览量(PV)、页面停留时长、访客(UV)等等。而对于产生的数据本身,其整个流程主要可以总结为以下几点:
- 数据采集
- 数据上报
- 数据存储
- 数据分析
- 数据展示
我们常说的『埋点』就是数据采集领域的术语,通过采集到用户产生的原始数据,进行一定层次的过滤,达到产品运营的需求。数据采集的方式也可以说是埋点的几种方式。
现状、痛点
目前公司App产品在未引入Flutter之前,一直采用纯原生的埋点功能,原生的实现方案相对来说是比较完善的,采用混合开发后,随着迭代后Flutter模块的增多,仅仅在Flutter侧以代码埋点通过channel调用原生模块埋点接口的方式以逐渐不能满足我们的需求,即使这种方式能精准采集到需要的信息,但是对于App产品来说,耗费的成本逐渐增大,运营、开发、测试,都需要参与,沟通成本也逐渐增大。另外一方面,现在市面已有的第三方统计平台,要么不支持Flutter,要么也只是提供一个简单的插件提供接口手动调用,我们迫切的需要一个类似原生的自动化埋点方案解决问题。
原生的实现方式
1. 无痕埋点
俗称"全埋点"、“无埋点”,通过在端上自动采集并上报尽可能多的数据,根据一定的规则过滤筛选出自己需要的可用数据。
优点:
- 能很大程度减少开发、测试的重复工作,不需要对业务标识进行唯一的区分,ID的规则由设计的SDK和产品沟通约定好即可,减少业务人员后续的沟通成本和使用步骤
- 数据可以回溯并相对全面
缺点:
- 需要设计出一套全埋点的技术成品,能获取到准确的指标数据,前期的技术投入大
- 数据量大。需要后端落地后进行大量处理,采用智能系统分析平台或者是数据库查询数据聚合。同时需要产品进行自我还原业务场景。
2. 可视化埋点
可视化埋点是通过运营人员在可视化的工具选择需要收集的埋点数据,端侧获取配置后,再基于预先设置的规则,通过组件或控件精准采集,根据配置条件自动埋点上报的方式。
优点:很大程度减少开发、测试的重复工作,数据量可靠,可以在线上可视化工具动态的进行埋点配置,无需每次等到发版才能生效。
缺点:采集信息不够灵活,并且无法解决数据回溯的问题
2. 具体实现
无痕埋点:以无痕埋点为切入点,结合现在已有的原生方案,迁移到Flutter平台。
无痕埋点需要自动采集数据,因此针对页面、控件等元素需要生成其 ID,该 ID 需尽量具备『唯一性』和『稳定性』。『唯一性』非常好理解,因为对于任意元素而言,其 ID 应该是与其他所有元素都不同的,这样我们才能根据 ID 唯一标识出那个我们想要的元素,采集上来的数据才是准确的,不重复的。而『稳定性』则是说,元素的 ID 应尽量不受版本的变动而改变,这样后期关联业务含义的操作才会更加便捷。
1. Flutter页面ID的规则
根据"唯一性"与"稳定性",将页面所在类的类型作为ID,它是相对唯一的,除了页面复用,基本不存在其他类名相同的页面(不同的package例外),其次它是相对稳定的,除了修改类名情况下才会改变,除了一些页面重大的改版之外不会轻易修改类名。在Flutter中,页面也是Widget,因此,ID定义规则如下:
ID = Widget Type
+“额外参数”(widget
为当前前台显示的页面)
2. Flutter页面的PV、UV
一旦有了页面的唯一ID的生成规则,我们就可以在页面曝光的时候,去生成这个ID,然后上传即可实现页面的PV、UV指标。至于页面的曝光时机,在Flutter存在接口RouteObserver
:
//继承这个类,在MaterialApp中可配置,可以配置多个Observer
class RouteObserver<R extends Route> extends NavigatorObserver {
void didPop(Route route, Route previousRoute) {
…
}
void didPush(…){…}
…
}
能监控页面的曝光时机还不够,有时我们不仅仅需要的是知道进入了哪个页面,还需要知道在某个页面停留了多长的时间,并且应用在前后台的切换也要计算进去。同样的,在Flutter中存在监听页面的生命周期的接口WidgetsBindingObserver
:
abstract class WidgetsBindingObserver {
//省略部分代码
/// Called when the system puts the app in the background or returns
/// the app to the foreground.
///
/// An example of implementing this method is provided in the class-level
/// documentation for the [WidgetsBindingObserver] class.
///
/// This method exposes notifications from [SystemChannels.lifecycle].
void didChangeAppLifecycleState(AppLifecycleState state) { }
}
其中AppLifecycleState
是个枚举类,包含四种状态:
enum AppLifecycleState {
resumed,
inactive,
paused,
detached,
}
该接口通过以上四种状态,我们可以知道在某个页面停留的时长是多久。
以上是采集页面pv、uv、页面路径的基本思路,具体的代码不多做介绍,逻辑参考原生的实现即可。后面我着重介绍用户行为操作,点击行为埋点数据的采集实现。
3. Flutter组件ID的规则
对于组件的ID来说,它的规则要比页面的定义更加复杂。首先,Flutter的组件本身并没有一个id的概念,虽然Flutter的每个Widget都可以通过一个唯一key
去标识,但是在创建Widget的时候除非有特殊的需求(比如复用等),我们一般不会去传入一个key,所以需要换个思路:根据视图树。
每个页面的组件都是根据其父子、兄弟关系构建出视图树绘制在页面上。从我们观测的组件的本身开始,在这个视图树上逐级向上遍历搜索,直到根节点,找到这个组件在这个树上的位置信息等特征信息,这样就能得到一个组件在视图树上的 一个组件路径,也就是说,我们可以根据这个路径,在视图树中定位到这个组件(图片引用自极客时间-Flutter专栏):
widget、Element、RenerObject关系 ![img]( )
三棵树 Flutter中,存在这么三棵树(为了便于理解我们抽象`RenderObject`也为一个树),当我们点击了某个Widget的时候,我们期望的结果是可以通过这个Widget获取它在视图树上的位置,可惜的是Flutter中的Widget并没有一个类似"parent"和"child"属性可以供我们去获取,也没有提供接口让我们去获取,其实这也比较好理解,因为Widget本身就只是一个配置信息,这点在Widget源码中注释也有体现:“Describes the configuration for an [Element].”
再从Element
树入手,通过对Element
源码的阅读,Element
实现了BuildContext
,而BuildContext
它定义了一系列的接口去获取父子element
与指定的RenderObject
、指定类型的Widget
、指定的State
等等:
abstract class BuildContext {
…
///搜索Element父节点
void visitAncestorElements(bool visitor(Element element));
///搜索Element子节点
void visitChildElements(bool visitor(Element element));
T findAncest