1.0.20 • Published 7 months ago

path-reporting-sdk v1.0.20

Weekly downloads
-
License
ISC
Repository
-
Last release
7 months ago

:::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类的功能和使用方法。每个方法的参数、返回值(如果有)、示例代码都详细列出,便于快速上手和使用。

npm.io

npm.io

一些思考:

北斗埋点不好维护用户操作数据,取数复杂,数据还有时效 (一般是T+1);神策埋点有实现的可能,但是做不到需求要的颗粒度,也需要进行二次开发 (神策埋点也不清楚公司是否购买)。最终决定自定义一套上报逻辑来实现,需要解决的问题:

操作链路节点唯一标识怎么生成?

操作链路信息怎么维护,父子级操作怎么描述?

用户操作是随机的,不能预期先后顺序,如何维护当前操作路径?

不同路径埋点信息很不统一,如何设计数据表?

如何保证上报的稳定性,减少丢失率?

调研的技术方案:

  • Snowflake算法:Twitter开源的一种分布式ID生成算法,能够生成全局唯一的64位ID。
  • UUID:通用唯一识别码,虽然也能生成唯一的ID,但在分布式系统中的性能和唯一性不如Snowflake。
  • 自定义ID生成器:自行设计的ID生成机制,可能需要更多的开发和维护成本。

选择的技术方案:Snowflake算法

② 链路信息的维护

在路径上报过程中,使用 traceIdpreviousSpanIdspanId 来标识操作的唯一性及其关系,以便更好地追踪和分析用户行为路径。

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 字段,用于存储操作的自定义属性。

  • 灵活性: 可以根据不同的操作类型动态添加自定义属性,无需频繁修改数据库表结构。
  • 扩展性: 支持未来新增更多自定义属性,而不会影响现有系统的稳定性。

npm.io

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保证数据发送,异步不阻塞页面卸载只适合发送小量数据

npm.io

页面渲染配置:

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;
}

展示结果:

npm.io

埋点演示:

20241202_161158.mp4

1.0.20

7 months ago

1.0.19

7 months ago

1.0.18

7 months ago