常见的数据埋点处理

业务说明及遇到的问题

  • 数据埋点、上报在电商业务中非常常见,前端也经常做,实际场景无非就是:记录商品浏览量、记录下单付款行为、记录取消订单行为等…

在我们公司的实际业务中,涉及到的如下图:
埋点业务
可以看到是以商品ID为主对象,进行一系列记录;看似没有任何难点,只要有对应的行为,那么请求接口上报即可,但这种常规的做法此时出现了极大的问题;

  • 遇到的问题:因为平台的用户量太大了,光是注册用户,就达到了1000万,巅峰时期平台销售额达到500万元;按照以前的埋点经验,仅仅埋点上报这一个业务需求,如果用户浏览一个商品进行一次操作就请求一次接口,超高并发直接会让服务器奔溃,根本扛不住,这时候前端开发就要想办法去优化。

处理方式与思路

思路:之前是浏览一个商品/行为操作,就上报一次,现在改为 浏览多个商品后,累积到一定数量或每隔几分钟上报一次,集中间隔性上报,大大缓解服务器并发压力,解决问题。

具体处理方式:
在localStorage中,批量存储需要上报的数据列表,达到触发条件时进行上报,清除本地现有数据
localStorage
实际上报请求

完整代码:
实际业务场景中,每3分钟或累计数量达到50条,就进行上报;而且相同的商品,在原有数据基础上更新其它行为差异,保持ID唯一性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
import { getSystemType } from './axiosSet'
import local from '@/Global/utils/localStorage'
import store from '@/store'
import axios from 'axios'
import Handle from '@/Global/utils/handle'
import CloneDeep from 'lodash.clonedeep'
import { throttle } from 'throttle-debounce'

const DelayTime = 3 * 60 * 1000 // 延时提交时间(单位:毫秒)
const MaxNum = 50 // localStorage中DataReportArr数组的最大长度
const _Axios = axios.create({ timeout: 55000 })
const apiUrl = 'post /v1/stat-goods/report'
let timer = 0

const DataReportFn = (params = {}, config = {}, otherConfig = {}) => {
try {
const token = local.get('accessToken')
const user = store.state.userInfo
if (!token || !user.member.id) {
console.log('没有token或者没有用户信息')
return
}
const systemInfo = local.get('systemParams')
if (!systemInfo || Object.keys(systemInfo).length === 0) {
console.log('没有初始化')
return
}
if (+systemInfo.stat2_is_open !== 1) {
console.log('stat2_is_open !==1 => 没有打开统计')
return
}
if (otherConfig.headers) {
otherConfig.headers.Authorization = `Bearer ${local.get('accessToken')}`
} else {
otherConfig.headers = { Authorization: `Bearer ${local.get('accessToken')}` }
}
const apiUrlArr = apiUrl.split(' ')
if (apiUrlArr.length === 1) {
apiUrlArr.unshift('get')
}

if (!params || Object.keys(params).length === 0) {
console.log('没有传入params,无法统计')
return
}

const { goodsIdArr, actionType } = params
if (!goodsIdArr || goodsIdArr.length === 0 || !Handle.isArray(goodsIdArr)) {
console.log('没有传入goodsIdArr,无法统计')
return
}
if (!actionType) {
console.log('没有传入actionType,无法统计')
return
}

let DataReportArr = local.get('DataReportArr') || []

// 进入方式 1首页、2扫码、3链接
let entry_type = 1
const share_from = Handle.getQueryByName(window.location.href, 'share_from') || null
const _index = []// 首页
const _qrcode = ['store_img', 'poster', 'qr_code', 'goods_img']// 扫码
const _link = ['store_link', 'link', 'goods_link']// 链接

if (share_from) {
if (_qrcode.includes(share_from)) { // 扫码
entry_type = 2
} else if (_link.includes(share_from)) { // 链接
entry_type = 3
} else { // 首页
entry_type = 1
}
} else {
entry_type = 1
}

const defaultStatData = {
entry_type: entry_type ? +entry_type : 1, // 进入方式 1首页、2扫码、3链接
credit_open_qty: 0, // 先用后付开通次数(无则传0, APP端才使用, 小程序公众号不管)
// ...
goods_id: 0, // 商品ID
exposure_qty: 0, // 商品曝光次数(无则传0)
detail_qty: 0, // 商品详情次数(无则传0)
share_qty: 0, // 分享次数(无则传0)
confirm_qty: 0, // 确认订单次数(无则传0)
cash_qty: 0, // 收银台次数(无则传0)
pay_qty: 0, // 支付次数(无则传0)
cancel_qty: 0 // 取消次数(无则传0)
}

const _goodsIdArr = Array.from(new Set(CloneDeep(goodsIdArr))) // 商品ID数组去重
for (let item of _goodsIdArr) {
// 本地存储中完全无数据 或 本地存储中之前没有记录过该商品ID 直接push
if (DataReportArr.length === 0 || (DataReportArr && !DataReportArr.some(it => +it.goods_id === +item))) {
const _defaultStatData = CloneDeep(defaultStatData)
for (let key in _defaultStatData) {
if (key === actionType) {
_defaultStatData[key] = ++_defaultStatData[key]
break
}
}
DataReportArr.push({ ..._defaultStatData, goods_id: +item })
} else { // 本地已存在该商品ID
for (let obj of DataReportArr) {
if (+obj.goods_id === +item) {
for (let key in obj) {
if (key === actionType) {
obj[key] = ++obj[key]
break
}
}
break
}
}
}
}

local.set('DataReportArr', DataReportArr) // 将临时数据存储至localStorage中

const _submit = throttle(3000, true, function () {
if (!local.get('DataReportArr')) {
console.log('localStorage中无DataReportArr数据, 不执行上报, 退出')
return
}
const _params = {
systemType: getSystemType(),
platform_type: getSystemType(),
stat: JSON.stringify(local.get('DataReportArr'))
}
return _Axios({
method: apiUrlArr[0],
url: `${systemInfo.stat2_domain_name}${apiUrlArr[1]}`,
data: _params,
params: apiUrlArr[0].toLowerCase() === 'get' ? _params : {},
...otherConfig
}).then((res) => {
if (+res.data.code === 0) {
// 提交成功后清除本地数据
local.remove('DataReportArr')
return res.data.data
} else {
throw res.data
}
}).catch(err => {
console.log(err)
})
})

if (local.get('DataReportArr') && local.get('DataReportArr').length >= +MaxNum) { // 本地存储中累计数据达到最大条数, 自动提交一次
_submit()
return
}

// 一段时间后自动提交
!timer && (timer = setTimeout(() => {
clearTimeout(timer)
timer = 0
_submit()
}, DelayTime))
} catch (err) {
console.log(err)
}
}

export default DataReportFn

比如在商品详情,直接调用封装好的方法,传入ID和行为特征即可:

1
this.DataReport({ goodsIdArr: [+this.id], actionType: 'detail_qty' })

总结

很多很简单常见的业务需求,看似非常好实现,但是当用户量、性能等因素都综合起来的话,就要考虑很多东西了,技术与方法的实现永远不是最重要的,业务理解与思路的提升,长久积累才是对开发者最大的提升。


常见的数据埋点处理
https://liujiaweb.cn/posts/22507.html
作者
Liu Jia
发布于
2021年10月1日
许可协议