揭秘!如何用Flutter设计一个100%准确的埋点框架?

 

阿里妹导读:用户行为埋点是用来记录用户在操作时的一系列行为,也是业务做判断的核心数据依据,如果缺失或者不准确将会给业务带来不可恢复的损失。闲鱼将业务代码从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的页面内来进行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混合开发的方案,因此接下来也是基于此来阐述闲鱼的方案。

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

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

 

 

看到这个方案可能会有人问,为什么这么绕,为什么不全部交给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/518157.shtml

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

相关文章

安装rzsz

yum install -y lrzsz

编码规范 | Java函数优雅之道(下)

上文背景 本文总结了一套与Java函数相关的编码规则&#xff0c;旨在给广大Java程序员一些编码建议&#xff0c;有助于大家编写出更优雅、更高质、更高效的代码。 内部函数参数尽量使用基础类型 案例一&#xff1a;内部函数参数尽量使用基础类型 现象描述&#xff1a; // 调…

python3-numpy IO load()、save()、savez()、loadtxt()、savetxt()、tofile()、fromfile()

Numpy 可以读写磁盘上的文本数据或二进制数据。 NumPy 为 ndarray 对象引入了一个简单的文件格式&#xff1a;npy。 npy 文件用于存储重建 ndarray 所需的数据、图形、dtype 和其他信息。 常用的 IO 函数有&#xff1a; load() 和 save() 函数是读写文件数组数据的两个主要函数…

抽象思想解读Linux进程描述符

来源 | 嵌入式客栈责编 | Carol头图 | CSDN 下载自视觉中国内核是怎么工作的&#xff1f;首先要理解进程管理&#xff0c;进程调度&#xff0c;本文开始阅读进程管理部分&#xff0c;首先从进程的抽象描述开始。抽象是软件工程的灵魂&#xff0c;而对于Linux操作系统而言&#…

CentOS Linux 7.7 安装kafka zookeeper

文章目录一、软件下载1. zookeeper2. kafka二、安装与启动2.1. jdk2.2. zookeeper2.3. kafka三、 kafka 基本演示一、软件下载 1. zookeeper http://archive.apache.org/dist/zookeeper/zookeeper-3.5.7/apache-zookeeper-3.5.7-bin.tar.gz 2. kafka https://archive.apach…

《Java开发手册》2019最新版发布!

致全球Java开发者&#xff1a; 代码是二进制世界的交流方式&#xff0c;极致的代码是我们的荣耀。 2017年春天&#xff0c;《阿里巴巴Java开发手册》发布&#xff0c;我们希望在涵盖编程规约、异常日志、单元测试、安全规约、MySQL数据库、工程规约、设计规约等7个维度上为开…

IDEA 惊天 bug:进程已结束,退出代码 1073741819

来源 | 沉默王二责编 | Carol头图 | CSDN 下载自视觉中国今天要写的文章中涉及到一串代码&#xff0c;关于 Undertow 的一个入门示例&#xff0c;贴出来大家看一下。public class UndertowTest {public static void main(final String[] args) {Undertow server Undertow.buil…

python3-Anaconda3 基本使用

1、下载 最新版本官网下载&#xff1a; https://www.anaconda.com/distribution/ 历史版本 清华镜像&#xff08;国内首选&#xff09;&#xff1a; https://mirrors.tuna.tsinghua.edu.cn/anaconda/archive/ 历史版本 官网镜像&#xff1a; https://repo.anaconda.com/archi…

淘宝应用柔性架构的探索

导读&#xff1a;随着淘宝业务的飞速发展&#xff0c;微服务架构在持续演进的过程中&#xff0c;也受到了越来越多的挑战&#xff1a;如同步模型带来的资源利用率有限、依赖调用并发度有限、下游故障引发应用自身出问题&#xff1b;又如静态限流随着业务代码的演进、依赖拓扑的…

js 传递参数中文乱码

换成这种方式接收 function getQueryString(key){var reg new RegExp("(^|&)"key"([^&]*)(&|$)");var result window.location.search.substr(1).match(reg);return result?decodeURIComponent(result[2]):null; }

iOS13 一次Crash定位 - 被释放的NSURL.host

每年一次的iOS升级&#xff0c;都会给开发者带来一些适配工作&#xff0c;一些原本工作正常的代码可能就会发生崩溃。 本文讲到了一种 CoreFoundation 对象的内存管理方式在iOS13上遇到的问题。 1. 问题 iOS 13 Beta 版本上&#xff0c;手淘出现了一个必现的崩溃&#xff1a; …

面试官吐槽:“Python程序员就是不行!”网友:我能把你面哭!

最近几年&#xff0c;Python莫名火了起来&#xff0c;很多公司都想赶上这“莫名”的热潮&#xff0c;招聘到大牛人才。但是&#xff0c;最近一个HR在社交网站的吐槽又火了&#xff1a;那么问题来了&#xff0c;市面上为什么鲜有企业满意的优秀的Python程序员&#xff1f;企业到…

python3-pandas 数据结构 Series、DataFrame 基础

Pandas 应用 Pandas 的主要数据结构是 Series &#xff08;一维数据&#xff09;与 DataFrame&#xff08;二维数据&#xff09;&#xff0c;这两种数据结构足以处理金融、统计、社会科学、工程等领域里的大多数典型用例。 数据结构 Series 是一种类似于一维数组的对象&#xf…

十年磨一剑 | 淘宝如何打造承载亿级流量的首页?

阿里妹导读&#xff1a;手机淘宝作为整个互联网领域旗舰 APP 之一&#xff0c;装机量和用户访问量都是名列前茅的。而首页作为打开手机淘宝的门面&#xff0c;是淘宝电商领域的主要流量入口和服务消费者的核心阵地&#xff0c;其业务的复杂性之高、系统的稳定性之重都有着极高的…

switchhosts 没有修改hosts的权限解决方案

使用swtichHost工具切换开发环境时候提示没有权限问题&#xff0c;如下图。。 解决方案有两点 1、进入 C:\Windows\System32\drivers\etc右键点击hosts的属性查看 属性的只读是否被勾选了&#xff0c;如果被勾选了将勾选勾去掉 上述完成后以管理员身份运行&#xff08;管理员…

闲鱼亿级商品结构化背后的思考和演进

1. 缘起 闲鱼是一个典型的C2C场景的闲置交易平台。每个在闲鱼的用户都能享受到自由交易的乐趣。在这里&#xff0c;可能你只要简单的输入商品名&#xff0c;商品价格&#xff0c;库存等信息就能完成一个商品的发布。即便是发布以后&#xff0c;你也可以随时修改价格&#xff0…

QingStor NeonSAN跻身四强 新风口下的青云QingCloud正在厚积薄发

人类以日新月异的速度刷新着科技的成果&#xff0c;其中存储的发展历史尤其悠久&#xff0c;堪称万年进化史。自文明诞生以来&#xff0c;我们就一直在寻求能够更有效存储信息的方式&#xff0c;从4万年前的洞穴壁画、6000年前泥板上的楔形文字&#xff0c;到今天正在普及的SSD…

python3-pandas DataFrame 索引、bool索引、pandas 字符串方法

1、DataFrame 索引 1.1 普通索引取值 pandas 取行或者列的注意点&#xff1a; 方括号写数组&#xff0c;表示取行&#xff0c;对行进行操作方括号写字符串&#xff0c;表示取列&#xff0c;对列进行操作 import pandas as pd import numpy as np # pandas 取行或者列的注意…

MySQL8.0.17 - 初探 Clone Plugin

MySQL8.0.17推出了一个重量级的功能&#xff1a;clone plugin。允许用户可以将当前实例进行本地或者远程的clone。这在某些场景尤其想快速搭建复制备份或者在group replication里加入新成员时非常有用。本文主要试玩下该功能&#xff0c;并试图阐述下其实现的机制是什么。 我们…

javascript判断IPV6格式

随着ipv6的普及&#xff0c;在web上添加ip6地址的要求逐渐增多&#xff0c;下面这个函数是我按照ipv6的定义写的判断&#xff0c;可以用来判断正常的,缩写的ipv6格式&#xff0c;同时可以判断ipv6和ipv4混合的格式&#xff1a; //统计 10F: 或者:10B的个数 function cLength(s…