小程序数据埋点实践之曝光量

什么是数据埋点

所谓数据埋点就是应用在规定流程中 对特定行为或事件进行数据采集 。使用采集的数据做用户分析和页面分析,可以获得应用的总体使用情况,为后续优化产品和运营提供数据支撑。常见数据埋点内容包括:访问量、停留时长、曝光量、点击量、跳出率等等。

微信小程序也为我们提供了自定义分析统计,其中包括 API 上报(代码埋点),填写配置(无埋点,只需在公众后台配置)。而第三方统计平台比较有名的就是阿拉丁统计,只需引入集成的 SDK,开发成本低,能够满足大部分的需求。

数据埋点需要分析页面流程,确定埋点需求,选择埋点方式。如果是代码埋点,主要关注触发时机、条件判断、捕获数据,其次要注意是否有遗漏的场景没有做到埋点。代码埋点虽然成本较大(侵入代码),但是精准度较高,能够很好的满足埋点需求。

什么是曝光量

曝光量顾名思义是 指定元素出现在可观察视图内的次数 ,也可以理解为展示量。

通常我们会使用 点击量 / 曝光量 得出 点击率 ,作为衡量一个内容是否受用户喜爱的指标之一。比如,曝光 100 次只有 10 人点击,和曝光 100 次 有 100 个人点击,很明显后者更受用户喜爱。利用这些数据参考,可以推荐更多用户喜爱的内容,以此来留住用户。

交叉观察者

IntersectionObserver 接口,提供了一种异步观察 目标元素与其祖先元素或顶级文档视窗(viewport)交叉状态 的方法,祖先元素与视窗(viewport)被称为根(root)。简单来说就是,观察的目标是否和祖先元素和视窗发生交叉,即进入或离开。

小程序从基础库 1.9.3 开始支持 wx.createIntersectionObserver 接口(组件内使用 this.createIntersectionObserver ),使用此接口可创建 IntersectionObserver对象 。对此接口不了解的可以查看官方文档

基础使用

// 创建实例
let ob = this.createIntersectionObserver()
// 相对于文档视窗监听
ob.relativeToViewport()
    .observe(‘.box‘, res => {
        // res.intersectionRatio 为相交比例
        if (res.intersectionRatio > 0) {
            console.log(‘进入页面‘)
        } else {
            console.log(‘离开页面‘)
        }
    })

阈值

在创建实例时可以传入一些配置,其中 thresholds (阈值)是比较重要的一项配置,它可以控制触发回调的时机。 thresholds 是一个数字类型的数组,默认为 [0] 。即相交比例为 0 时触发一次回调,下面我们来设置阈值,看看会有什么改变:

// 创建实例
let ob = this.createIntersectionObserver({
    thresholds: [0, 0.5, 1]
})

从图上可以看到,元素在相交比例为 00.51 都各自触发了一次回调。在统计曝光量设置阈值非常有用,通常我会设置为 1 ,表示元素要完全展示在页面上才会进行记录,这样数据会更加真实准确。

收缩和扩展参照区域

除了阈值之外还有另一项重要的设置,在使用 relativeTorelativeToViewport 规定参照区域时,我们可以传入配置 margins 来收缩和扩展参照区域。 margins 包括 leftrighttopbottom 四个参数配置。

// 创建实例
let ob = this.createIntersectionObserver()
// 相对于文档视窗监听
ob.relativeToViewport({
        bottom: -330
    })
    .observe(‘.box‘, res => {
        // res.intersectionRatio 为相交比例
        if (res.intersectionRatio > 0) {
            console.log(‘进入页面‘)
        } else {
            console.log(‘离开页面‘)
        }
    })

上面将参照区域底部收缩 330px,可以理解为整体的区域从底部开始被裁剪 330px,因此元素只有进入页面上半区才会触发回调。

进入正题

经过以上一些介绍,相信大家对交叉观察者的好处和使用都了解的差不多。接下来进入正题 ~

背景

此次我做的项目是资讯类目的小程序,主要用于发布和转载一些学术文章。对于这种资讯的项目,需要通过数据埋点来收集用户的阅读习惯,以此来为用户推荐文章。

埋点方面用微信后台提供的自定义分析以文章为单位进行收集,而我们自己后台会以用户为单位进行收集。前者得出整体用户阅读偏好和文章热度,后者主要精确到用户,分析用户单位的阅读偏好。

改造组件

在分析页面布局和pm的商讨后,多处需要统计曝光量的文章区域展示都大致相同,刚好也在封装的列表组件里。于是将收集曝光量的逻辑都交由组件内部处理。

组件改造:

  1. 定义 isObserver 属性,该属性由外部传入的布尔值控制是否收集曝光量
  2. 监听传入的 list ,为每个元素绑定交叉观察者

以下部分代码省略,只展示主要逻辑:

<block wx:for="{{list}}" wx:key="id">
    <view class="artic-item artic-item-{{index}}" data-id="{{item.id}}" data-index="{{index}}">
    </view>
</block>
const app = getApp()
Component({
    data: {
        currentLen: 0
    }
    properties: {
        list: {
            type: Array,
            value: []
        },
        isObserver: {
            type: Boolean,
            value: false
        }
    },
    observers: {
        list(list) {
            if (this.data.isObserver === false) {
                return
            }
            if (list.length) {
                // currentLen 记录当前列表的长度
                // 用于计算监听元素的索引,对已经监听过的元素不再重复监听
                let currentLen = this.data.currentLen
                for (let i = 0; i < list.length - currentLen; i++) {
                    let ob = this.createIntersectionObserver({
                        thresholds: [1]
                    })
                    ob.relativeToViewport()
                        .observe(‘.artic-item-‘ + (currentLen + i), res => {
                            // 获取元素的dataset
                            let {
                                id,
                                index
                            } = res.dataset
                            if (res.intersectionRatio === 1) {
                                // 此处收集曝光量,内部处理逻辑会在下面提及
                                this.sendExsureId(id)
                                // 元素出现后取消观察者监听,避免重复触发
                                ob.disconnect()
                            }
                        })
                }
            }
            this.data.currentLen = list.length
        }
    }
})

发现??

理想情况应该是切换到第二个分类打印3个文章,但由于组件开始记录第一个分类列表的 currentLen ,在切换到第二个分类时, currentLen 没有被清除,导致循环长度错误。

解决:首先记录列表第一项的 id ,当监听列表变化,用新列表的第一项 id 作与之比较。若不相等,则表示列表被重新赋值,此时将 currentLen 置为0。

Component({
    data: {
        flagId: 0,
        currentLen: 0
    }
    properties: {
        list: {
            type: Array,
            value: []
        },
        isObserver: {
            type: Boolean,
            value: false
        }
    },
    observers: {
        list(list) {
            if (this.data.isObserver === false) {
                return
            }
            if (list.length) {
                // 比较id
                if (this.data.flagId != list[0].id) {
                    this.data.currentLen = 0
                }
                let currentLen = this.data.currentLen
                for (let i = 0; i < list.length - currentLen; i++) {
                    let ob = this.createIntersectionObserver({
                        thresholds: [1]
                    })
                    ob.relativeToViewport()
                        .observe(‘.artic-item-‘ + (currentLen + i), res => {
                            let {
                                id,
                                index
                            } = res.dataset
                            if (res.intersectionRatio === 1) {
                                this.sendExsureId(id)
                                ob.disconnect()
                            }
                        })
                }
            }
            // 设置列表第一项id
            this.data.flagId = list[0] ? list[0].id : 0
            this.data.currentLen = list.length
        }
    }
})

组件优化

因为需要提前监听文章的相交状态,在 list 传入时就开始循环 observe 。现在假设一个场景,在进入页面时,已经为一些文章注册完成回调,但用户并没有看过这些文章就退出页面。那是不是表示这些实例都没有被 disconnect

解决:在 observe 时将每一个观察者实例存入数组,当组件销毁时检查数组中是否有观察者实例,如果有,则调用这些实例的 disconnect

Component({
    data: {
        currentLen: 0,
        obItems: [] // 存放实例的数组
    },
    observers: {
        list(list) {
            if (this.data.isObserver === false) {
                return
            }
            if (list.length) {
                if (this.data.flagId != list[0].id) {
                    this.data.currentLen = 0
                    // 取消实例的监听
                    this.removeObItems()
                }
                let currentLen = this.data.currentLen
                for (let i = 0; i < list.length - currentLen; i++) {
                    let ob = this.createIntersectionObserver({
                        thresholds: [1]
                    })
                    ob.relativeToViewport().observe(‘.artic-item-‘ + (currentLen + i), res => {
                        let {
                            index,
                            id
                        } = res.dataset
                        if (res.intersectionRatio === 1) {
                            this.sendExsureId(id)
                            ob.disconnect()
                            // 取消监听后 将实例移出数组
                            this.data.obItems.shift()
                        }
                    })
                    // 将实例存入数组
                    this.data.obItems.push(ob)
                }
            } else {
                // 取消实例的监听
                this.removeObItems()
            }
            this.data.flagId = list[0] ? list[0].id : 0
            this.data.currentLen = list.length
        }
    },
    lifetimes: {
        detached() {
            // 组件销毁时 取消实例的监听
            this.removeObItems()
        }
    },
    methods: {
        removeObItems() {
            if (this.data.obItems.length) {
                this.data.obItems.forEach(ob => {
                    ob.disconnect()
                })
            }
        }
    }
})

收集处理

现在组件能够收集到曝光文章的ID,剩下的就是往后台发送数据。那么问题来了,难道文章曝光一次就发起一次请求吗?如果不怕和后端同事干架的话,你可以这么做。要知道多次发起请求,服务器??会很大。用户量比较大后,对服务器能够承受的并发量会有很大的考验。所以正确的做法应该是,把收集到的ID缓存起来,在达到一定数量的时候一起发送过去。

接下来对收集的数据做些处理:

// 这个上面收集曝光量的函数
sendExsureId(id) {
    if (typeof app.globalData.exposureIds === ‘undefined‘) {
        // exposureIds 是定义在全局用于存放曝光文章 ID 的数组
        app.globalData.exposureIds = []
    }
    app.globalData.exposureIds.push(id)
    // 当数组到达 50 个,开始上报数据
    if (app.globalData.exposureIds.length >= 50) {
        wx.$api.recordExposure({
            // 因为 ID 比较多,我和后端约定好使用逗号分隔
            ids: app.globalData.exposureIds.join(‘,‘)
        })
        // 上报后清空数组
        app.globalData.exposureIds = []
    }
}

看起来好像实现到这里就大功告成,但是我们还要考虑一种情况。假如用户只看了 40 个就退出小程序,而上报条件是达到 50 个才会发送数据,那么这部分有用的数据就会被丢失。因为小程序没有回调能够监听到小程序被销毁,这里只能使用小程序的 onHide 函数来做些事情。当小程序进入后台时 onHide 函数就会被执行,此时可以在函数里上报数据。

App({
    onHide() {
        if (this.globalData.exposureIds.length) {
            wx.$api.recordExposure({
                ids: this.globalData.exposureIds.join(‘,‘)
            })
            this.globalData.exposureIds = []
        }
    }
})

写在最后

说实话,在埋点这方面的知识不算很熟悉,业务场景也比较简单。因为没有大佬指导,也是看着需求往这方面去做,有哪里错误或遗漏请指出。如果你有更好的方案或经验,欢迎评论区交流??~

原文地址:https://www.cnblogs.com/chanwahfung/p/12676918.html

时间: 2024-10-11 22:45:04

小程序数据埋点实践之曝光量的相关文章

微信小程序数据请求方法wx.request小测试

微信小程序数据请求方法 wx.request wxml文件: <view> <textarea value="{{textdata}}"/> </view> <button bindtap="RequestData" value="Button">Button</button> 主要是一个按钮,点击后将请求的数据写入到textarea中 js文件: Page({ data:{ textd

微信小程序开发入门与实践

基础知识---- MINA 框架 为方便微信小程序开发,微信为小程序提供了 MINA 框架,这套框架集成了大量的原生组件以及 API.通过这套框架,我们可以方便快捷的完成相关的小程序开发工作. MINA 框架提供了自己的视图层描述语言 WXML 和 WXSS,以及基于 JavaScript 的逻辑层框架,并在视图层与逻辑层间提供了数据传输和事件系统,因此我们主要聚焦于数据与逻辑上. 响应的数据绑定 框架的核心是一个响应的数据绑定系统. 整个系统分为两块:视图层(View)和逻辑层(App Ser

微信小程序“信用卡还款”项目实践

小程序概述 11月3日晚,微信团队对外宣布,微信小程序开放公测.开发者可登陆微信公众平台申请,开发完成后可以提交审核,公测期间暂不能发布. 我们前一段时间也进行了小程序开发,现在来对之前的开发体验做一个总结. 1. 小程序是什么? 微信小程序是一种介于原生app.和web app的hybrid.通过微信进行加载,实现类似原生app的流畅.相对原生app来说,小程序更加轻量.更新实时.跨平台:相对web app来说,小程序资源离线,体验更流畅. 微信小程序的设计目标是通过尽可能简单.高效的方式让开

微信小程序-数据缓存

每个微信小程序都可以有自己的本地缓存,可以通过 wx.setStorage(wx.setStorageSync).wx.getStorage(wx.getStorageSync).wx.clearStorage(wx.clearStorageSync)可以对本地缓存进行设置.获取和清理.本地缓存最大为10MB. 注意: localStorage 是永久存储的,但是我们不建议将关键信息全部存在 localStorage,以防用户换设备的情况. wx.setStorage(OBJECT) 将数据存储

小程序数据签名校验-Java端

JDK内置的签名算法不包含小程序需要的(对称解密使用的算法为 AES-128-CBC,数据采用PKCS#7填充),所以需要引用第三方jar. compile group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '1.54' 以下是具体的代码: //自行在构造函数中赋值 String sessionKey; /** * AES-128-CBC解密 * 使用PKCS填充 * @param encryptedData * @param

商城类小程序源码,优质资源分享!(公众号小程序数据互通)

给大家分享一套不错的小程序公众号商城源码! CRMEB是通用ThinkPhp5.0+Vue+EasyWeChat 开发的一套新零售商城系统: 将源码安装成功后.通过配置开放平台后,统一用户ID身份,用户数据互通,系统共用一个后台,后台产品发布和用户数据是同步的: 系统后台采用form-builder组件和后台多任务操作框架,方便快速开发增删改.封装layui.table 可快速开发数据列表页面.PHPExcel数据导出.数据库在线词典.日志小工具.系统参数配置.系统强大完善的权限控制.系统菜单配

C4J私有化的小程序数据统计分析

线上流量红利逐渐消失的今天,产品的获客成本从几元,飙升到了现在的几百元,甚至上千元,流量大战悄然打响!而 "微信社会"中的小程序,凭借着离消费者一指之遥的距离,成为商家不被淘汰的制胜法宝 . 2019年4月,开源的用户行为分析厂商Cobub推出全渠道的用户增长利器-C4J,即Cobub for Java,全新一代低成本.私有化.全渠道.高性能的用户行分析平台.新增对小程序模块的统计分析,实现了产品多渠道数据的打通,形成跨渠道用户全生命周期的行为数据流, 从而提升用户体验,驱动用户及业务

原生微信小程序数据渲染

一直在写vue,第一次接触微信小程序,还是原生,最开始做的时候真的很闹心啊啊啊啊啊啊啊啊啊啊啊啊!!所以最近大概更新的都是微信小程序原生的内容了~~么么哒!!一定会继续努力的!!tips:在小程序项目中,app.json文件里的 { //页面配置路径 "pages":[ "page/index/index", //默认首页 "page/index/logs", ] } 正题:微信小程序原生数据渲染条件渲染:wx:if wx:elif wx:els

微信小程序数据过滤(filter)方法

因为微信小程序的wxml和js的内部实现机制是分开编译的.所以在wxml是没办法调用js的函数的.这会导致WXML缺少一个我们常用的功能,那就是没有办法在视图层对数据做一些特殊处理.比如我们从后端获取到一个时间戳,然后需要在界面上把这些日期都格式化显示为2017-01-01这种格式的日期形式,在Vue, Angular之类的前端Web框架中,提供了如filter之类相应比较好用的方案.小程序是没有这些方法的.但是最近小程序推出了wxs类型文件就是解决这类问题的. 使用 首先新建filter.wx