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

tdem-rn-sdk

v0.0.14

Published

TDEM SDK for React Native (Native-first thin facade)

Readme

tdem-rn-sdk

TDEM 是腾讯云数字体验监控平台提供的监控 SDK。React Native 版采用 Native-first 薄壳(thin facade) 架构:崩溃、卡顿、ANR、启动、会话回放、挣扎行为等能力由 Android / iOS 原生 SDK 采集,JS 侧只负责 JS 错误、接口监控、路由 PV、RN 行为采集(点击 / 滑动 / 长按 / 表单),并统一交由原生通道上报。JS 进程不直连 collector。

环境要求

| 项目 | 要求 | |------|------| | react-native | >= 0.70.0 | | react | >= 18.0.0 | | Android | minSdk 21,compileSdk 33,Gradle / AGP 需运行在 JDK 11 | | iOS | 12.0+(示例工程按 12.4 配置),Xcode 15.3+ | | Expo | 需自定义原生构建(expo prebuild / expo-dev-client),不支持 Expo Go |

安装

npm install tdem-rn-sdk

Android

包内已提供 react-native.config.js,autolinking 会自动接入 TDEMRnPackage。原生 SDK 发布在 CNB Maven 源,需在工程根 android/build.gradle 补上仓库地址:

allprojects {
    repositories {
        maven { url "https://maven.cnb.cool/tencent-dem/release/-/packages/" }
    }
}

然后重新构建:

npx react-native run-android

Android Gradle Plugin 默认使用 7.3.1(通过 rootProject.ext.gradlePluginVersion 可覆盖),源码兼容 Java 8(sourceCompatibility = VERSION_1_8),JDK 11 / JDK 17 均可构建。

iOS

包内自带 TDEMiOSSDK.xcframework,通过 TDEMRn.podspec 一并链接,无需额外声明 Pod:

cd ios && pod install
npx react-native run-ios

安装 SDK 后必须重新构建原生工程。仅重启 Metro 会得到 TDEMRn native module is not linked 报错。

快速开始

把 TDEM 放在应用入口的第一导入(早于业务里的 ScrollView / TextInput),并在模块顶层创建实例,保证宿主组件包装在业务绑定之前生效:

import TDEM from 'tdem-rn-sdk'; // 必须先于业务 react-native 组件导入

const tdem = new TDEM({
  id: 'your-project-id',           // 项目标识(必填)
  url: 'https://dem.rumt-zh.com',  // 采集入口地址(必填)
  env: 'production',
  version: '1.0.0',
  userId: 'user-123',

  // JS 侧采集开关
  onError: true,                   // JS 错误 / Promise 未捕获,默认 true
  spa: true,                       // 路由 PV,默认 false
  api: true,                       // fetch / XHR 监控,默认 false
  behavior: true,                  // RN 行为采集,默认 false(还需 tdem.wrap)
});

function App() {
  return <YourApp />;
}

// 采集行为时必须用 wrap 包住根组件
export default tdem.wrap(App);

初始化配置

通用配置

| 参数 | 类型 | 必填 | 默认值 | 说明 | |------|------|------|--------|------| | id | string | 是 | - | 项目标识 | | url | string | 是 | - | 统一采集入口地址 | | userId | string | 否 | - | 业务用户标识 | | env | string | 否 | - | 运行环境 | | version | string \| number | 否 | - | 应用版本号 | | debug | boolean | 否 | false | 打开后 JS 侧输出 [TDEM-ReatNative] 调试日志 | | beforeReport | (event) => event \| null \| false \| void | 否 | - | 公共事件过滤钩子,作用于每条送往原生的事件;返回 null / false / undefined 即拦截 |

JS 侧采集开关

这些开关只影响 JS 插件,取值为 false 关闭、true 或对象形式开启。

| 参数 | 类型 | 默认值 | 说明 | |------|------|--------|------| | onError | boolean \| OnErrorConfig | true | JS 错误与未处理 Promise rejection | | spa | boolean \| SpaConfig | false | 路由 PV 自动上报 | | api | boolean \| ApiMonitorConfig | false | fetch / XMLHttpRequest 监控 | | behavior | boolean \| BehaviorConfig | false | RN 行为采集(需配合 tdem.wrap);true 打开点击 / 滑动 / 长按 / 输入 / 目标文案 |

原生侧采集开关

透传给 Android / iOS 原生 SDK。crash / replay / struggle 默认开启。

| 参数 | 类型 | 默认值 | 说明 | |------|------|--------|------| | crash | boolean | true | 崩溃采集(Android 为 Java + Native) | | replay | boolean | true | 会话回放 | | struggle | boolean | true | 用户挣扎行为 | | behavior | boolean | 恒为关 | 原生 TAP 自动采集当前始终关闭,RN 点击一律走 JS Fiber 采集,避免与 JS 侧重复计数 |

OnErrorConfig

interface OnErrorConfig {
  ignoreErrors?: string[];  // 错误 message 命中任一子串则不上报
}

SpaConfig

interface SpaConfig {
  ignore?: Array<string | RegExp>;  // 命中的路由名 / pathname 不上报 PV
}

ApiMonitorConfig

interface ApiMonitorConfig {
  apiDetail?: boolean;        // 是否附带请求体,默认 false;附带内容截断到 1000 字符
  ret?: string[];            // 读取业务码的响应字段名,默认 ret / retcode / code / errcode / errorcode
  retCodeHandler?: (data: string, url?: string, ctx?: unknown, payload?: unknown)
    => { code?: string | number; isErr?: boolean } | null | undefined;
  reqParamHandler?: (data: unknown, url?: string, ctx?: unknown) => unknown;  // 需 apiDetail
}

未指定 retCodeHandler 时,SDK 解析响应 JSON 顶层字段:命中 ret 列表中的字段则取其值作为业务码,值不为 0 / '0' 视为业务失败;否则若存在字符串 status,非 ok / success 视为失败。SDK 自身上报域名及常见第三方监控域名会被自动跳过。

BehaviorConfig

interface BehaviorConfig {
  scroll?: boolean;          // 滑动,默认 true
  longPress?: boolean;       // 长按,默认 true
  input?: boolean;           // 表单 focus/change/blur/submit,默认 true
  captureText?: boolean;     // 交互目标可见文案 target_text,默认 true
  scrollThreshold?: number;  // 判定滑动的位移(pt),默认 10
  longPressMs?: number;      // 判定长按的按住时长,默认 500
}

behavior: true 等价于打开上述全部子开关。闸门仍是「远端 started × 本地 behavior」;子开关只在插件已挂载后裁剪事件类型。原生 mapInitConfig.behavior 始终为 false,不会启动 Native TAP。

采集启动闸门

JS 插件是否挂载由远程配置和本地开关共同决定:

挂载 = 原生回传 started === true  且  本地开关 === true
  • 原生拉取远程配置后通过事件回传 { enabled, started, tdemSampleRate, replaySampleRate };
  • started 表示「远端开启 且 会话采样命中 且 运行时已启动」。远端开启但采样未命中时,JS 插件不会挂载,而不是挂上后再被原生丢弃;
  • 旧版原生桥若只回传 enabled,则回退按 enabled 判断;
  • 远程配置尚未回调前,任何 JS 采集插件都不挂载。

路由 PV

JS 不直接发 PV 事件,而是通知原生「换页」,由原生页面管理发出 page_view,因此点击、回放等事件都能共享同一个当前页。

React Navigation

import {NavigationContainer, createNavigationContainerRef} from '@react-navigation/native';
import TDEM, {createReactNavigationIntegration} from 'tdem-rn-sdk';

const navigationRef = createNavigationContainerRef();
const spa = createReactNavigationIntegration(tdem);

<NavigationContainer
  ref={navigationRef}
  onReady={() => spa.registerNavigationContainer(navigationRef)}
/>

嵌套 Stack / Tab / Drawer 会自动解析到叶子路由名(例如从 HomeTab 进入商品详情,上报的是 ProductDetail)。也可以不注册容器,改为在 onStateChange 中透传 state:

<NavigationContainer onStateChange={state => spa.onStateChange(state)} />

Expo Router

import {useEffect} from 'react';
import {useNavigationContainerRef} from 'expo-router';
import {createExpoRouterIntegration} from 'tdem-rn-sdk';

const spa = createExpoRouterIntegration(tdem);

export default function Layout() {
  const ref = useNavigationContainerRef();
  useEffect(() => {
    spa.registerNavigationContainer(ref);
    return () => spa.remove();
  }, [ref]);
  // ...
}

Expo Router 集成按 route.key 判重,因此 /post/1 → /post/2(同名不同 key)会被视为新页面。

pathname 方式

import {usePathname} from 'expo-router';
import {useTDEMPageView} from 'tdem-rn-sdk';

useTDEMPageView(tdem, usePathname());

手动换页

tdem.setPage('/checkout');                                  // 声明当前页
tdem.reportPageView('/checkout', {page_title: '结算页'});    // 换页并带标题
tdem.reportPageView('/checkout', {
  page_title: '结算页',
  page_url: '/checkout?step=1',                             // 可进一步区分 query 参数
});
tdem.getCurrentPage();                                       // 读取当前页

开启 spa 后,reportPageView 会自动去重(同名连续路由只报一次),命中 ignore 规则的路由不报。App 切到后台会报一次 _background_;回前台恢复离开前的页面,避免 click / scroll 挂在 _background_ 上。

行为采集

开启 behavior: true(或 BehaviorConfig)并用 tdem.wrap() 包裹根组件后,SDK 会:

  1. 在根节点挂一层全屏 TouchBoundary,按 DOWN → MOVE → UP 分类,抬起后只发一条 click / scroll / long_press(取消不发)。位移 ≥ 10pt 为滑动,按住 ≥ 500ms 为长按。
  2. JS 包装 react-native 上的 ScrollView / FlatList / SectionList / TextInput,补列表惯性滚动和表单事件。包装发生在 new TDEM({ behavior }) 与 wrap() 时,因此 TDEM 必须作为入口第一导入。
export default tdem.wrap(App);

滑动方向:|dx| > |dy|*1.5 为 left/right,|dy| > |dx|*1.5 为 up/down,否则 mixed。列表滚动若与 Boundary 刚发出的 swipe 时间接近且位移相近,会去重。

目标名 target_element 的解析优先级:

  1. 可读标签(就近取第一个):tdem-label → accessibilityLabel → aria-label → testID
  2. 无标签时取组件名路径,自外向内用 > 拼接,最多 4 段(View / Text 等宿主基础组件会被跳过)

建议给关键可点区域加 accessibilityLabel 或 testID,可读性最好。无法识别目标时不上报。点击 / 长按带屏幕坐标(已换算为原生坐标)和当前页面名。

click / scroll / long_press 还可带 target_text:运行时从 Fiber 可见 Text children 抽取,截断 127 字符。不会读取 TextInput 的 value,密码 / secureTextEntry 跳过。命中 token / 手机号 / 邮箱时整段不报(与 iOS / Android 同一套规则);tdem-label / accessibilityLabel 命中则回落到下一档身份。behavior.captureText === false 可关。

表单事件只报身份和长度分桶,不上报明文:

| 时机 | 事件 | 字段 | |------|------|------| | focus | form_focus | target_element、field_type、is_secure、value_length_bucket | | 失焦且长度变化 | form_change | 同上 + change_source(单字符 typing / 多字符 paste);编辑中 500ms 节流一条 | | blur | form_blur | measurements.focus_duration_ms | | onSubmitEditing | form_submit | submit_method: 'confirm' |

value_length_bucket 为 0 / 1_10 / 11_50 / 51_plus。secureTextEntry 或 textContentType 为 password / newPassword 时只报 field_type=secure、is_secure=1。

构建期组件标注(可选)

打开后 Metro 会在编译期给应用 JSX 注入 data-tdem-component / data-tdem-element / data-tdem-source-file,使 click 事件能带上源文件名,便于对照源码定位。默认关闭,关闭时点击采集仍正常工作。

在 metro.config.js 中包一层:

const {withTDEMConfig} = require('tdem-rn-sdk/metro');

module.exports = withTDEMConfig(
  {
    // 你原有的 Metro 配置
  },
  {annotateReactComponents: true},
);

同时在 babel.config.js 加上标注插件,避免 Metro worker / 缓存没带上 .tsx:

module.exports = {
  presets: ['module:metro-react-native-babel-preset'],
  plugins: [require('tdem-rn-sdk/metro-annotate')],
};

对象形式可进一步配置:

| 选项 | 类型 | 默认值 | 说明 | |------|------|--------|------| | ignoredComponents | string[] | [] | 跳过标注的组件名 / 标签名 | | autoInjectLabel | boolean | false | 自动把组件内首个静态文案注入为 tdem-label(截断 64 字符);已有 tdem-label / accessibilityLabel / testID 时不覆盖 | | textComponentNames | string[] | ['Text'] | autoInjectLabel 读取文案时视为文本组件的名字 |

module.exports = withTDEMConfig(config, {
  annotateReactComponents: {
    autoInjectLabel: true,
    ignoredComponents: ['DebugPanel'],
  },
});

修改标注配置后需清缓存重启 Metro:npx react-native start --reset-cache。

实例方法

自定义事件

tdem.track('add_to_cart', {
  tags: {channel: 'app'},
  properties: {sku: 'A-1001'},
});

tdem.trackStruggle('repeat_submit', {
  severity: 'high',
  tags: {page: 'checkout'},
});

自定义测速

// 方式一:SDK 计时
tdem.startMeasure('load_profile');
await loadProfile();
tdem.endMeasure('load_profile', {tags: {source: 'cache'}});

// 方式二:自行传入耗时(ms)
tdem.measure('load_profile', 128, {properties: {size: '2kb'}});

endMeasure 若没有配对的 startMeasure 则静默忽略。

日志与异常上报

tdem.captureMessage('checkout entered', 'info');  // level 默认 'info'
tdem.captureException(new Error('pay failed'));   // 也接受字符串

用户标识与全局标签

tdem.setUser('user-123');
tdem.clearUser();

tdem.setTags({app_channel: 'appstore', ab_group: 'B'});
tdem.removeTags('ab_group');            // 也支持字符串数组
tdem.clearTags();

页面停留

tdem.startPage('/product/1');  // 进入页面
tdem.leavePage();              // 离开页面

会话与生命周期

const config = tdem.getConfig();       // 读取当前配置
const sessionId = await tdem.getSessionId();

tdem.endLaunch();     // 手动结束启动打点(需开启 launch)
await tdem.flush();   // 立即上报缓冲数据
await tdem.stop();    // 停止采集

原始事件上报

tdem.reportTDEMEvent({
  event_type: 'custom_event',
  event_category: 'behavior',
  timestamp: Date.now(),
  data: {key: 'value'},
});

支持传入数组批量上报。所有事件都会先经过 beforeReport,再交给原生通道。

事件一览

JS 侧产生的事件类型:

| event_type | event_category | 触发来源 | |--------------|------------------|----------| | js_error | error | 全局 JS 异常(onError) | | promise_error | error | 未处理的 Promise rejection(onError) | | api_call | api | 每次 fetch / XHR 完成(api) | | api_error | api | 网络失败、HTTP 错误或业务码失败(api) | | click | behavior | 轻点可识别目标,抬起后发出(behavior + wrap) | | scroll | behavior | 滑动或列表滚动(位移 ≥ 10pt) | | long_press | behavior | 按住 ≥ 500ms 后抬起 | | form_focus / form_change / form_blur / form_submit | behavior | TextInput 焦点与长度分桶,无明文 | | measure | measure | endMeasure | | log | log | captureMessage |

page_view 由原生页面管理发出,崩溃、卡顿、ANR、启动、回放、挣扎等事件同样由原生直接采集上报。

beforeReport 中可按类型区分处理:

beforeReport: (event) => {
  if (event.event_type === 'api_call' && String(event.data?.url).includes('/health')) {
    return null;  // 拦截
  }
  return event;
}

导出常量

SDK 将事件类型以常量形式导出,便于在 beforeReport 或自定义逻辑中做类型比较:

import TDEM, {
  JS_ERROR_EVENT, PROMISE_ERROR_EVENT,
  API_CALL_EVENT, API_ERROR_EVENT,
  CLICK_EVENT, SCROLL_EVENT, LONG_PRESS_EVENT,
  FORM_FOCUS_EVENT, FORM_CHANGE_EVENT, FORM_BLUR_EVENT, FORM_SUBMIT_EVENT,
  MEASURE_EVENT, LOG_EVENT, PAGE_VIEW_EVENT, SET_PAGE_EVENT,
  BEHAVIOR_CATEGORY, ERROR_CATEGORY, API_CATEGORY,
} from 'tdem-rn-sdk';

// 示例:只拦截点击事件
beforeReport: (event) => {
  if (event.event_type === CLICK_EVENT && event.event_category === BEHAVIOR_CATEGORY) {
    return null;
  }
  return event;
}

高级导出

以下导出面向自定义集成和插件开发场景。

Plugin 类

用于编写自定义采集插件,遵循 init → onNewTdem → destroy 生命周期:

import TDEM, { Plugin } from 'tdem-rn-sdk';

const myPlugin = new Plugin({
  name: 'myPlugin',
  init(config) { /* SDK 初始化时调用一次 */ },
  onNewTdem(instance, config) { /* 每个实例 patch 时调用 */ },
  destroy(instance) { /* 插件卸载时清理 */ },
});

TDEM.use(myPlugin);     // 注册
TDEM.unuse(myPlugin);   // 移除

getActiveRouteName

从 React Navigation state 中解析叶子路由名,适用于在 onStateChange 中自行处理路由:

import { getActiveRouteName } from 'tdem-rn-sdk';

function onStateChange(state) {
  const routeName = getActiveRouteName(state);
  if (routeName) {
    tdem.reportPageView(routeName);
  }
}

captureClickFromTouch

手动从触摸事件触发点击采集,适用于无法使用 tdem.wrap() 的场景(如自定义手势系统):

import { captureClickFromTouch } from 'tdem-rn-sdk';
import type { TouchLikeEvent } from 'tdem-rn-sdk';

function handleTouch(event: TouchLikeEvent) {
  captureClickFromTouch(tdem, event);
}

常见问题

TDEMRn native module is not linked 安装依赖后没有重新构建原生工程。执行 pod install 并重新 run-ios / run-android;Expo 需要自定义原生构建,Expo Go 不支持。

没有任何事件上报 按顺序检查:tdem.isNativeStarted() 是否为 true → tdem.getRemoteConfig() 是否已回调 → tdem.canCollect() 是否放行(远端开关、采样命中)→ 对应本地开关是否已开。开 debug: true 可看到 plugin patch / plugin skip 与每条 report 日志。

点击 / 滑动没有采到 确认 behavior: true,且导出的是 tdem.wrap(App);TDEM 必须作为入口第一导入,否则 ScrollView / TextInput 包装可能错过已绑定的宿主组件。点击目标需可识别(加 accessibilityLabel / testID 最稳)。

Failed to construct transformer / Dynamic requires are not currently supported 旧版把 Metro transformer 打成了单包,运行时 require 宿主 babel transformer 会失败。升级到当前包后执行 npx react-native start --reset-cache。metro.config.js 的用法不用改,也不要手写 babelTransformerPath 去指 SDK 内部文件。

Android 构建失败 确认已配置 CNB Maven 仓库地址,且 JAVA_HOME 指向 JDK 11 或 JDK 17。

开发

npm run build    # bob → lib/;runtime rollup+terser → build/;metro 保持未打包 CJS
npm test         # 运行单元测试
npm run watch    # 监听模式

example/ 目录是可运行的示例工程,覆盖错误、接口、路由 PV、点击 / 滑动 / 输入采集等场景。example/index.js 第一行是 import './tdem',保证 new TDEM({ behavior }) 在 App 绑定 ScrollView / TextInput 之前改写宿主组件:

cd example
npm install
npm run android   # 或 npm run ios

License

MIT