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-sdkAndroid
包内已提供 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-androidAndroid 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 installnpx 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 会:
- 在根节点挂一层全屏
TouchBoundary,按 DOWN → MOVE → UP 分类,抬起后只发一条click/scroll/long_press(取消不发)。位移 ≥ 10pt 为滑动,按住 ≥ 500ms 为长按。 - 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 的解析优先级:
- 可读标签(就近取第一个):
tdem-label→accessibilityLabel→aria-label→testID - 无标签时取组件名路径,自外向内用
>拼接,最多 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 iosLicense
MIT
