path-reporting-sdk
v1.0.20
Published
:::tips 版本号:1.0.18
Downloads
187
Readme
:::tips 版本号:1.0.18
发布日期:2024/12/01
:::
path-reporting-sdk: https://www.npmjs.com/package/path-reporting-sdk
import { behavior } from 'path-reporting-sdk';
behavior.init('https://your-reporting-url.com', worker_id, data_center_id);
- `url`:路径上报地址(必填)。
- `worker_id`:工作流ID(可选,默认为1,生成唯一标识的配置项)。
- `data_center_id`:数据中心ID(可选,默认为1,生成唯一标识的配置项)。
(2) 设置互斥操作类型列表: 使用setExclusiveOperations
方法来设置互斥操作类型列表。
behavior.setExclusiveOperations({
HOME_PAGE: ['HOME_SEARCH', 'VIEW_ACTIVITY'],
PROUDCT_PAGE: ['PROUDCT_VIEW', 'PROUDCT_VIEW_COMMISSION', 'PROCUCT_SHARE']
});
(3) 登录绑定用户信息: 使用login
方法来绑定用户信息。
behavior.login('hz23068370', '熊小刚');
(4) 注册全局属性: 使用registerPage
方法来注册全局属性。
behavior.registerPage({ systemId: 1 });
try { // 初始化上报配置 behavior?.init('http://b.dome.com/api/save'); // 设置互斥(同级)操作~ behavior?.setExclusiveOperations({ HOME_PAGE: ['HOME_SEARCH', 'VIEW_ACTIVITY'], PROUDCT_PAGE: ['PROUDCT_VIEW', 'PROUDCT_VIEW_COMMISSION', 'PROCUCT_SHARE'] }); // 设置全局属性 behavior?.registerPage({ systemId: 1 }); // 模拟登录应用 behavior?.login('hz23068370', '熊小刚'); behavior?.save({ operationType: 'LOGIN' })
} catch (error) { console.log(error); }
```typescript
<Button color='primary' size='small'
onClick={() => {
try {
behavior.clearSameOperationType(behavior?.exclusiveOperations?.HOME_PAGE);
behavior.save({
operationType: 'HOME_SEARCH',
properties: JSON.stringify({
searchKeyword: '史带、大地'
})
})
} catch (error) {
console.log(error)
}
}}>
首页搜索
</Button>
<Button color='primary' size='small'
onClick={() => {
try {
behavior.clearSameOperationType(behavior?.exclusiveOperations?.HOME_PAGE);
behavior.save({
operationType: 'VIEW_ACTIVITY',
properties: JSON.stringify({
activityName: '2022年9月新会员注册送1000元'
})
})
} catch (error) {
console.log(error)
}
}}>
查看活动
</Button>
<Button color='primary' size='small'
onClick={() => {
try {
behavior.clearSameOperationType(behavior?.exclusiveOperations?.PROUDCT_PAGE);
behavior.save({
operationType: 'PROUDCT_VIEW',
properties: JSON.stringify({
productName: '世纪泰康个人住院医疗保险',
productId: 18,
planId: 45
})
})
} catch (error) {
console.log(error)
}
}}>
查看产品
</Button>
<Button color='primary' size='small'
onClick={() => {
try {
behavior.clearSameOperationType(behavior?.exclusiveOperations?.PROUDCT_PAGE);
behavior.save({
operationType: 'PROUDCT_VIEW_COMMISSION',
properties: JSON.stringify({
productName: '小淘气5号少儿重大疾病保险',
commission: '35%'
})
})
} catch (error) {
console.log(error)
}
}}>
查看佣金
</Button>
<Button color='primary' size='small'
onClick={() => {
try {
behavior.clearSameOperationType(behavior?.exclusiveOperations?.PROUDCT_PAGE);
behavior.save({
operationType: 'PROCUCT_SHARE',
properties: JSON.stringify({
productName: '孝心安5号老年人意外险 ',
shareType: '微信',
account: 'wx_123456'
})
})
} catch (error) {
console.log(error)
}
}}>
分享
</Button>
Behavior
类用于管理用户行为路径的记录和上报。它提供了初始化配置、设置互斥操作类型、绑定用户信息、注册全局属性、上报操作路径等功能。
构造函数
new Behavior();
描述: 创建一个新的Behavior
实例。
参数: 无
描述: 初始化配置,设置路径上报地址和其他可选配置。
参数:
url
(string): 路径上报地址(必填)。worker_id
(number, optional): 工作流ID(可选,默认为1)。data_center_id
(number, optional): 数据中心ID(可选,默认为1)。
示例:
behavior.init('https://your-reporting-url.com', 1, 1);
描述: 设置互斥操作类型列表。
参数:
exclusiveOperations
(object): 互斥操作类型列表,键为操作类型字符串,值为互斥操作类型的字符串数组。
示例:
behavior.setExclusiveOperations({
HOME_PAGE: ['HOME_SEARCH', 'VIEW_ACTIVITY'],
PROUDCT_PAGE: ['PROUDCT_VIEW', 'PROUDCT_VIEW_COMMISSION', 'PROCUCT_SHARE']
});
描述: 登录绑定用户信息,将用户信息存储在全局属性中。
参数:
id
(string): 用户ID/工号(必填)。name
(string, optional): 用户名称(可选)。
示例:
behavior.login('hz23068370', '熊小刚');
描述: 注册全局属性,将属性对象合并到全局属性中。
参数:
attribute
(object): 属性对象。
示例:
behavior.registerPage({ systemId: 1 });
描述: 上报操作路径,将操作数据发送到指定的路径上报地址,并将操作数据入栈。
参数:
data
(object): 上报的JSON数据对象。
示例:
behavior.save({
operationType: 'PROCUCT_SHARE',
properties: JSON.stringify({
productName: '孝心安5号老年人意外险 ',
shareType: '微信',
account: 'wx_123456'
})
})
描述: 清空栈内同一操作类型(包括同级操作)以及后续操作路径。
参数:
types
(array): 互斥操作类型数组。
示例:
behavior.clearSameOperationType(['HOME_SEARCH', 'VIEW_ACTIVITY']);
描述: 清空整个操作栈。
参数: 无
通过以上文档,开发者可以清楚地了解Behavior
类的功能和使用方法。每个方法的参数、返回值(如果有)、示例代码都详细列出,便于快速上手和使用。
一些思考:
北斗埋点不好维护用户操作数据,取数复杂,数据还有时效 (一般是T+1);神策埋点有实现的可能,但是做不到需求要的颗粒度,也需要进行二次开发 (神策埋点也不清楚公司是否购买)。最终决定自定义一套上报逻辑来实现,需要解决的问题:
操作链路节点唯一标识怎么生成?
操作链路信息怎么维护,父子级操作怎么描述?
用户操作是随机的,不能预期先后顺序,如何维护当前操作路径?
不同路径埋点信息很不统一,如何设计数据表?
如何保证上报的稳定性,减少丢失率?
调研的技术方案:
- Snowflake算法:Twitter开源的一种分布式ID生成算法,能够生成全局唯一的64位ID。
- UUID:通用唯一识别码,虽然也能生成唯一的ID,但在分布式系统中的性能和唯一性不如Snowflake。
- 自定义ID生成器:自行设计的ID生成机制,可能需要更多的开发和维护成本。
选择的技术方案:Snowflake算法
② 链路信息的维护
在路径上报过程中,使用 traceId
、previousSpanId
和 spanId
来标识操作的唯一性及其关系,以便更好地追踪和分析用户行为路径。
traceId:
- **定义**: 标识整个操作链路的唯一ID。
- **用途**: 用于关联同一操作链路中的所有操作,便于追踪整个用户行为路径。
- **生成时机**: 当操作链路开始时生成,即第一个操作的 `traceId` 由 `#getNextId()` 生成。
previousSpanId:
- **定义**: 标识前一个操作的唯一ID。
- **用途**: 用于表示当前操作的前一个操作,帮助构建操作链路的父子关系。
- **生成时机**: 对于第一个操作,`previousSpanId` 为空字符串 `''`;对于后续操作,`previousSpanId` 为前一个操作的 `spanId`。
spanId:
- **定义**: 标识当前操作的唯一ID。
- **用途**: 用于唯一标识当前操作,便于单独追踪和分析。
- **生成时机**: 每个操作生成一个新的 `spanId`,由 `#getNextId()` 生成。
③ 操作路径栈(这里用数组模拟)
stack 数组
- **定义**: 用于存储用户操作路径的数组。
- **用途**: 记录用户执行的每一个操作,以便追踪操作序列和构建操作链路。
- **数据结构**: 每个元素是一个对象,包含操作的相关信息,如 一级操作的操作id (traceId), 操作父级的id (previousSpanId),当前操作的id (spanId),操作类型 (operationType) 等。
数组的特性:
- **顺序性**: 数组是有序的,能够自然地表示操作的先后顺序。
- **易于访问**: 通过索引可以直接访问和修改数组中的元素,操作效率高。
- **动态性**: 数组可以动态地添加和删除元素,适应操作路径的变化,<u>如操作了二级动作,进入子页面操作了三级动作,返回上一些继续操作其他二级动作。这时就需要清空栈中历史的二三级动作,让新的二级操作入栈,从而维护最新操作链路。</u>
操作栈的应用:
- **追踪操作序列**: 通过数组记录用户的操作序列,便于追踪和分析用户的操作路径。
- **构建操作链路**: 通过 `traceId`、`previousSpanId` 和 `spanId` 构建操作链路,便于理解用户的操作流程。
- **管理操作状态**: 通过数组管理操作状态,便于清除特定操作类型及其后续操作路径。
实现细节:
- **入栈操作**: 使用 `#push` 方法将新的操作元素添加到数组末尾。
- **出栈操作**: 通过索引访问和修改数组中的元素,实现对特定操作的处理。
- **清空操作**: 使用 `clear` 方法清空整个操作栈,或使用 `clearSameOperationType` 方法清空特定操作类型及其后续操作路径。
/**
* 入栈
* @param {object} element 入栈元素
*/
#push(element) {
this.stack[this.top++] = element;
}
/**
* 预览前一个元素操作路径(当前操作还没入栈,所以取栈顶元素)
*/
#previous() {
return this.stack[this.top - 1] || {};
}
/**
* 获取栈顶元素
*/
#root() {
return this.stack[0] || {};
}
/**
* 检测栈内路径长度
*/
#length() {
return this.top;
}
/**
* 清空栈内同一操作类型(包括同级操作)以及后续操作路径(这里使用数组方法)
* @param {object} types 互斥操作类型数组
*/
clearSameOperationType(types) {
let index = 1e5;
for (let i = 0; i < types?.length && this.top > 0; i++) {
const _index = this.stack?.findIndex((_stack) => _stack.operationType === types[i]);
if (_index !== -1) index = Math.min(index, _index);
}
if (index !== 1e5) {
this.stack = this.stack.slice(0, index);
this.top = index;
}
}
/**
* 清空整个操作栈
*/
clear() {
this.stack = [];
this.top = 0;
}
④ 动态属性 properties
在应用的操作日志记录中,不同操作类型可能需要记录不同的自定义属性。为了灵活地支持这些需求,我们在数据库表中引入了 properties
json 字段,用于存储操作的自定义属性。
- 灵活性: 可以根据不同的操作类型动态添加自定义属性,无需频繁修改数据库表结构。
- 扩展性: 支持未来新增更多自定义属性,而不会影响现有系统的稳定性。
CREATE TABLE IF NOT EXISTS `t_behavior_travel` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
`trace_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '整个操作链路的唯一标识符',
`span_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '当前事件在链路中的唯一标识符',
`previous_span_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '前一个事件的唯一标识符',
`system_id` tinyint(1) DEFAULT NULL COMMENT '登录系统: 1: 测试应用, 2: XXX',
`operation_type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '操作类型',
`properties` json DEFAULT NULL COMMENT '其他非统一字段数据存入json',
`create_user_no` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '操作人',
`create_user_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '操作人姓名',
`page_name` varchar(100) DEFAULT NULL,
`trigger_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '触发时间',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否删除, 0:否, 1: 是',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
⑤ navigator.sendBeacon 请求
在用户操作路径上报中,选择 navigator.sendBeacon
方法来发送数据,以确保数据的可靠性和性能。
可靠性
- 保证数据发送: `navigator.sendBeacon` 方法确保数据在页面卸载(如关闭浏览器窗口、导航到新页面)之前发送出去,即使用户快速离开页面,数据也能成功发送。
- 异步发送: `sendBeacon` 是异步的,不会阻塞页面的卸载过程,从而避免影响用户体验。
性能
- 低开销: `sendBeacon` 方法的实现优化了数据传输的开销,适合发送小量数据。
- 不影响页面加载: 由于是异步发送,不会影响页面的加载时间和性能。
兼容性
- 广泛支持: `navigator.sendBeacon` 在现代浏览器中得到了广泛支持,包括 Chrome、Firefox、Safari 和 Edge 等主流浏览器。
应用场景
- 页面卸载事件: 在 `beforeunload` 或 `unload` 事件中使用 `sendBeacon`,确保在用户离开页面时数据能够成功发送。
- 实时数据上报: 适用于需要实时上报用户操作路径的场景,如点击事件、页面浏览时间等。
与其他方案的对比
| 方案 | 优点 | 缺点 |
| --- | --- | --- |
| XMLHttpRequest
| 支持复杂请求,可以发送大量数据 | 同步请求会阻塞页面卸载,可能导致数据丢失 |
| fetch
| 异步请求,支持 Promise | 在页面卸载时可能无法保证数据发送成功 |
| navigator.sendBeacon
| 保证数据发送,异步不阻塞页面卸载 | 只适合发送小量数据 |
页面渲染配置:
const moment = require('moment-timezone');
const CONFIG = {
// 登录, 无配置
LOGIN: [],
// 浏览首页
HOME: [],
// 首页-检索
HOME_SEARCH: [
{
label: '搜索关键字',
key: 'properties',
type: 'json',
isTitle: true,
jsonTemplate: '{searchKeyword}'
},
{
label: '检索时间',
key: 'trigger_time',
type: 'dateTime'
}
],
// 首页-查看活动
VIEW_ACTIVITY: [
{
label: '活动名称',
key: 'properties',
type: 'json',
isTitle: true,
jsonTemplate: '{activityName}'
},
{
label: '操作时间',
key: 'trigger_time',
type: 'dateTime'
}
],
// 浏览产品页
PRODUCT: [],
// 查看产品
PROUDCT_VIEW: [
{
label: '产品名称',
key: 'properties',
type: 'json',
jsonTemplate: '{productName}'
},
{
label: '产品ID',
key: 'properties',
type: 'json',
jsonTemplate: '{productId}'
},
{
label: '计划ID',
key: 'properties',
type: 'json',
jsonTemplate: '{planId}'
},
{
label: '操作时间',
key: 'trigger_time',
type: 'dateTime'
}
],
// 查看产品佣金
PROUDCT_VIEW_COMMISSION: [
{
label: '产品名称',
key: 'properties',
type: 'json',
jsonTemplate: '{productName}'
},
{
label: '产品分佣比例方式',
key: 'properties',
type: 'json',
jsonTemplate: '{commission}'
},
{
label: '操作时间',
key: 'trigger_time',
type: 'dateTime'
}
],
// 产品分享
PROCUCT_SHARE: [
{
label: '产品名称',
key: 'properties',
type: 'json',
jsonTemplate: '{productName}'
},
{
label: '分享对象',
key: 'properties',
type: 'json',
jsonTemplate: '{shareType}({account})'
},
{
label: '操作时间',
key: 'trigger_time',
type: 'dateTime'
}
]
}
module.exports = (data) => {
const configItem = CONFIG[data?.operation_type];
if (!configItem || configItem.length === 0) return '-';
let result = '', _text = '';
for (let i = 0; i < configItem.length; i++) {
const item = configItem[i];
if (item.type === 'dateTime') {
_text = `${item?.label}: ${data[item?.key] ? moment(data[item?.key])?.tz('Asia/Shanghai')?.format('YYYY-MM-DD HH:mm:ss') : '-'}`;
} else if (item.type === 'json') {
if (!data?.properties) continue;
_text = '';
const _json = data?.properties;
let _jsonTemplateResult = item?.jsonTemplate; // 不能直接操作item?.jsonTemplate,匹配结果会覆盖掉配置字符串
const _keys = _jsonTemplateResult?.match(/\{([^\}]*)\}/g);
for (let i = 0; i < _keys?.length; i++) {
_jsonTemplateResult = _jsonTemplateResult?.replace(/\{([^\}]*)\}/g, (_, target) => `${_json[target]}`);
}
_text = `${item?.label}: ${_jsonTemplateResult}`;
} else {
_text = `${item?.label}: ${data[item?.key] || ((data[item?.key] === 0 || data[item?.key] === false) ? data[item?.key] : '-')}`;
}
result +=`<p ${item?.isTitle ? 'class=title' : ''}>${_text}</p>`;
}
return result;
}
展示结果:
埋点演示: