npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

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算法

② 链路信息的维护

在路径上报过程中,使用 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 字段,用于存储操作的自定义属性。

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

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

展示结果:

埋点演示:

20241202_161158.mp4