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

onerway-analytics

v1.5.0

Published

Onrway Analytics SDK

Readme

onerway-analytics

客户端埋点 SDK,支持双发:自建数据服务 + Google Tag Manager(GTM)。

安装

npm

npm install onerway-analytics

CDN / 本地文件

<script src="./dist/bundle/index.js"></script>
<!-- 全局变量:window.OnrwayAnalytics -->

快速开始

import { createAnalytics } from 'onerway-analytics';

const analytics = createAnalytics({
  appKey:    'your-app-key',
  serverUrl: 'https://your-server.com/collect',
  env:       'prod',
  debug:     true,
});

上报模式

双发(自建服务 + GTM)

const analytics = createAnalytics({
  appKey:    'your-app-key',
  serverUrl: 'https://your-server.com/collect',
  env:       'prod',
  gtm: {
    containerId: 'GTM-XXXXXXX',
    enabled:     true,
  },
});

只发自建服务

不配置 gtm,或设置 gtm.enabled: false:

const analytics = createAnalytics({
  appKey:    'your-app-key',
  serverUrl: 'https://your-server.com/collect',
  env:       'prod',
});

只发 GTM

设置 disableServer: true,serverUrl 可留空,所有事件只推入 dataLayer:

const analytics = createAnalytics({
  appKey:        'your-app-key',
  serverUrl:     '',
  disableServer: true,
  gtm: {
    containerId: 'GTM-XXXXXXX',
    enabled:     true,
  },
});

disableServer: true 时,analytics.flush() 和重试机制均不生效。


API

事件埋点

// 普通事件(进入缓冲区,定时批量发送)
analytics.track('button_click', { target_id: 'buy_btn', scene: 'product' });

// 关键事件(立即发送,不等缓冲区)
analytics.track('payment_success', { order_id: '456' }, { keyEvent: true });

用户标识

// 设置用户 ID 与属性
analytics.identify('user-123', { plan: 'pro', email: '[email protected]' });

// 获取用户信息
const profile = analytics.getUserProfile();
// { userId: 'user-123', anonymousId: 'uuid...', attributes: { plan: 'pro' } }

// 重置用户身份
analytics.reset();

GTM 编程式控制

// 直接推送自定义数据到 dataLayer
analytics.gtm.push({ event: 'custom_event', key: 'value' });

// 推送带事件名的数据
analytics.gtm.track('checkout_start', { step: 1 });

// 设置用户 ID
analytics.gtm.setUserId('user-123');

// 设置用户属性
analytics.gtm.setUserProperties({ plan: 'pro' });

其他方法

analytics.flush();                        // 立即刷新缓冲区
analytics.getSessionId();                 // 获取当前 session ID
analytics.getClientId();                  // 获取匿名客户端 ID
analytics.setClientType('MINI_PROGRAM'); // 设置客户端类型
analytics.setMerchantLanguage('zh');     // 设置商户语言
analytics.destroy();                      // 销毁实例,移除事件监听(SPA 路由切换时使用)

自动埋点

默认开启,通过 data-track-* 属性声明式配置:

<!-- 自定义事件名 -->
<button data-track-name="subscribe_click">订阅</button>

<!-- 带 JSON 属性 -->
<button data-track-name="buy_now"
        data-track-properties='{"product":"shoes","price":299}'>
  立即购买
</button>

<!-- 任意 data-track-* 前缀自动成为属性键 -->
<a data-track-name="share"
   data-track-platform="wechat"
   data-track-position="top">分享</a>

<!-- 忽略该元素 -->
<div data-track-ignore>不追踪</div>

限定追踪范围:

createAnalytics({
  serverUrl:          '...',
  autoTrackSelector:  '#app', // 只追踪 #app 内的点击
});

关闭自动埋点:

createAnalytics({ serverUrl: '...', autoTrack: false });

采样控制

通过 sampleRate 控制上报比例,降低高流量场景的服务器压力。决策在每次页面加载时独立进行——命中采样的会话全量上报,未命中的会话全部丢弃(包括 GTM)。

// 只上报约 10% 的页面会话
const analytics = createAnalytics({
  appKey:     'your-app-key',
  serverUrl:  'https://your-server.com/collect',
  sampleRate: 0.1,
});
  • sampleRate: 1(默认)— 全量上报
  • sampleRate: 0 — 全部丢弃,可用于临时关闭上报
  • sampleRate: 0.1 — 约 10% 的页面会话上报,每次刷新重新决策

采样决策作用于整个会话:同一次页面加载内,所有 track() 要么全部上报,要么全部丢弃,不会出现部分事件缺失的碎片数据。


配置项

| 配置 | 类型 | 默认值 | 说明 | |---|---|---|---| | appKey | string | — | 应用标识 | | serverUrl | string | — | 数据接收端点(disableServer: true 时可留空) | | apiKey | string | — | API Key(不填则按 env 自动解析) | | env | string | — | 环境:beta | test | uat | prod | | debug | boolean | false | 开启控制台日志 | | autoTrack | boolean | true | 自动点击追踪 | | autoTrackPageView | boolean | true | 初始化时自动发送 page_view;设为 false 可手动控制 | | autoTrackSelector | string | — | 自动埋点的 CSS 选择器范围 | | sampleRate | number | 1 | 采样率 0~1;每次页面加载独立掷骰,0.1 表示约 10% 的页面会话上报,0 全不报,1 全量报 | | system | string | — | 系统标识,写入每条事件 | | domain | string | — | 业务域标识,写入每条事件 | | keyEvents | string[] | — | 关键事件名单,命中的事件立即发送 | | bufferFlushInterval | number | 5000 | 缓冲区定时刷新间隔(ms) | | bufferMaxSize | number | 25 | 缓冲区达到此数量立即刷新 | | maxRetryCount | number | 3 | 同一事件重发预算(含离页补发),上限 5;预算跨页面保留 | | persistKeyEvents | boolean | false | 将关键事件保存到本地直到 HTTP 确认;仅对属性可安全持久化的应用启用 | | sessionTimeout | number | 1800000 | 会话超时(ms),默认 30 分钟 | | performance.enabled | boolean | true | Core Web Vitals 采集开关 | | gtm.enabled | boolean | — | GTM 双发开关 | | gtm.containerId | string | — | GTM 容器 ID(GTM-XXXXXXX 格式) | | gtm.dataLayerName | string | 'dataLayer' | 自定义 dataLayer 变量名 | | disableServer | boolean | false | 关闭自建服务上报,只推 GTM |


上报数据结构

自建服务(批量 POST)

{
  "client_id":       "uuid",
  "user_id":         "user-123",
  "session_id":      "uuid",
  "user_properties": { "plan": "pro" },
  "context": {
    "sdk_version": "1.0.0",
    "app_key":     "your-app-key",
    "browser":     { "name": "Chrome", "version": "135" },
    "operating_system": { "name": "Windows", "version": "10" },
    "device_type": "desktop",
    "country":     "CN",
    "language":    "zh-CN",
    "timezone":    "Asia/Shanghai"
  },
  "marketing": {
    "utm_source":   "google",
    "utm_medium":   "cpc",
    "utm_campaign": "spring_sale",
    "landing_page": "https://example.com/?utm_source=google",
    "referrer_url": "https://google.com"
  },
  "events": [
    {
      "event_id":          "uuid",
      "event_name":        "button_click",
      "timestamp":         1700000000000,
      "key_event":         false,
      "batch_event_index": 0,
      "batch_ordering_id": "1700000000000-1",
      "batch_page_id":     "abc-xyz",
      "event_properties": {
        "system":      "web",
        "domain":      "payment",
        "scene":       "product",
        "action":      "click",
        "target_type": "button",
        "target_id":   "buy_btn",
        "page_url":    "https://example.com/product"
      }
    }
  ]
}

GTM dataLayer

每个 track() 调用推入一条:

{
  "event":      "button_click",
  "action":     "click",
  "target_id":  "buy_btn",
  "session_id": "uuid",
  "page_url":   "https://example.com/product",
  "referrer":   ""
}

identify() 推入:

{ "userId": "user-123" }
{ "plan": "pro", "email": "[email protected]" }

采集能力

| 采集项 | 说明 | |---|---| | 点击事件 | 自动埋点(data-track-*)+ 手动 analytics.track() | | 页面浏览 | 初始化时自动发送 page_view(可通过 autoTrackPageView: false 关闭) | | JS 错误 | 全局 window.error / unhandledrejection 自动捕获 | | Core Web Vitals | LCP / FCP / CLS / INP / TTFB(web_vital 事件) | | 设备上下文 | 浏览器、OS、屏幕分辨率、语言、时区、设备类型 | | 营销归因 | UTM 参数持久化、落地页、来源 URL(SPA 路由切换后仍保留) |


可靠性机制

  • 缓冲区 + 批量发送:事件先入缓冲区,满 bufferMaxSize 或到 bufferFlushInterval 后批量发送
  • 有界重试:网络错误、408、429、5xx 最多重试 maxRetryCount 次(上限 5),指数退避加随机抖动;其他 4xx 终止。fetch/XHR 均有 10 秒超时。
  • 可选关键事件持久化:persistKeyEvents: true 时,关键事件发送前保存完整 payload,HTTP 2xx 后移除;重启保留 event_id 和原页面归属,按 appKey/serverUrl 隔离。自动事件不落盘;队列最多 500 条、24 小时有效,超出重试预算或不可重试错误终止。存储不可用时降为内存发送。
  • 页面生命周期:visibilitychange(hidden) / pagehide 提前刷新,pageshow/visible 恢复正常发送;原请求与离页副本任一 HTTP 2xx 即确认,另一个副本后续失败不会恢复已确认事件。全部请求使用 apiKey header;正常使用普通 fetch,隐藏时使用 keepalive,缺少 fetch 时正常页面降级异步 XHR。无 URI apiKey 或同步 XHR。
  • 首次即时发送:正常页面的首次请求直接发送,不受两个并发的队列限制;只有失败重试和历史恢复共用两个并发。浏览器仍决定实际网络调度。
  • 离页合批:收集所有未确认事件(包括正常请求在途的副本),关键事件优先,按相同 client/session/context/marketing/user 等请求上下文合并。一次回调内直接提交全部预算内批次,不等待前一批响应。
  • 总字节控制:单批及同一隐藏周期离页提交总量限制为 48 KiB UTF-8 字节,同时扣除尚未完成的 keepalive 在途字节;不同生命周期回调不重复提交同一隐藏周期已提交的事件。单事件超大或待确认队列满时丢弃新增事件。SDK 无法控制其他库占用的 keepalive 额度。
  • 送达边界:服务端必须按 event_id 去重;HTTP 2xx 仅作为传输确认。离页后后续排队请求不保证继续执行,已持久化的关键事件等下次同源访问恢复;用户不再访问、存储不可用、超过上限/有效期/重试预算时仍可能丢失。自动事件不保证跨页面补发。
  • 采样控制:sampleRate 在客户端决定是否上报,降低高流量场景的服务器压力

SPA 使用建议

路由切换时销毁旧实例,重建新实例以正确发送新页面的 page_view:

// 路由离开时
analytics.destroy();

// 路由进入时
analytics = createAnalytics({ ... });
// 或手动控制
analytics = createAnalytics({ ..., autoTrackPageView: false });
analytics.track('page_view', { page_url: location.href });

发送可靠性验证(1.3.2)

  • npm ci,npm run typecheck,npm test -- --runInBand。

  • npm run test:browser:Playwright + 本地 HTTP/CORS 接收端,覆盖密集请求、断网恢复、刷新、原生 pagehide、关闭重开、401。默认使用 macOS 已安装 Chrome;其他环境可设置 CHROME_PATH 或先安装 Playwright Chromium。

  • npm pack 会重新构建模块及浏览器包,排除测试产物。生产 APISIX 鉴权、真实入库和统计去重需要部署后单独验收;本地测试不调用生产接口。

  • npm run test:browser:comparison:使用相邻收银台实际安装的 Axios 对照 V1 传输(可用 CHECKOUT_DIR 指定仓库路径)。每场景双写 24 个事件,覆盖正常立即跳转、已有请求积压的慢网跳转、慢网关闭;离开后不再访问或初始化 SDK,接收端比较 V1 已收到的事件是否都包含在 V2 中。V1 省略指纹等待,使其发送时机更有利;这不是完整支付业务 E2E,也不代表生产零漏报。