Justin's Words

用户行为系统

什么是用户行为系统

用户行为系统应该是一个可以追溯用户操作路径、提供用户操作分析和解决用户投诉的系统。它可以帮我们回溯用户从进来页面后,他的每一步操作,点击了页面哪个位置,做了何种行为,比如抽奖、付款、充值等。

为什么要有用户行为系统

痛点

我这里经历过一些用户投诉过来的问题,有时候一些问题非常难去定位,往往需要花大量时间去重现用户出现的问题,我们部门定的标准是用户投诉问题,必须在6个小时内解决。

我们的日志只有埋点上报、接口调用记录和 nginx 访问日志,信息过少,如果这些日志都没问题了,那想解决用户投诉问题就只能靠不断重现了,幸运的话6个小时内就可以重现并解决,如果是很隐蔽的bug,估计6个小时都没法重现,如果是资金相关投诉,就很严重了。

更有可能是有些用户是假投诉,没有遇到这个问题也投诉了要赔付,我们又不具备分辨他是真假的能力,因为是不可能重现的,如果没法验证没有证据,我们只能认栽赔付。

用户行为系统要怎么设计

旁路逻辑,非侵入式

用户行为收集应该是一个旁路的工具,它和具体的业务场景没有关系,调用方只需要将它引入到页面来,不需要管这个工具的内部实现,具体调用参数写好就可以了,就算这个工具报错,也不会对业务方产生任何阻塞。

我们使用的框架是vue,最简单的调用就是:

1
2
3
<div v-behavior="userBehaviorKey">
...
</div>

如果用的不是vue,那么也可以直接引入调用:

1
2
3
4
var behaveReport = require('my.behave.module');
var MY_UNIQ_KEY = 'MY_UNIQ_KEY';

behaveReport.init(MY_UNIQ_KEY);

行为收集

收集用户行为的场景

  • 用户进来页面收集一次
  • 如果是桌面页面,监听用户的点击事件,每次点击都去收集
  • 如果是移动端页面,监听用户的触屏事件,每次触屏都去收集

其他场景看自己需要,可以自由添加,所以我会提供一个选项自行添加:

1
2
3
4
5
6
var BEHAVIORS = ['load', 'touchstart'];
BEHAVIORS = options.BEHAVIORS || BEHAVIORS;

BEHAVIORS.forEach(function (behave) {
window.addEventListener(behave, handleAddEventListner);
});

节流

如果用户在 500ms 内连续点了好几次,其实只是点了一个地方,我们就要考虑节流了,这种情况就只收集一次。

1
2
3
if ((time - LAST_TIME) > MIN_INTERVAL_TIMER) {
pushBehaveLog(eventLog);
}

未上报数据保存

数据采集下来后不是立即上报,先压入一个数据,并在个地方先保存下来,可以保存在内存里面,也可以选择保存到 sessionStorage,之所以选择 sessionStorage,是因为它的数据只在当前打开窗口有效,这样可以避免污染到下次打开页面的数据。

1
2
var logs = getBehaveLog();
logs.stack.push(log); // 压入新收集到的数据

收集什么数据

需要收集什么数据看自己需要,这里我提供几个点,全部都是点击当时的数据:

  • 标签名(h1/p/span/div)
  • 类名(.btn-default)
  • 元素id
  • 点击的文字
  • 坐标轴,包括相对整个页面的x轴和y轴
  • 事件类型(click/touchstart)
  • 点击时间,这里记录的是用户侧时间,上报后我们会在服务端加上个服务器时间字段
  • 当前页面链接
1
2
3
4
5
6
7
8
9
10
11
var eventLog = {
tagName: ev.target.tagName,
className: ev.target.className,
id: ev.target.id,
text: ev.target.innerText,
x: Math.round(ev.clientX || ev.pageX || (ev.targetTouches && ev.targetTouches[0].clientX)),
y: Math.round(ev.clientY || ev.pageY || (ev.targetTouches && ev.targetTouches[0].clientY)),
type: ev.type,
createTime: DateFormat(new Date(), 'yyyy-mm-dd hh:ii:ss'),
location: location.pathname + location.search + location.hash
};

数据区分

数据收集上去了,还要区分是哪个用户的行为或者是哪个业务上报的,所以每次上报需要带上用户的 uid 和各个业务自定义的 key。

另外,如果用户在这个页面跳出去又跳回来了,两次的行为收集就需要做一个区分,所以需要每次进入页面提供一个 uuid,在这次页面的行为都会带上这个 uuid,跳出去后再次进来,uuid 就需要不一样了。

1
2
3
4
5
6
7
8
9
10
/**
* 初始化行为事件储存队列
*/
function initBehaveLog() {
var uuid = getUuid();
var logs = {
id: uuid,
stack: []
};
}

上报时机

  • 由于我们的页面大部分采用 hash 路由,所以 hash 改变的时候会上报一次
  • 当行为收集达到指定个数,那么把已收集的先上报,并清空本地已收集数据
  • 每隔指定时间就上报一次,并清空本地已手机数据
  • 在页面被关闭之前,同样上报一次

如果上报方法发现上报队列没数据,就不上报。

hash 改变和页面关闭前上报

1
2
3
4
5
var REPORT_BEHAVIORS = ['hashchange', 'beforeunload'];

REPORT_BEHAVIORS.forEach(function (behave) {
window.addEventListener(behave, reportELK);
});

达到指定个数上报

1
2
3
if (logs.stack.length >= MAX_STORED_LENGTH) {
reportELK();
}

每隔指定时间上报一次

1
2
// 每隔指定时间就上报一次
reportTimer = setInterval(reportELK, REPORT_INTERVAL_TIME);

如何上报

前端上报一般是异步请求,但其实我们不用关心上报结果是否成功,用图片的形式发请求岂不是更好。

还是有个棘手的问题,在用户关闭页面前怎么上报,如果是异步上报肯定上报不上去的,这里使用同步请求好一点。

1
2
3
4
5
6
7
8
9
10
if (ev && ev.type && ev.type.indexOf && ev.type.indexOf('unload') > -1 && XMLHttpRequest) {
var reportMsg = encodeURIComponent(JSON.stringify({
msg: logs
}));
var url = '/myReportPath?key=' + REPORT_KEY + '&msg=' + reportMsg;

var xhr = new XMLHttpRequest();
xhr.open('POST', url, false); // false 表明使用同步请求
xhr.send();
}

数据落地和查询

数据落地

我们使用了 ELK 来储存数据,ELK 优点包括检索性能高效和集群线性扩展,适合用来储存大量的数据。

数据查询

数据查询可以通过 ELK 提供的查询接口来拉取数据,拉取到数据之后还需要对数据做进一步的解析才能转为可读信息,这里可以提供一个查询系统专门来查询收集到的行为数据,并支持下面维度的查询:

  • 按业务查询
  • 按用户查询
  • 开始时间
  • 结束时间

未来规划

既然我们能拿到用户的操作行为路径了,那能不能直接就把用户的行为回放出来,我们直接在用户操作的页面一步步回放用户每一步操作了什么,把整个路径播放出来,是不是更简单更容易分析,可以朝这个方向发展。

真实使用案例

曾经有用户通过某些业务上的巧合,虽然他并不具备赔付的条件,但是这种巧合可以让他具备投诉的条件,如果是之前,我们很难去区分他是不是假投诉,也没有证据来去验证,该业务接上这个用户行为系统后,我们快速查清这个用户其实是通过某种巧合来凑成我们给他赔付的条件,实际上他的实际操作并没有问题,我们的业务也没有bug,仅仅是巧合。