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 🙏

© 2026 – Pkg Stats / Ryan Hefner

@hulihan97/message-reporting

v0.1.1

Published

Lightweight browser SDK for manual JSON reporting.

Readme

📡 message-reporting

一个面向浏览器环境的轻量级手动上报 SDK,支持 HTML 直引和 Vue/TypeScript 模块化接入,并根据页面阶段、能力检测与数据体积在 sendBeaconfetchgif 之间自动选择通道。

🎯 项目定位

用于在浏览器端手动上报业务数据(如埋点、日志等),支持多种发送方式并自动降级,确保在不同浏览器环境和页面状态下都能可靠地上报数据。

🛠️ 技术栈

  • 语言: TypeScript(严格模式)
  • 构建工具: tsup
  • 目标环境: ES2019+ 浏览器
  • 产物格式: ESM、CJS、IIFE(全局变量 MessageReportingSDK

✨ 功能说明

  • 提供 init 初始化方法,统一配置上报地址与发送选项
  • 提供 reportreportImmediate 两个手动上报入口
  • 支持 sendBeaconfetchgif 三种发送方式与可预测降级链路
  • 输出 esmcjsiife 三种构建格式
  • 暴露完整 TypeScript 类型定义
  • 自动采集运行时环境信息(浏览器、操作系统、网络状态等)
  • 支持跨页面 traceId 追踪,便于后端串联用户会话链路
  • 自动识别微信/支付宝 H5 容器

📦 安装依赖

npm install

🔨 构建

npm run build

构建完成后会生成以下产物:

  • dist/index.js - ESM 格式
  • dist/index.cjs - CJS 格式
  • dist/index.global.js - IIFE 格式(全局变量 MessageReportingSDK
  • dist/index.d.ts - TypeScript 类型定义

🚀 模块化接入(Vue/TypeScript)

基础用法

import { init, report, reportImmediate } from 'message-reporting';

// 初始化 SDK
init({
  endpoint: 'https://api.example.com/report',
  gifEndpoint: 'https://api.example.com/report.gif',
});

// 常规事件上报
await report({
  event: 'page_view',
  page: '/home',
});

// 即时上报(适用于页面退出场景)
await reportImmediate({
  event: 'page_exit',
  page: '/home',
});

页面退出场景

// 监听页面可见性变化,在页面隐藏时立即上报
window.addEventListener('visibilitychange', () => {
  if (document.visibilityState === 'hidden') {
    reportImmediate({
      event: 'page_leave',
      duration: Date.now() - pageEnterTime,
    });
  }
});

启用签名验证的配置示例

import { init, report, reportImmediate } from 'message-reporting';

// 初始化 SDK 并启用签名验证
init({
  // 主上报地址(必填)
  endpoint: 'https://api.example.com/report',
  // gif 兜底上报地址(可选)
  gifEndpoint: 'https://api.example.com/report.gif',
  // 启用签名验证(默认 false)
  enableSignature: true,
  // 签名密钥(enableSignature 为 true 时必填)
  signingKey: 'your-secret-key-here',
  // 签名模式下 GIF 上报阈值(默认 1000 字节)
  encryptedGifMaxPayloadBytes: 1000,
});

// 常规上报,SDK 会自动注入 _ts、_nonce、_sign 安全字段
await report({
  event: 'page_view',
  page: '/home',
});

完整配置示例

init({
  // 主上报地址(必填)
  endpoint: 'https://api.example.com/report',
  // gif 兜底上报地址(可选,默认复用 endpoint)
  gifEndpoint: 'https://api.example.com/report.gif',
  // 自定义请求头(可选)
  headers: {
    'X-Custom-Header': 'value',
  },
  // fetch 请求凭据模式(可选,默认 'same-origin')
  fetchCredentials: 'include',
  // beacon 最大 payload 体积(可选,默认 60KB)
  beaconMaxPayloadBytes: 60 * 1024,
  // gif 最大 payload 体积(可选,默认 1.5KB)
  gifMaxPayloadBytes: 1500,
  // 小包是否优先使用 gif(可选,默认 true)
  preferGifForSmallPayload: true,
  // 是否携带运行时环境信息(可选,默认 false)
  includeRuntimeInfo: false,
  // 是否启用签名验证(可选,默认 false)
  enableSignature: false,
  // 签名密钥(可选,enableSignature 为 true 时必填)
  signingKey: '',
  // 签名模式下 GIF 上限(可选,enableSignature 为 true 时默认 1000)
  encryptedGifMaxPayloadBytes: 1000,
});

🌐 原生 HTML 使用方式

方式一:本地文件引入

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>SDK 示例</title>
</head>
<body>
  <button id="btn-report">点击上报</button>

  <!-- 引入 SDK -->
  <script src="./dist/index.global.js"></script>
  <script>
    // 初始化 SDK
    MessageReportingSDK.init({
      endpoint: 'https://api.example.com/report',
      gifEndpoint: 'https://api.example.com/report.gif',
    });

    // 按钮点击上报
    document.getElementById('btn-report').addEventListener('click', function() {
      MessageReportingSDK.report({
        event: 'button_click',
        button: 'submit',
        timestamp: Date.now(),
      }).then(function(result) {
        console.log('上报结果:', result);
      });
    });
  </script>
</body>
</html>

方式二:CDN 引入

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>SDK 示例</title>
</head>
<body>
  <button id="btn-report">点击上报</button>

  <!-- 通过 CDN 引入(使用 unpkg 或 jsdelivr) -->
  <script src="https://unpkg.com/@hulihan97/message-reporting/dist/index.global.js"></script>
  <!-- 或 -->
  <!-- <script src="https://cdn.jsdelivr.net/npm/@hulihan97/message-reporting/dist/index.global.js"></script> -->
  
  <script>
    // 初始化 SDK
    MessageReportingSDK.init({
      endpoint: 'https://api.example.com/report',
    });

    // 页面加载完成上报
    window.addEventListener('load', function() {
      MessageReportingSDK.report({
        event: 'page_load',
        loadTime: performance.now(),
      });
    });

    // 页面关闭前立即上报
    window.addEventListener('beforeunload', function() {
      MessageReportingSDK.reportImmediate({
        event: 'page_unload',
        stayDuration: Date.now() - pageStartTime,
      });
    });

    // 按钮点击上报
    document.getElementById('btn-report').addEventListener('click', function() {
      MessageReportingSDK.report({
        event: 'button_click',
        button: 'submit',
      });
    });
  </script>
</body>
</html>

方式三:动态加载

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>SDK 动态加载示例</title>
</head>
<body>
  <script>
    // 动态加载 SDK
    (function() {
      var script = document.createElement('script');
      script.src = './dist/index.global.js';
      script.onload = function() {
        // SDK 加载完成后初始化
        MessageReportingSDK.init({
          endpoint: 'https://api.example.com/report',
        });

        // 初始化完成后可正常使用
        MessageReportingSDK.report({
          event: 'sdk_loaded',
        });
      };
      document.head.appendChild(script);
    })();
  </script>
</body>
</html>

方式四:结合业务逻辑

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>完整业务示例</title>
</head>
<body>
  <button id="btn-action">执行操作</button>

  <script src="./dist/index.global.js"></script>
  <script>
    // 页面进入时间
    var pageEnterTime = Date.now();

    // 初始化 SDK
    MessageReportingSDK.init({
      endpoint: 'https://api.example.com/report',
      gifEndpoint: 'https://api.example.com/report.gif',
      includeRuntimeInfo: true,
    });

    // 页面加载完成上报
    window.addEventListener('load', function() {
      MessageReportingSDK.report({
        event: 'page_view',
        url: window.location.href,
        referrer: document.referrer,
        loadTime: performance.now(),
      });
    });

    // 监听页面可见性变化
    document.addEventListener('visibilitychange', function() {
      if (document.visibilityState === 'hidden') {
        MessageReportingSDK.reportImmediate({
          event: 'page_hidden',
          stayDuration: Date.now() - pageEnterTime,
        });
      }
    });

    // 按钮点击上报
    document.getElementById('btn-action').addEventListener('click', function() {
      MessageReportingSDK.report({
        event: 'action_click',
        action: 'submit',
      });
    });

    // 错误捕获上报
    window.addEventListener('error', function(e) {
      MessageReportingSDK.report({
        event: 'js_error',
        message: e.message,
        filename: e.filename,
        lineno: e.lineno,
      });
    });
  </script>
</body>
</html>

📖 API 说明

init(options)

初始化 SDK 配置。未传 endpoint 或传入空字符串会立即抛错。

参数说明:

| 参数 | 类型 | 必填 | 默认值 | 说明 | |------|------|------|--------|------| | endpoint | string | 是 | - | 主上报地址 | | gifEndpoint | string | 否 | endpoint | gif 兜底上报地址 | | headers | Record<string, string> | 否 | { 'content-type': 'application/json' } | 请求头配置 | | fetchCredentials | RequestCredentials | 否 | 'same-origin' | fetch 凭据模式 | | beaconMaxPayloadBytes | number | 否 | 61440 (60KB) | beacon 最大体积 | | gifMaxPayloadBytes | number | 否 | 1500 (1.5KB) | gif 最大体积 | | preferGifForSmallPayload | boolean | 否 | true | 小包优先使用 gif | | includeRuntimeInfo | boolean | 否 | false | 是否携带运行时环境信息 | | enableSignature | boolean | 否 | false | 是否启用签名验证 | | signingKey | string | 否(启用签名时必填) | - | 签名密钥(Base64 或明文) | | encryptedGifMaxPayloadBytes | number | 否 | 1000(签名模式)/ 1500(非签名模式) | 签名模式下 GIF 阈值 |

report(payload, options?)

常规上报入口。会根据 payload 大小和环境能力选择 giffetch,页面隐藏场景也会自动优先考虑 sendBeacon

参数说明:

  • payload: 上报数据对象,必须为普通 JSON 对象
  • options.stage: 可选,覆盖页面阶段('normal' | 'hidden' | 'unload'
  • options.transport: 可选,强制指定发送通道('beacon' | 'fetch' | 'gif'

reportImmediate(payload, options?)

即时上报入口。适用于页面隐藏、退出或希望优先使用 sendBeacon 的场景。

参数说明:report

🔄 发送策略

通道选择逻辑

SDK 根据以下因素自动选择发送通道:

  1. 页面阶段 - 页面隐藏/退出时优先 sendBeacon
  2. 数据体积 - 小体积数据(≤1.5KB)优先 gif
  3. 浏览器能力 - 检测是否支持 sendBeaconfetchImage

降级链路

| 首选通道 | 降级顺序 | |----------|----------| | beacon | beaconfetchgif | | fetch | fetchbeacongif | | gif | giffetchbeacon |

体积限制

  • beacon: 最大 60KB(浏览器限制)
  • gif: 最大 1.5KB(受 URL 长度限制)
  • fetch: 无明确限制

🧠 智能特性

🔗 traceId 追踪

  • 自动生成并持久化到 localStorage
  • 跨页面复用同一追踪标识
  • 每条上报数据自动携带 traceId 字段
  • 支持 crypto.randomUUID() 或降级为时间戳+随机串

💻 运行时信息采集

开启 includeRuntimeInfo 后自动收集:

{
  "ua": "Mozilla/5.0...",
  "browser": "chrome",
  "os": "windows",
  "hostApp": "browser",
  "isWechat": false,
  "isAlipay": false,
  "language": "zh-CN",
  "platform": "Win32",
  "onLine": true,
  "network": {
    "effectiveType": "4g",
    "downlink": 10,
    "rtt": 50,
    "saveData": false
  }
}

⚠️ 注意:开启后因数据包体积限制,gif 上报可能会失效。

安全配置

签名验证机制

SDK 支持在上报数据中自动注入签名验证字段,确保数据完整性和防重放攻击。启用后,SDK 会自动在 payload 中注入以下字段:

  • _ts - 当前时间戳(毫秒),用于服务端校验请求时效性
  • _nonce - 防重放随机字符串,确保每次请求唯一
  • _sign - 签名摘要值(基于 timestamp + nonce + traceId + signingKey 的 MD5 摘要)

配置参数说明

| 参数 | 类型 | 必填 | 默认值 | 说明 | |------|------|------|--------|------| | enableSignature | boolean | 否 | false | 是否启用签名验证。设为 true 时必须同时提供 signingKey | | signingKey | string | 启用签名时必填 | - | 签名密钥(明文或 Base64),需与服务端保持一致 | | encryptedGifMaxPayloadBytes | number | 否 | 1000(签名模式) | 签名模式下 GIF 上报的最大 payload 阈值(字节)。因签名字段会增加体积,建议低于非签名模式 |

GIF 阈值调整说明

启用签名验证后,每个上报数据会额外携带 _ts_nonce_sign 三个安全字段,导致 payload 体积增加。因此:

  • 签名模式下encryptedGifMaxPayloadBytes 默认值为 1000(低于非签名模式的 1500),以预留空间容纳签名字段
  • 非签名模式下,GIF 阈值使用 gifMaxPayloadBytes(默认 1500
  • 若业务数据量较大,可适当调大 encryptedGifMaxPayloadBytes,但需注意 GIF 通道的 URL 长度限制

⚠️ 注意:启用签名验证后,若 payload 体积超过 GIF 阈值,SDK 会自动降级为 fetch 通道发送

📱 容器识别

自动识别以下宿主环境:

  • wechat - 微信 H5 容器
  • alipay - 支付宝 H5 容器
  • browser - 普通浏览器

📝 类型导出

SDK 对外导出以下 TypeScript 类型:

  • InitOptions - 初始化配置选项
  • ResolvedInitOptions - 标准化后的内部配置
  • ReportOptions - 上报覆盖选项
  • ReportPayload - 上报数据结构
  • ReportResult - 上报执行结果
  • TransportMode - 发送通道类型
  • PageStage - 页面阶段类型
  • ReportContext - 上报上下文信息
  • RuntimeLike - 运行时依赖接口
  • NetworkInfoLike - 网络信息接口

📊 上报结果结构

每次上报返回 ReportResult 对象:

{
  ok: boolean,              // 是否成功
  transport: TransportMode, // 实际使用的通道
  payloadSize: number,      // 数据体积(字节)
  attemptedTransports: TransportMode[] // 尝试过的通道列表
}

⚠️ 注意事项

  1. 必须先调用 init - 未初始化直接调用 report 会抛错
  2. payload 必须为对象 - 不支持数组或原始值
  3. gif 通道限制 - 数据体积受 URL 长度限制,建议 ≤1.5KB
  4. localStorage 权限 - 某些浏览器可能限制 localStorage 访问,traceId 会降级为会话级
  5. 跨域问题 - 确保上报地址支持跨域请求(CORS)