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

elevator-link

v0.8.0

Published

对齐后端 elevator 通用埋点接口的前端 SDK

Readme

elevator-link

对接 elevator 通用埋点接口的前端采集 SDK。页面浏览、停留时长、元素点击、热区曝光、站外跳转全自动采集,业务侧只需声明两个 DOM 属性。

包内不含任何域名、白名单与密钥,baseUrl 一律由接入方传入。采集范围由服务端配置下发,加减埋点不需要前端发版。

安装

npm i elevator-link

快速开始

import { Track } from 'elevator-link'

// 放在首屏渲染完成之后调用
void Track.init({
  baseUrl: 'https://elevator.example.com',
  bizLine: 'myoffer',
  bizEntityAlias: 'hsbc-hk',
})

不必 await,也不必 try/catch。参数缺失、接口拒绝、网络失败都会被内部收敛成「打一行告警 + 不采集」,不会向业务抛错,也不会阻塞页面。

启动时机要放在首屏 DOM 就绪之后。 init 的最后一步会读页面根节点的 data-track-page 作为 pageId,DOM 还空着就启动会读到 null,首屏的元素采集不会注册。用 vue-router 的话等 router.isReady():

void router.isReady().then(() => {
  app.mount('#app')
  Track.init({ /* ... */ })
})

声明采集对象

页面根节点标 pageId,需要采集的元素标 trackId:

<div data-track-page="OfferHome">
  <button data-track-id="btn-activate" data-track-props='{"offerId":"OF20260901"}'>
    立即启用
  </button>
  <div data-track-id="home-banner">首页横幅</div>
</div>

data-track-page 和 data-track-id 的值必须出现在后端 /track/client/init 下发的配置里,SDK 才会采集。页面上挂了属性但配置里没有的元素不会产生任何事件 —— 采集范围由服务端配置控制,前端加属性不用等后端、后端改配置不用前端发版。

data-track-props 是可选的业务扩展属性,JSON 字符串,原样透传进事件的 props。上限 2KB,超限时整个 props 被替换为 { propsTruncated: true }。

采集的事件

| 事件 | 触发时机 | durationMs | |---|---|---| | site_enter | 一次访问的首个页面 | 无 | | page_view | 页面结束时(路由切换 / 卸载 / 切后台) | 本页可见停留时长 | | click | 点击当场 | 无 | | area_view | 页面结束时,一个热区一条 | 该热区累计可见时长 | | site_leave | 经由本站跳转到外部站点 | 无 |

时长统计的都是可见时长:切到后台的那段不计入。热区以可见面积 ≥50% 为准,累计不足 1 秒不产生事件。

环境字段

每条事件都带 deviceType(Mobile / PC)、os、browser、ua。前三个是 SDK 解析 UA 的结果,os 与 browser 认不出时为空;ua 是 navigator.userAgent 原串,不裁剪不清洗,供后端做二次解析或排查设备问题。四个字段在 Track.init 时解析一次,之后每条事件复用。

navigator 不可用(SSR、某些 WebView)时四个字段全为空,事件照常上报。

热区被点过会带 props.areaClick = true

这一段时间里落点在热区范围内发生过点击,那条 area_view 就带 props: { areaClick: true },没点过不带这个键(空即未点,不发 false)。不产出额外的 click 事件,所以任何点击量口径都不受影响。

判定只看落点在不在热区的 DOM 范围内 —— 不要求落点自己声明 data-track-id,也不要求它配了 click。问的是「这片区域有没有被点」,不是「哪个按钮被点了」。标记和 durationMs 一样是增量语义,每次结算后清零,所以拆段后的下一段要重新点过才会再带。

areaClick 不在服务端的提升键列表里,落在 props JSON 列。要按它筛选又不想拆 JSON 的话,需要后端把它加进提升键列表、加一个表列。元素自己在 data-track-props 里写的键不受影响,两者并存。

page_view 与 area_view 的 durationMs 是增量语义 —— 同一页面若因切后台等原因多次结算,各条独立,相加得到总时长。

上下文变化会拆分时长

page_view 与 area_view 都在页面结束才结算,而用户可能中途切了语种、登录、登出或换了卡。所以 Track.setLang()、Track.setUser()、Track.clearUser() 都会先把当前页已累计的停留与曝光按旧上下文结算上报,再以新上下文重新起段。

不拆的话一条事件要横跨两种上下文,只能记其中一边:切语种后整段停留挂新语种;token 过期登出后,登录态下那段浏览被记成匿名(uid / cid 为空)。

「繁中下看 20 秒 → 切成英文 → 又看 30 秒 → 刷新」得到:

| 事件 | lang | durationMs | |---|---|---| | page_view | zn_HK | 20 秒 | | area_view | zn_HK | 该热区在繁中下的曝光 | | page_view | en_HK | 30 秒 | | area_view | en_HK | 该热区在英文下的曝光 |

前两条在切换那一刻发出,后两条在刷新时发出。durationMs 是增量语义,同一页面各条相加仍等于总停留。

登录态同理:「匿名浏览 20 秒 → 登录 → 又看 30 秒」得到两条 page_view,前一条 uid / cid 为空,后一条带身份。只换 sessionToken 不拆段(不影响事件归属);用相同的 uid / cid 重复调 setUser() 也不拆段,业务可以放心用 watch(..., { immediate: true }) 同步身份。

拆出来的续段不足 1 秒不单独成条,与热区「划过不算」同一把尺。业务改身份的写法基本都是「先更新身份、紧接着跳转」—— 登录成功后 setUser() 再 router.push,退出登录 / 改密成功后 clearUser() 再回登录页。中间只隔几十到几百毫秒,页面其实已经在离开了,不拦的话每次登录、每次登出都会多一条几乎零时长的记录。身份真的在浏览途中变化时(token 过期那种)续段是长的,照常成条。

切成相同语种同样不产生任何事件。热区的上下文快照会跟着更新,所以原地保留(没被重渲染换掉)的热区在切换后那一段也带新值。

click 这类即时事件不受影响,本来就是发生当下取值。

报表侧注意事件条数变化 —— 同一页面可能有多条 page_view,按 (visit_id, page_id) 去重算浏览量、按 SUM 算停留。

空闲超时 15 分钟

连续 15 分钟没有任何用户操作,就认为访客已经离开:结算上报一次当前页的 page_view 和各热区 area_view,然后停止计时、不再续。用户开着标签页走人,这一页的数据会在 15 分钟后落库,而不是永远不上报、或者等浏览器关闭时产出一条十几小时的记录。

停留时长记到最后一次操作为止,空闲那段不算。 用户第 2 分钟停止操作、第 17 分钟被判定离开,上报的 durationMs 是 2 分钟 —— 事件在第 17 分钟发出,但时长不含中间那 15 分钟。

这不是「时长上限」。 任何操作都会把计时器推后,所以一直在用的页面停留时长可以正常超过 15 分钟 —— 每 10 分钟操作一次连用 40 分钟,得到的是一条 40 分钟的 page_view,不会被切成多条。

算作操作的事件:click、keydown、scroll、mousemove、touchstart、wheel,外加「从后台切回本标签页」。scroll 和 mousemove 必须算在内,否则读长文的用户会被误判成已离开。全部以 passive 监听注册,处理函数只写一个时间戳。

全程没有任何操作的页面会记 durationMs: 0,这条 page_view 照发 —— 它表达「页面打开过但没有任何互动」,是有用的跳出信号。duration 本来就是增量语义,报表按 (visit_id, page_id) 去重算浏览量、按 SUM 算停留,0 值对两者都没有影响。

超时上报后该页面不再产生停留数据,之后切前后台、切路由都不会补发。但用户若回来继续点,click 事件照常上报 —— 点击不依赖页面计时。

页面在后台时不做空闲判定:切后台那一刻已经结算过停留了,而且「切到别的标签页」本身说明不了用户是否离开,等切回来重新起算。判定是 10 秒一次轮询,实际触发比 15 分钟最多晚 10 秒。

离开本站(site_leave)

site_leave 表示访客经由本站跳转到了外部站点,目标地址记在 props.content(完整 URL),检测来源记在 props.leaveReason。

自动检测三类,不需要给元素加 data-track-id,也不受下发配置约束:

| 方式 | leaveReason | |---|---| | 点击 <a href="站外">(含 target="_blank") | anchor | | <form action="站外"> 提交 | form | | window.open('站外') | window_open |

location.href = url 拦不住,需要业务显式通知:

Track.leaveSite(url)   // leaveReason: 'explicit_call'
location.href = url

原因是 HTML 规范把 Location 的 href、assign、replace 标记为 [LegacyUnforgeable],不可重定义也不可覆盖,SDK 无法包装。顺序要先调 leaveSite 再赋值 —— 反了页面已经在卸载,请求可能来不及发出。

站外的判定按 origin 严格比较(协议 + 域名 + 端口)。以下都不算离开:同源链接、javascript: / mailto: / tel: 等非 http(s) 协议、纯 # 锚点、空 href、带 download 属性的下载链接。

target="_blank" 算离开,但当前页面不会卸载,所以访客可以连续点多个外链,每次各记一条。

刷新、关闭标签、切到后台都不产生 site_leave —— 访客并没有离开本站。这些时刻只结算 page_view 和 area_view。因此本 SDK 不提供「整次访问时长」这个字段,需要的话由数仓按 visit_id 聚合 page_view.duration_ms 得出(它是增量语义,可以直接 SUM)。

富文本与动态内容

运营后台注入的富文本(v-html / innerHTML)里的外链无需任何埋点属性即可检测,因为 click 走的是 document 上的事件委托,动态插入的节点天然覆盖,不需要注册。深层嵌套、协议相对写法(//host/path)、<svg> 里的 <a> 都能正确取到目标。

链接目标读的是 HTMLAnchorElement.href(浏览器解析后的绝对地址)而非原始属性,所以页面存在 <base href> 时不会漏报 —— 读原始属性会把 base 指向站外的相对链接判成同源。<form action> 同理。

iframe 内的外链检测不到。 iframe 是独立文档,点击不冒泡到父文档,同源也一样。富文本用 iframe 渲染时(部分编辑器的预览模式)里面的跳转全部采集不到,要支持得在 iframe 内部单独初始化一次 SDK,跨域则无解。

API

Track.init(options)            // 启动采集,幂等
Track.setUser({ uid, cid, sessionToken })  // 登录成功后调用
Track.clearUser()              // 登出
Track.setLang('zh-HK')         // 站点语言切换
Track.setEntity('boc-hk')      // 切业务主体(传别名),会重新拉配置并重建监听
Track.setChannel('kol_abc')    // 渠道来源
Track.leaveSite(url)           // location.href 跳转前主动通知

业务主体(别名换标识)

bizEntityAlias 传的是别名,事件里上报的 bizEntity 是服务端按别名换出来的标识,两者可以不同值。SDK 不提供直接设置 bizEntity 的入口 —— 目的是让同一个主体在数仓里只有一种口径,否则各接入方传 hsbc / HSBC / hsbc-hk 会裂成三个主体。

别名未登记时服务端把 bizEntity 回落为别名原文并返回空 pages:页面浏览、停留、进入离开照常采集,元素采集不启动。接入方不需要为这种情况写分支。

setEntity(alias) 切主体时会请求 /track/client/config 换配置。请求返回前沿用上一份 bizEntity,失败则主体与配置整体保持原样,不会出现空主体的中间态。

sessionToken 传明文,SDK 取其 SHA-256 前 32 位作为 sessionId 上报,明文不出网。

登录前已上报的事件不会被追溯改写,分析时靠 clientId / visitId 串联登录前后的行为,所以不必为了等登录而延后 init。

init 之外的方法都是纯本地状态写入(setEntity 例外,会发一次配置请求),可以安全地重复调用。

InitOptions

| 字段 | 必填 | 说明 | |---|---|---| | baseUrl | ✅ | elevator 服务地址,不含路径 | | bizLine | ✅ | 业务线标识,需在后端注册 | | bizEntityAlias | | 业务主体别名,由研发在服务端登记;未传时按空串匹配。真正入库的 bizEntity 由服务端换出后下发,SDK 不接受直接指定 | | lang | | 显式语种,优先级高于 URL 参数与 navigator.language | | channel | | 显式渠道,未传时从 URL 的 channel / utm_source 取 |

接入约定

data-track-page 挂在每个页面自己的根节点上。 SDK 取 document.querySelector('[data-track-page]') 的第一个命中元素,挂在 <RouterView> 外层容器会让所有页面共用同一个 pageId。

data-track-id 在同一页面内要唯一。 热区结算按元素逐条产出,同一 trackId 挂在 N 个同时存在的元素上会得到 N 条 area_view,trackId 相同、无法区分是哪一个被看到。click 没有这个问题。

元素因重渲染被替换是另一回事:旧节点在移除时停止计时、新节点从零开始,两段各自成条 —— 这是有意的,因为它们可能发生在不同的语种下(见下)。报表要该热区在本页的总曝光时,按 (visit_id, page_id, track_id) 求和。

异步路由组件、异步路由守卫都支持(0.6.1 起)。SDK 在 history.pushState 之后用 setTimeout(..., 0) 读 data-track-page,那一刻 DOM 可能还是上一个页面的(浏览器后退 + 守卫里有网络请求最容易撞上)。所以 SDK 还盯着 data-track-page 的属性变化,值变了就校正当前段的页面归属并按新配置重新注册热区,不产出额外事件。

埋点元素允许嵌套。 点击时沿祖先链往上找第一个「配置里监听了 click」的元素,所以卡片埋点套按钮埋点的场景下,点没配置的按钮会正确归到外层卡片。一次点击只上报一条,不向上重复。

运行环境

需要 IntersectionObserver(Safari ≥ 12.1)。不支持时热区采集整体降级关闭,其余采集不受影响。

sendBeacon、localStorage、sessionStorage、crypto.randomUUID 缺失时均有降级路径。sessionStorage 不可用(如 Safari 无痕)时 visitId 无法跨页面延续,每次页面加载会成为一次新访问。

已知边界

访客通过地址栏输入、书签、或直接关闭标签离开时,SDK 无法感知目标,不产生 site_leave —— 这是刻意的,那些情况并非「经由本站跳转」。

iframe 内的点击采集不到(见上文富文本一节)。

计时使用 Date.now(),它不是单调时钟。页面长时间停留期间若发生系统校时,durationMs 可能出现跳变。

变更记录

0.8.0

  • 每条事件新增 ua 字段,值是 navigator.userAgent 原串(不裁剪、不清洗),与原有的 deviceType / os / browser 并存。后端需要先加 ua 列,否则这个字段会被丢弃

0.7.1

  • 热区点击标记的键由 content(字符串 'true')改为 areaClick(布尔 true)。0.7.0 请直接跳过 —— 两者是同一个功能的两种写法,content 那版不再支持

0.7.0

  • 热区在本段时间内被点过时,area_view 带点击标记(没点过不带这个键)。不产出额外的 click 事件,点击量口径不受影响

0.6.2

  • 修复登录 / 退出登录 / 改密成功后,所在页面多出一条几乎零时长的 page_view(同一个 page_id 两条,一条带 uid 一条不带)。这些流程都是先更新身份再跳转,resegment 拆出来的续段只有几十到几百毫秒。现在续段不足 1 秒不单独成条,与热区用同一个门槛

0.6.1

  • 修复浏览器后退回到某个页面后,该页面的 area_view 一条都不上报、page_view 的 pageId 还是上一页的。成因是 URL 在 popstate 之前就变了,而框架要等异步路由守卫跑完才渲染,SDK 读 data-track-page 时读到的还是上一页的值。现在 data-track-page 变化会校正当前段的页面归属并按新配置重新注册热区(不产出额外事件)

0.6.0

  • Track.setUser() / Track.clearUser() 在 uid 或 cid 实际变化时也会结算当前页并重新起段。修的是「登录态浏览一阵、token 过期登出后,那段浏览的 uid / cid 被记成空」—— 登录 / 登出 / 切卡前后的停留与曝光现在各自成条。报表侧注意事件条数变化
  • 只更新 sessionToken、或用相同 uid / cid 重复调用,都不产生额外事件

0.5.0

  • Track.setLang() 现在会结算当前页并以新语种重新起段:切语种前后的停留与曝光各自成条、时长独立统计,page_view 与 area_view 都按语种拆分。报表侧注意事件条数变化 —— 同一页面可能有多条 page_view,按 (visit_id, page_id) 去重算浏览量、按 SUM 算停留
  • 热区的语种快照随 setLang 更新,原地保留的热区在切换后那一段带新语种

0.4.2

  • 修复 page_view / area_view 的 lang 取值时机:改为记录该段时长发生时的语种,此前取的是上报时刻的值。这两种事件都在页面结束才结算,用户中途切过语种的话,发生在旧语种下的停留与曝光会被打上新语种的标签
  • 撤销 0.4.1 的热区按 trackId 合并:重渲染换掉节点产生的多段曝光分属不同上下文(语种就不同),各自成条才对。报表要总曝光时按 (visit_id, page_id, track_id) 求和

0.4.1

  • 结算后清理已脱离文档且时长已结算的热区条目,避免频繁重渲染的页面上内部 Map 无限增长
  • (本版引入的「热区按 trackId 合并」已在 0.4.2 撤销,勿使用本版)

0.4.0

破坏性变更:业务主体改为「别名换标识」。 接入方传别名,真正入库的 bizEntity 由服务端换出后下发。

  • Track.init({ bizEntity }) → Track.init({ bizEntityAlias })
  • Track.setEntity(bizEntity) → Track.setEntity(bizEntityAlias)
  • 事件的 bizEntity 一律取 init / config 响应值,不再等于传入的入参;SDK 不提供直接设置它的入口
  • 别名未登记时服务端把 bizEntity 回落为别名原文并返回空 pages,页面级行为照常采集,SDK 无额外分支
  • setEntity 请求失败时主体与配置整体保持原样,不会出现空主体的中间态

升级要点: 接入方原来传的主体标识,需由研发在服务端 track_biz_entity.biz_entity_alias 登记后才生效。别名与 biz_entity 登记为同值时,接入方只改字段名即可,无其他调整。

0.3.0

  • 新增空闲超时:连续 15 分钟无操作即认为访客离开,结算上报当前页的 page_view / area_view 后停止计时,不再续。有操作会把计时推后,正常使用的页面停留时长仍可超过 15 分钟
  • 空闲结算的时长记到最后一次操作为止,不含空闲那段(第 2 分钟停手、第 17 分钟上报,记 2 分钟)
  • 路由切换的结算加上空批判断,不再可能落 duration=0 的 page_view(空闲超时那条除外,它有意义)

0.2.0

破坏性变更:site_leave 语义完全改变。 原先由页面卸载与切后台触发、携带整次访问累计时长;现在只在经由本站跳转到外部站点时产生、不携带 durationMs,目标地址落在 props.content。

  • site_leave 新增 props.content(目标完整 URL)与 props.leaveReason(anchor / form / window_open / explicit_call)
  • 新增 Track.leaveSite(url),用于 location.href 这类无法拦截的跳转
  • 移除 props.visitDurationPartial(随 durationMs 一起失去意义)
  • 刷新、关标签、切后台改为只结算 page_view 与 area_view
  • 需要「整次访问时长」的话,改由数仓按 visit_id 聚合 page_view.duration_ms

同版本修复:

  • 首屏热区未被 IntersectionObserver 观察,导致首屏 area_view 恒为空
  • 跨路由存活的热区(固定侧栏、keep-alive 复用)从第二个页面起不再统计
  • 切后台再切回后,page_view 会把已上报的时长重复计入
  • 点击「嵌套在已配置元素内的未配置元素」时,外层的 click 被吞掉
  • body 超过 64KB 时未按体积拆批,在卸载期通道上会整批发不出去
  • init 失败时仍写入访问状态,导致服务恢复后 site_enter 永久不再上报
  • <base href> 存在时,指向站外的相对链接被漏判为同源

0.1.0

首个版本。

License

MIT