揭秘!一个高准确率的Flutter埋点框架如何设计

背景

用户行为埋点是用来记录用户在操作时的一系列行为,也是业务做判断的核心数据依据,如果缺失或者不准确将会给业务带来不可恢复的损失。闲鱼将业务代码从Native迁移到Flutter上过程中,发现原先Native体系上的埋点方案无法应用在Flutter体系之上。而如果我们只把业务功能迁移过来就上线,对业务是极其不负责任的。因此,经过不断探索,我们沉淀了一套Flutter上的高准确率的用户行为埋点方案。

用户行为埋点定义

先来讲讲在我们这里是如何定义用户行为埋点的。在如下用户时间轴上,用户进入A页面后,看到了按钮X,然后点击了这个按钮,随即打开了新的页面B。

这个时间轴上有如下5个埋点事件发生:

  • 进入A页面。A页面首帧渲染完毕,并获得了焦点。
  • 曝光坑位X。按钮X处于手机屏幕内,且停留一段时间,让用户可见可触摸。
  • 点击坑位X。用户对按钮X的内容很感兴趣,于是点击了它。按钮X响应点击,然后需要打开一个新页面。
  • 离开A页面。A页面失去焦点。
  • 进入B页面。B页面首帧渲染完毕,并获得焦点。

在这里,打埋点最重要的是时机,即在什么时机下的事件中触发什么埋点,下面来看看闲鱼在Flutter上的实现方案。

实现方案

进入/离开页面

在Native原生开发中,Android端是监听Activity的onResume和onPause事件来做为页面的进入和离开事件,同理iOS端是监听UIViewController的viewWillAppear和viewDidDisappear事件来做为页面的进入和离开事件。同时整个页面栈是由Android和iOS操作系统来维护。

在Flutter中,Android和iOS端分别是用FlutterActivity和FlutterViewController来做为容器承载Flutter的页面,通过这个容器可以在一个Native的页面内(FlutterActivity/FlutterViewController)来进行Flutter原生页面的切换。即在Flutter自己维护了一个Flutter页面的页面栈。这样,原来我们最熟悉的那套在Native原生上方案在Flutter上无法直接运作起来。

针对这个问题,可能很多人会想到去注册监听Flutter的NavigatorObserver,这样就知道Flutter页面的进栈(push)和出栈(pop)事件。但是这会有两个问题:

  • 假设A、B两个页面先后进栈(A enter -> A leave -> B enter)。然后B页面返回退出(B leave),此时A页面重新可见,但是此时是收不到A页面push(A enter)的事件。
  • 假设在A页面弹出一个Dialog或者BottomSheet,而这两类也会走push操作,但实际上A页面并未离开。

好在Flutter的页面栈不像Android Native的页面栈那么复杂,所以针对第一个问题,我们可以来维护一个和页面栈匹配的索引列表。当收到A页面的push事件时,往队列里塞一个A的索引。当收到B页面的push事件时,检测列表内是否有页面,如有,则对列表最后一个页面执行离开页面事件记录,然后再对B页面执行进入页面事件记录,接着往队列里塞一个B的索引。当收到B页面的pop事件时,先对B页面执行离开页面事件记录,然后对队列里存在的最后一个索引对应的页面(假设为A)进行判断是否在栈顶(ModalRoute.of(context).isCurrent
),如果是,则对A页面执行进入页面事件记录。

针对第二个问题,Route类内有个成员变量overlayEntries,可以获取当前Route对应的所有图层OverlayEntry,在OverlayEntry对象中有个成员变量opaque可以判断当前这个图层是否全屏覆盖,从而可以排除Dialog和BottomSheet这种类型。再结合问题1,还需要在上述方案中加上对push进来的新页面来做判断是否为一个有效页面。如果是有效页面,才对索引列表中前一个页面做离开页面事件,且将有效页面加到索引列表中。如果不是有效页面,则不操作索引列表。

以上并不是闲鱼的方案,只是笔者给出的一个建议。因为闲鱼APP在一开始落地Flutter框架时,就没有使用Flutter原生的页面栈管理方案,而是采用了Native+Flutter混合开发的方案。具体可参考前面的一篇文章《已开源|码上用它开始Flutter混合开发——FlutterBoost》。因此接下来也是基于此来阐述闲鱼的方案。

闲鱼的方案如下(以Android为例,iOS同理):

注:首次打开指的是基于混合栈新打开一个页面,非首次打开指的是通过回退页面的方式,在后台的页面再次到前台可见。

看似我们将何时去触发进入/离开页面事件的判断交给Flutter侧,实际上依然跟Native侧的页面栈管理保持了一致,将原先在Native侧做打埋点的时机告知Flutter侧,然后Flutter侧再立刻通过channel来调用Native侧的打埋点方法。那么可能会有人问,为什么这么绕,不全部交给Native侧去直接管理呢?交给Native侧去直接管理这样做针对非首次打开这个场景是合适的,但是对首次打开这个场景却是不合适的。因为在首次打开这个场景下,onResume时Flutter页面尚未初始化,此时还不知道页面信息,因此也就不知道进入了什么页面,所以需要在Flutter页面初始化(init)时再回过来调Native侧的进入页面埋点接口。为了避免开发人员去关注是否为首次打开Flutter页面,因此我们统一在Flutter侧来直接触发进入/离开页面事件。

曝光坑位

先讲下曝光坑位在我们这里的定义,我们认为图片和文本是有曝光意义的,其他用户看不见的是没有曝光意义的,在此之上,当一个坑位同时满足以下两点时才会被认为是一次有效曝光:

  • 坑位在屏幕可见区域中的面积大于等于坑位整体面积的一半。
  • 坑位在屏幕可见区域中停留超过500ms。

基于此定义,我们可以很快得出如下图所示的场景,在一个可以滚动的页面上有A、B、C、D共4个坑位。其中:

  • 坑位A已经滑出了屏幕可见区域,即invisible;
  • 坑位B即将向上从屏幕中可见区域滑出,即visible->invisible;
  • 坑位C还在屏幕中央可视区域内,即visible;
  • 坑位D即将滑入屏幕中可见区域,invisible->visible;

那么我们的问题就是如何算出坑位在屏幕内曝光面积的比例。要算出这个值,需要知道以下几个数值:

  • 容器相对屏幕的偏移量
  • 坑位相对容器的偏移量
  • 坑位的位置和宽高
  • 容器的位置和宽高

其中坑位和容器的宽和高很容易获取和计算,这里就不再累述。

获取容器相对屏幕的偏移量

//监听容器滚动,得到容器的偏移量
double _scrollContainerOffset = scrollNotification.metrics.pixels;

获取坑位相对容器的偏移量

//曝光坑位Widget的context
final RenderObject childRenderObject = context.findRenderObject();
final RenderAbstractViewport viewport = RenderAbstractViewport.of(childRenderObject);
if (viewport == null) {return;
}
if (!childRenderObject.attached) {return;
}
//曝光坑位在容器内的偏移量
final RevealedOffset offsetToRevealTop = viewport.getOffsetToReveal(childRenderObject, 0.0);

逻辑判断

if (当前坑位是invisible && 曝光比例 >= 0.5) {记录当前坑位是visible状态记录出现时间
} else if (当前坑位是visible && 曝光比例 < 0.5) {记录当前坑位是invisible状态if (当前时间-出现时间 > 500ms) {调用曝光埋点接口}
}

点击坑位

点击坑位埋点没什么难点,很容易就可以想到下面的方案:

效果

经过多轮迭代和优化,目前线上Flutter页面的埋点准确率已经达到100%,有力地支持了业务的分析和判断。同时这套方案让业务同学在做开发时,对于页面进入/离开、曝光坑位可以做到无感知,即不用关心何时去触发,做到了简单易用和无侵入性。

展望

此外,针对页面进入/离开这个场景,由于闲鱼是基于Flutter Boost混合栈的方案,因此我们的解决方案还不够通用。不过未来随着闲鱼上的Flutter页面越来越多,我们后续也会去实现基于Flutter原生的方案。

原文链接
本文为云栖社区原创内容,未经允许不得转载。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/518707.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

如何运行没有Root权限的Docker?干货来了!

作者 | Vaibhav Raizada译者 | 天道酬勤责编 | 徐威龙封图| CSDN 下载于视觉中国在本文中&#xff0c;我们讨论了如何在没有root权限的情况下运行Docker&#xff0c;以便更好地管理容器中的安全性。Docker作为Root用户Docker以root用户身份运行其容器。但是你的工作负载真的需要…

搭建主从数据库出现的错误 error connecting to master ‘slave@172.17.0.2:3306‘ - retry-time: 30 retries: 1

在搭建主从数据库的时候出现了报错 出现错误的截图&#xff1a; 解决办法&#xff1a; 重新授权 CREATE USER slave% IDENTIFIED BY 123456; GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO slave%;参考链接: 搭建主从数据库出现的错误error connecting to master …

Java-For循环

public class ForDemo01 {public static void main(String[] args) {int a 1; // 初始化条件while (a<100){ // 条件判断System.out.println(a);a2;}System.out.println("while 循环结束&#xff01;");// 初始化 // 条件判断 // 迭代for(int i1;i<100;i){S…

spring.shardingsphere.rules.sharding.sharding-algorithms.database_inline.props‘ is not valid

spring.shardingsphere.rules.sharding.sharding-algorithms.database_inline.props is not valid 解决方案&#xff1a; 原配置 修改后

以云战“疫”,这次阿里云又让人们惊了……

本文转载自CSDN博主「L-JingJing」的原创文章 近日&#xff0c;阿里云对外宣布其容器服务调度GPU云服务器启动加速计算&#xff0c;最快只需60秒即可完成新冠病毒的核酸对比工作&#xff1b;同时将向医疗科研机构、疾控中心等一线病毒研究机构免费开放基因计算服务&#xff0c…

Java-增强for循环

public class ForDemo05 {public static void main(String[] args) {int[] numbers {10, 20, 30, 40}; // 定义一个数组for (int number : numbers) {System.out.println(number);}} }https://www.bilibili.com/video/BV12J41137hu?p42&spm_id_frompageDriver

五年从P5到P8,在阿里学做个靠谱的人

师兄文化&#xff0c;是阿里的老传统&#xff0c;新人入职都要认个师兄。 不是江湖上这种师兄哈&#xff0c;但帅是一样帅的 今天和大家聊聊我在阿里当师兄的故事。 我是“改之”&#xff0c;不是“有则改之无则加勉”的改之&#xff0c;而是“杨过&#xff0c;字改之”的那…

@开发者,微软 CEO 萨提亚带领 60 位大咖的集结令,你敢接吗?

2020年初&#xff0c;一场突如其来的疫情打乱了所有人的脚步&#xff0c;给人们的生活、工作、学习带来诸多不便&#xff0c;与此同时&#xff0c;我们看到一些企业迅速响应&#xff1a;各式买菜小程序、远程工具、在线教育的火爆……这背后&#xff0c;是企业的数字化转型步伐…

支付宝技术风险负责人陈亮:把事情做到极致,技术的差异性才会体现出来

“很多事情&#xff0c;说出来很多人都在做&#xff0c;但是只有真正做到极致&#xff0c;技术的差异性才会体现出来”&#xff0c;蚂蚁金服技术风险部研究员陈亮&#xff08;花名&#xff1a;俊义&#xff09;在接受 InfoQ 采访时如是说道。在此前的支付宝技术嘉年华&#xff…

Java-break-continue

https://www.bilibili.com/video/BV12J41137hu?p43&spm_id_frompageDriver

2020 年,为什么非要采用 DevOps 文化不可?

来源 | DevOps Zone 译者 | 苏本如&#xff0c;责编 | 夕颜头图 | CSDN 下载自视觉中国出品 | CSDN&#xff08;ID:CSDNnews&#xff09;2020年已经到来&#xff0c;它的到来带来了信息和技术&#xff08;IT&#xff09;领域的诸多创新和变革&#xff0c;特别是对DevOps技术的创…

走进KeyDB

KeyDB项目是从redis fork出来的分支。众所周知redis是一个单线程的kv内存存储系统&#xff0c;而KeyDB在100%兼容redis API的情况下将redis改造成多线程。 网上公开的技术细节比较少&#xff0c;本文基本是通过阅读源码总结出来的&#xff0c;如有错漏之处欢迎指正。 多线程架…

Java-打印三角形

public class TestDemo01 {public static void main(String[] args) {// 打印三角形 5 行for (int i 1; i < 5; i) {// 先打印出左边的 直角三角形for (int j 5; j > i; j--) {System.out.print(" ");}for (int j 1; j<i; j) {System.out.print("*…

Springboot2.x +JPA 集成 Apache ShardingSphere 读写分离

分库分表背景: 数据库性能瓶颈&#xff1a;主要分为按照业务来划分或者按照数据量来划分。 拆分方式&#xff1a; 水平拆分(每个表的结构都一样)&#xff1a;订单表数据量大&#xff0c;我们可以水平拆分 &#xff0c;分成order表1、order表2、order表3 。。。 垂直拆分&#x…

只要 8 个步骤,学会这个 Docker 命令终极教程!

作者 | Timothy Mugayi译者 | 弯月 责编 | 徐威龙封图| CSDN 下载于视觉中国Docker容器已经从一种锦上添花的技术转变成了部署环境的必需品。有时&#xff0c;作为开发人员&#xff0c;我们需要花费大量时间调试或研究Docker工具来帮助我们提高生产力。每一次新技术浪潮来临之际…

优秀工程师必备的一项技能,你解锁了吗?

阿里妹导读&#xff1a;很多程序员在工作一段时间后会遇到迷茫期&#xff0c;虽有技术傍身&#xff0c;也难免会产生焦虑&#xff0c;反复思考怎样才能快速成长。关于如何提高自己的思考力&#xff0c;运用思考的力量推动能力提升&#xff0c;以此实现技术成长&#xff0c;阿里…

Springboot2.x +JPA 集成 Apache ShardingSphere 分表+读写分离

分库分表背景: 数据库性能瓶颈&#xff1a;主要分为按照业务来划分或者按照数据量来划分。 拆分方式&#xff1a; 水平拆分(每个表的结构都一样)&#xff1a;订单表数据量大&#xff0c;我们可以水平拆分 &#xff0c;分成order表1、order表2、order表3 。。。 垂直拆分&#x…

Java-方法重载

https://www.bilibili.com/video/BV12J41137hu?p47&spm_id_frompageDriver

Blink 有何特别之处?菜鸟供应链场景最佳实践

作者&#xff1a;晨笙、缘桥 菜鸟供应链业务链路长、节点多、实体多&#xff0c;使得技术团队在建设供应链实时数仓的过程中&#xff0c;面临着诸多挑战&#xff0c;如&#xff1a;如何实现实时变Key统计&#xff1f;如何实现实时超时统计&#xff1f;如何进行有效地资源优化&a…

为什么要在油气行业中应用 IoT?这 8 个应用场景告诉你 IoT 在油气行业中可以做什么...

作者 | Vova Shevchyk译者 | 风车云马 责编 | 徐威龙封图| CSDN 下载于视觉中国如今&#xff0c;物联网已经进入了各行各业&#xff1a;汽车、农业、绿色能源。物联网还将征服的领域之一是石油和天然气领域。在这些特殊的行业环境中&#xff0c;公司雇佣专业人员来预测机器何时…