@arms/rum-reactnative 0.1.3-beta.2 → 0.1.3-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,6 +4,7 @@
4
4
 
5
5
  ## 功能特性
6
6
 
7
+ - **原生崩溃采集** — 自动捕获 iOS(KSCrash)与 Android(Java + NDK)原生崩溃,支持堆栈双侧截断与数据体积优化
7
8
  - **API性能监控** — 自动拦截 XMLHttpRequest / fetch 请求,采集耗时、状态码等指标
8
9
  - **JS错误捕获** — 自动捕获 JavaScript 运行时异常
9
10
  - **控制台错误监听** — 监听 `console.error` 输出
@@ -4,9 +4,81 @@
4
4
 
5
5
  ## 版本列表
6
6
 
7
- | 版本 | 发布时间 | 发布说明 |
8
- |------|----------|----------|
9
- | v0.1.3 | 2026年07月 | 原生崩溃采集安全加固与自监控系统集成:<br/>• 修复 Android/iOS 原生崩溃采集多项安全漏洞(缓冲区溢出、路径遍历、crash_id 校验)<br/>• 提升采集稳定性(栈帧上限 256、线程安全、多格式时间戳解析)<br/>• 集成 rum-core Telemetry 自监控,SDK 内部异常自动上报<br/>• 公开导出 logger、callMonitored 供用户扩展使用 |
10
- | v0.1.2 | 2026年05月12日 | 包名正式化与核心采集能力修复:<br/>• rum-core 升级至 >=0.1.4,包管理迁移至 Yarn 3 workspaces<br/>• 修复 PV 路由采集(首屏丢失、无 path 路由过滤、navigationRef 时序)<br/>• 修复 API 监控默认配置误关闭、propagatorTypes 空值崩溃<br/>• Reporter AppState 回调 this 绑定修复<br/>• 完善 README、接入指南、配置参考等文档 |
11
- | v0.1.1 | 2025年11月28日 | React Native SDK首个正式版本,提供各种配置、API,并提供了以下采集能力:<br/>• API监控(XMLHttpRequest / fetch),包括链路追踪等特性的支持<br/>• 异常监控(JS Error、Promise Rejection、console.error)<br/>• Session相关功能(采样、超时管理)<br/>• 自定义上报(Custom、Exception、Resource、View、Event)<br/>• React Navigation集成支持 |
12
- | v0.1.0 | 2025年08月20日 | React Native首个版本,提供各种配置、API,并提供了以下采集能力:<br/>• API监控(XMLHttpRequest / fetch),包括链路追踪等特性的支持<br/>• 异常监控(JS Error、Promise Rejection、console.error)<br/>• Session相关功能(采样、超时管理)<br/>• 自定义上报(Custom、Exception、Resource、View、Event)<br/>• React Navigation集成支持 |
7
+ ### v0.1.3(2026年07月)
8
+
9
+ 原生崩溃采集安全加固与自监控系统集成。
10
+
11
+ - 修复 Android/iOS 原生崩溃采集多项安全漏洞(缓冲区溢出、路径遍历、crash_id 校验)
12
+ - 提升采集稳定性(栈帧上限 256、线程安全、多格式时间戳解析)
13
+ - 集成 rum-core Telemetry 自监控,SDK 内部异常自动上报
14
+ - 公开导出 logger、callMonitored 供用户扩展使用
15
+
16
+ #### v0.1.3-beta.3(2026年07月16日)
17
+
18
+ API 监控增强与代码规范化。
19
+
20
+ - 新增 XMLHttpRequest abort 事件处理,触发时标记 success 为 Failed
21
+ - 修正 reviseApiAttr 保留 success 三态值(-1/0/1)的行为
22
+ - 将 reporter sendRequest 失败日志等级由 error 降为 warn
23
+
24
+ #### v0.1.3-beta.2(2026年07月16日)
25
+
26
+ React Native 0.68-0.70 多版本兼容适配。
27
+
28
+ - react-native peerDependencies 版本下限调整为 0.68.0
29
+ - podspec / Android build.gradle 按 RN 版本条件适配依赖与插件配置
30
+ - iOS crash 处理和代码生成适配多 RN 版本环境
31
+ - 修复多平台崩溃采集数据不统一问题,保证字段和结构一致性
32
+ - 强化 NDK 崩溃处理边界检查,提升稳定性
33
+ - 缺少原生模块时自动降级,不影响纯 JS 功能运行
34
+
35
+ #### v0.1.3-beta.1(2026年07月16日)
36
+
37
+ 崩溃堆栈截断策略与模块基址计算修复。
38
+
39
+ - 统一安卓与 iOS 端双侧截断堆栈帧策略,保留前 256 + 后 256 帧,中间插入标记帧
40
+ - NDK 最大帧数由 256 扩充至 512
41
+ - 模块基址计算改用 rum_module_base_for_seg,确保 referenced_addrs 与 binary_images 过滤一致
42
+ - 添加边界检查防止 ELF note section 解析时二次 SIGSEGV 崩溃
43
+
44
+ #### v0.1.3-beta.0(2026年07月15日)
45
+
46
+ iOS 与 Android NDK 崩溃采集修复及数据体积优化。
47
+
48
+ - 修复 iOS objc_exception 和 cpp_exception 被 RN 桥拦截问题,改为新 NSThread 抛出异常绕过拦截
49
+ - 修复 iOS KSCrash 安装失败静默吞掉、userInfo 异步更新竞态及 context 同步延迟问题
50
+ - 优化 binary_images 按需过滤,iOS 减少 70-85%、Android NDK 减少 70-90% 崩溃数据体积
51
+ - 移除 Android Java 崩溃 all_threads 字段,减少 60-80% 负载
52
+ - 修复 Android NDK 崩溃文件重复上报问题,统一 crash_id 和文件名前缀匹配逻辑
53
+ - 补充双平台缺失字段:iOS device brand/app package,Android rn_version,NDK platform/exception 结构
54
+ - 修复 NDK 崩溃信号处理器二次 SIGSEGV,增加地址映射检测和缓冲区中间刷新
55
+
56
+ ### v0.1.2(2026年05月12日)
57
+
58
+ 包名正式化与核心采集能力修复。
59
+
60
+ - rum-core 升级至 >=0.1.4,包管理迁移至 Yarn 3 workspaces
61
+ - 修复 PV 路由采集(首屏丢失、无 path 路由过滤、navigationRef 时序)
62
+ - 修复 API 监控默认配置误关闭、propagatorTypes 空值崩溃
63
+ - Reporter AppState 回调 this 绑定修复
64
+ - 完善 README、接入指南、配置参考等文档
65
+
66
+ ### v0.1.1(2025年11月28日)
67
+
68
+ React Native SDK 首个正式版本,提供各种配置、API,并提供了以下采集能力:
69
+
70
+ - API 监控(XMLHttpRequest / fetch),包括链路追踪等特性的支持
71
+ - 异常监控(JS Error、Promise Rejection、console.error)
72
+ - Session 相关功能(采样、超时管理)
73
+ - 自定义上报(Custom、Exception、Resource、View、Event)
74
+ - React Navigation 集成支持
75
+
76
+ ### v0.1.0(2025年08月20日)
77
+
78
+ React Native 首个版本,提供各种配置、API,并提供了以下采集能力:
79
+
80
+ - API 监控(XMLHttpRequest / fetch),包括链路追踪等特性的支持
81
+ - 异常监控(JS Error、Promise Rejection、console.error)
82
+ - Session 相关功能(采样、超时管理)
83
+ - 自定义上报(Custom、Exception、Resource、View、Event)
84
+ - React Navigation 集成支持
@@ -18,7 +18,7 @@
18
18
 
19
19
  ### 前提条件
20
20
 
21
- - React Native版本 >= 0.71(SDK 使用新架构 TurboModule,需 RN 0.71 及以上)
21
+ - React Native版本 >= 0.68(SDK 已适配 0.68-0.70 多版本环境,原生模块缺失时自动降级为纯 JS 模式)
22
22
  - Node.js >= 18
23
23
  - npm或yarn包管理工具
24
24
  - Android 构建:需安装 NDK 与 CMake(由宿主工程的 `ndkVersion` 决定,SDK 不强制特定版本)
@@ -77,9 +77,9 @@ class ApiCollector {
77
77
  }
78
78
  hackXhr() {
79
79
  const oldXHRProto = XMLHttpRequest.prototype;
80
- const self = this;
80
+ const that = this;
81
81
  (0, _rumCore.interceptFunction)(oldXHRProto, 'open', function (method, url, async, user, password) {
82
- const filters = self.ctx.getConfig().filters || {};
82
+ const filters = that.ctx.getConfig().filters || {};
83
83
  const urlStr = String(url);
84
84
  const methodStr = method.toUpperCase();
85
85
  // chrome 对 URL 的最大长度为 8182 个字符
@@ -93,7 +93,7 @@ class ApiCollector {
93
93
  password,
94
94
  headers: {}
95
95
  };
96
- const baseRumEvent = self.ctx.session.getBaseEvent();
96
+ const baseRumEvent = that.ctx.session.getBaseEvent();
97
97
  this.apiAttr = {
98
98
  ...baseRumEvent,
99
99
  url: urlStr,
@@ -111,7 +111,7 @@ class ApiCollector {
111
111
  if (!this.apiAttr || !this.apiCache) return;
112
112
  let apiAttr = this.apiAttr;
113
113
  this.apiCache.data = body;
114
- self.injectTracing(this.apiAttr, tracingHeaders => {
114
+ that.injectTracing(this.apiAttr, tracingHeaders => {
115
115
  Object.keys(tracingHeaders).forEach(key => {
116
116
  tracingHeaders[key] && this.setRequestHeader(key, tracingHeaders[key]);
117
117
  });
@@ -124,7 +124,7 @@ class ApiCollector {
124
124
  } = this;
125
125
  const {
126
126
  evaluateApi
127
- } = self.ctx.getConfig();
127
+ } = that.ctx.getConfig();
128
128
  if (!this.apiAttr) return;
129
129
  apiAttr.status_code = status;
130
130
  apiAttr.message = statusText;
@@ -148,15 +148,21 @@ class ApiCollector {
148
148
  _rumCore.logger.warn('api-collector', 'evaluateApi failed for XHR', e);
149
149
  }
150
150
  }
151
- self.sendAPI(apiAttr, entry);
151
+ that.sendAPI(apiAttr, entry);
152
152
  });
153
153
  this.addEventListener('error', () => {
154
154
  this.apiAttr && (this.apiAttr.success = _rumCore.ResourceStatus.Failed);
155
155
  });
156
+ this.addEventListener('abort', () => {
157
+ if (this.apiAttr) {
158
+ this.apiAttr.success = _rumCore.ResourceStatus.Failed;
159
+ this.apiAttr.message = 'abort';
160
+ }
161
+ });
156
162
  }, true);
157
163
  }
158
164
  hackFetch() {
159
- const self = this;
165
+ const that = this;
160
166
  const originFetch = this.originFetch = globalThis.fetch;
161
167
  const filters = this.ctx.getConfig().filters || {};
162
168
  globalThis.fetch = function (input, init = {}) {
@@ -175,7 +181,7 @@ class ApiCollector {
175
181
  if ((0, _rumCore.urlMatch)(urlStr, filters.resource) || method === 'HEAD' || init?.mode === 'no-cors' || urlStr.length > 8182) {
176
182
  return originFetch.call(global, input, init);
177
183
  }
178
- const baseRumEvent = self.ctx.session.getBaseEvent();
184
+ const baseRumEvent = that.ctx.session.getBaseEvent();
179
185
  let apiAttr = {
180
186
  ...baseRumEvent,
181
187
  url: urlStr,
@@ -184,7 +190,7 @@ class ApiCollector {
184
190
  };
185
191
  let apiResp;
186
192
  let apiError;
187
- self.injectTracing(apiAttr, tracingHeaders => {
193
+ that.injectTracing(apiAttr, tracingHeaders => {
188
194
  Object.keys(tracingHeaders).forEach(key => {
189
195
  if (!headers) return;
190
196
  if (headers instanceof Headers) {
@@ -198,7 +204,7 @@ class ApiCollector {
198
204
  async function fetchFinally() {
199
205
  const {
200
206
  evaluateApi
201
- } = self.ctx.getConfig();
207
+ } = that.ctx.getConfig();
202
208
  const entry = {
203
209
  name: apiAttr.url,
204
210
  entryType: _api.ApiType.FETCH,
@@ -218,7 +224,7 @@ class ApiCollector {
218
224
  _rumCore.logger.warn('api-collector', 'evaluateApi failed for fetch', e);
219
225
  }
220
226
  }
221
- self.sendAPI(apiAttr, entry);
227
+ that.sendAPI(apiAttr, entry);
222
228
  }
223
229
  return originFetch.call(globalThis, input, init).then(response => {
224
230
  // @ts-ignore
@@ -138,7 +138,7 @@ class RNReporter extends _rumCore.Reporter {
138
138
  }
139
139
  }
140
140
  catchHandle = err => {
141
- _rumCore.logger.error('reporter', 'sendRequest failed', err);
141
+ _rumCore.logger.warn('reporter', 'sendRequest failed', err);
142
142
  };
143
143
  destroy() {
144
144
  this.appStateListener?.remove();
@@ -23,8 +23,9 @@ function reviseApiAttr(attrs) {
23
23
  if ((0, _rumCore.isString)(attrs.message)) {
24
24
  obj.message = attrs.message.substring(0, 1e3);
25
25
  }
26
- if ('success' in attrs) {
27
- obj.success = attrs.success ? 1 : 0;
26
+ // 对齐 Browser 端:保留三态 (-1/0/1),避免 Unknown(-1) 被误转为 Success(1)
27
+ if ((0, _rumCore.isNumber)(attrs.success) && attrs.success >= -1 && attrs.success <= 1) {
28
+ obj.success = attrs.success;
28
29
  }
29
30
  if ((0, _rumCore.isNumber)(attrs.duration) && attrs.duration >= 0) {
30
31
  obj.duration = attrs.duration;
@@ -14,7 +14,7 @@ var _rumCore = require("@arms/rum-core");
14
14
  var _reactNative = require("react-native");
15
15
  var _NativeRumSdkReactnative = _interopRequireDefault(require("../NativeRumSdkReactnative"));
16
16
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
17
- const _pkg = {"version":"0.1.3-beta.2"};
17
+ const _pkg = {"version":"0.1.3-beta.4"};
18
18
  const VERSION = exports.VERSION = _pkg.version;
19
19
 
20
20
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -73,9 +73,9 @@ export default class ApiCollector {
73
73
  }
74
74
  hackXhr() {
75
75
  const oldXHRProto = XMLHttpRequest.prototype;
76
- const self = this;
76
+ const that = this;
77
77
  interceptFunction(oldXHRProto, 'open', function (method, url, async, user, password) {
78
- const filters = self.ctx.getConfig().filters || {};
78
+ const filters = that.ctx.getConfig().filters || {};
79
79
  const urlStr = String(url);
80
80
  const methodStr = method.toUpperCase();
81
81
  // chrome 对 URL 的最大长度为 8182 个字符
@@ -89,7 +89,7 @@ export default class ApiCollector {
89
89
  password,
90
90
  headers: {}
91
91
  };
92
- const baseRumEvent = self.ctx.session.getBaseEvent();
92
+ const baseRumEvent = that.ctx.session.getBaseEvent();
93
93
  this.apiAttr = {
94
94
  ...baseRumEvent,
95
95
  url: urlStr,
@@ -107,7 +107,7 @@ export default class ApiCollector {
107
107
  if (!this.apiAttr || !this.apiCache) return;
108
108
  let apiAttr = this.apiAttr;
109
109
  this.apiCache.data = body;
110
- self.injectTracing(this.apiAttr, tracingHeaders => {
110
+ that.injectTracing(this.apiAttr, tracingHeaders => {
111
111
  Object.keys(tracingHeaders).forEach(key => {
112
112
  tracingHeaders[key] && this.setRequestHeader(key, tracingHeaders[key]);
113
113
  });
@@ -120,7 +120,7 @@ export default class ApiCollector {
120
120
  } = this;
121
121
  const {
122
122
  evaluateApi
123
- } = self.ctx.getConfig();
123
+ } = that.ctx.getConfig();
124
124
  if (!this.apiAttr) return;
125
125
  apiAttr.status_code = status;
126
126
  apiAttr.message = statusText;
@@ -144,15 +144,21 @@ export default class ApiCollector {
144
144
  logger.warn('api-collector', 'evaluateApi failed for XHR', e);
145
145
  }
146
146
  }
147
- self.sendAPI(apiAttr, entry);
147
+ that.sendAPI(apiAttr, entry);
148
148
  });
149
149
  this.addEventListener('error', () => {
150
150
  this.apiAttr && (this.apiAttr.success = ResourceStatus.Failed);
151
151
  });
152
+ this.addEventListener('abort', () => {
153
+ if (this.apiAttr) {
154
+ this.apiAttr.success = ResourceStatus.Failed;
155
+ this.apiAttr.message = 'abort';
156
+ }
157
+ });
152
158
  }, true);
153
159
  }
154
160
  hackFetch() {
155
- const self = this;
161
+ const that = this;
156
162
  const originFetch = this.originFetch = globalThis.fetch;
157
163
  const filters = this.ctx.getConfig().filters || {};
158
164
  globalThis.fetch = function (input, init = {}) {
@@ -171,7 +177,7 @@ export default class ApiCollector {
171
177
  if (urlMatch(urlStr, filters.resource) || method === 'HEAD' || init?.mode === 'no-cors' || urlStr.length > 8182) {
172
178
  return originFetch.call(global, input, init);
173
179
  }
174
- const baseRumEvent = self.ctx.session.getBaseEvent();
180
+ const baseRumEvent = that.ctx.session.getBaseEvent();
175
181
  let apiAttr = {
176
182
  ...baseRumEvent,
177
183
  url: urlStr,
@@ -180,7 +186,7 @@ export default class ApiCollector {
180
186
  };
181
187
  let apiResp;
182
188
  let apiError;
183
- self.injectTracing(apiAttr, tracingHeaders => {
189
+ that.injectTracing(apiAttr, tracingHeaders => {
184
190
  Object.keys(tracingHeaders).forEach(key => {
185
191
  if (!headers) return;
186
192
  if (headers instanceof Headers) {
@@ -194,7 +200,7 @@ export default class ApiCollector {
194
200
  async function fetchFinally() {
195
201
  const {
196
202
  evaluateApi
197
- } = self.ctx.getConfig();
203
+ } = that.ctx.getConfig();
198
204
  const entry = {
199
205
  name: apiAttr.url,
200
206
  entryType: ApiType.FETCH,
@@ -214,7 +220,7 @@ export default class ApiCollector {
214
220
  logger.warn('api-collector', 'evaluateApi failed for fetch', e);
215
221
  }
216
222
  }
217
- self.sendAPI(apiAttr, entry);
223
+ that.sendAPI(apiAttr, entry);
218
224
  }
219
225
  return originFetch.call(globalThis, input, init).then(response => {
220
226
  // @ts-ignore
@@ -134,7 +134,7 @@ class RNReporter extends Reporter {
134
134
  }
135
135
  }
136
136
  catchHandle = err => {
137
- logger.error('reporter', 'sendRequest failed', err);
137
+ logger.warn('reporter', 'sendRequest failed', err);
138
138
  };
139
139
  destroy() {
140
140
  this.appStateListener?.remove();
@@ -15,8 +15,9 @@ export function reviseApiAttr(attrs) {
15
15
  if (isString(attrs.message)) {
16
16
  obj.message = attrs.message.substring(0, 1e3);
17
17
  }
18
- if ('success' in attrs) {
19
- obj.success = attrs.success ? 1 : 0;
18
+ // 对齐 Browser 端:保留三态 (-1/0/1),避免 Unknown(-1) 被误转为 Success(1)
19
+ if (isNumber(attrs.success) && attrs.success >= -1 && attrs.success <= 1) {
20
+ obj.success = attrs.success;
20
21
  }
21
22
  if (isNumber(attrs.duration) && attrs.duration >= 0) {
22
23
  obj.duration = attrs.duration;
@@ -3,7 +3,7 @@
3
3
  import { AppType } from '@arms/rum-core';
4
4
  import { Dimensions, Platform } from 'react-native';
5
5
  import NativeRumSdkReactnative from "../NativeRumSdkReactnative.js";
6
- const _pkg = {"version":"0.1.3-beta.2"};
6
+ const _pkg = {"version":"0.1.3-beta.4"};
7
7
  export const VERSION = _pkg.version;
8
8
 
9
9
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arms/rum-reactnative",
3
- "version": "0.1.3-beta.2",
3
+ "version": "0.1.3-beta.4",
4
4
  "description": "RUM SDK For ReactNative",
5
5
  "homepage": "https://help.aliyun.com/zh/arms/user-experience-monitoring/",
6
6
  "main": "./lib/commonjs/index.js",
@@ -21,6 +21,7 @@
21
21
  },
22
22
  "files": [
23
23
  "lib",
24
+ "src/NativeRumSdkReactnative.ts",
24
25
  "android",
25
26
  "ios",
26
27
  "docs",
@@ -107,7 +108,7 @@
107
108
  "typescript": "^5.8.3"
108
109
  },
109
110
  "peerDependencies": {
110
- "@arms/rum-core": ">=0.1.4",
111
+ "@arms/rum-core": "0.1.7",
111
112
  "react": "*",
112
113
  "react-native": ">=0.68.0"
113
114
  },
@@ -0,0 +1,107 @@
1
+ import type { TurboModule } from 'react-native';
2
+ import { NativeModules, TurboModuleRegistry } from 'react-native';
3
+
4
+ /**
5
+ * 原生设备信息,对应存储模型中的公共属性:
6
+ * - device.* (id / type / brand / model / name)
7
+ * - os.* (type / version / container / container_version)
8
+ * - app.package
9
+ * - viewport.width / viewport.height
10
+ */
11
+ export interface NativeDeviceInfo {
12
+ device_id?: string;
13
+ device_type?: string;
14
+ device_brand?: string;
15
+ device_model?: string;
16
+ device_name?: string;
17
+ os_type?: string;
18
+ os_version?: string;
19
+ app_package?: string;
20
+ app_version?: string;
21
+ viewport_width?: number;
22
+ viewport_height?: number;
23
+ }
24
+
25
+ export interface Spec extends TurboModule {
26
+ getItem(key: string): Promise<string | null>;
27
+ setItem(key: string, value: string): Promise<void>;
28
+ removeItem(key: string): Promise<void>;
29
+ clear(): Promise<void>;
30
+ getAllKeys(): Promise<string[]>;
31
+
32
+ /**
33
+ * 采集原生平台设备信息,仅在初始化时调用一次。
34
+ */
35
+ getDeviceInfo(): Promise<NativeDeviceInfo>;
36
+
37
+ // === 崩溃采集相关 ===
38
+
39
+ /** 获取待上报的崩溃报告列表(下次启动时调用) */
40
+ getPendingCrashes(): Promise<string>;
41
+
42
+ /** 清除已上报的崩溃文件 */
43
+ clearCrashReports(crashIds: string): Promise<void>;
44
+
45
+ /** 清除所有崩溃报告文件(用于清理无 crash_id 的旧版文件) */
46
+ clearAllCrashReports(): Promise<void>;
47
+
48
+ /** 清理过期和超限的崩溃文件(maxFiles 上限,maxAgeMs 过期时间) */
49
+ cleanupOldCrashes(maxFiles: number, maxAgeMs: number): Promise<void>;
50
+
51
+ /** 查询上次运行是否发生过崩溃 */
52
+ crashedLastRun(): Promise<boolean>;
53
+
54
+ /** 更新崩溃上下文快照(session/view 变更时调用) */
55
+ updateCrashContext(context: string): void;
56
+
57
+ /** 触发测试崩溃(仅用于开发调试,调用后 App 立即崩溃) */
58
+ triggerTestCrash(crashType: string): void;
59
+ }
60
+
61
+ /**
62
+ * 获取原生模块。
63
+ *
64
+ * 优先使用 TurboModuleRegistry(RN 0.68+ TurboModule 架构),
65
+ * 若未找到则降级到 Legacy Bridge 的 NativeModules(RN 0.68-0.70 未开启 New Arch 时),
66
+ * 若均不可用则返回一组空实现兜底。
67
+ *
68
+ * 使用非强制的 `TurboModuleRegistry.get`(而非 `getEnforcing`),避免在原生
69
+ * 二进制未注册该模块时于「模块加载阶段」直接抛异常——那会连锁导致整个 JS
70
+ * bundle 评估失败(表现为所有路由丢失 default export、App 白屏)。
71
+ *
72
+ * 当原生模块缺失时(通常是新增原生代码后未重新编译原生 App,只重跑了 Metro),
73
+ * 返回一组空实现兜底:崩溃采集与原生设备信息降级为不可用,但 PV / 行为 / API /
74
+ * JS 异常等纯 JS 能力不受影响。需重新构建原生工程(如 `expo run:ios` /
75
+ * `pod install` + 重新编译)后崩溃采集才会真正生效。
76
+ */
77
+ const nativeModule =
78
+ TurboModuleRegistry.get<Spec>('RumSdkReactnative') ??
79
+ (NativeModules.RumSdkReactnative as Spec | undefined);
80
+
81
+ if (!nativeModule) {
82
+ console.warn(
83
+ "[rum] Native module 'RumSdkReactnative' not found in the native binary. " +
84
+ 'Crash reporting and native device info are disabled. ' +
85
+ 'Rebuild the native app (e.g. `expo run:ios` / `expo run:android`, ' +
86
+ 'or `pod install` + rebuild) so the module gets registered.'
87
+ );
88
+ }
89
+
90
+ /** 原生模块缺失时的空实现,保证调用方不会因缺方法而抛错。 */
91
+ const fallback: Spec = {
92
+ getItem: async () => null,
93
+ setItem: async () => {},
94
+ removeItem: async () => {},
95
+ clear: async () => {},
96
+ getAllKeys: async () => [],
97
+ getDeviceInfo: async () => ({}),
98
+ getPendingCrashes: async () => '[]',
99
+ clearCrashReports: async () => {},
100
+ clearAllCrashReports: async () => {},
101
+ cleanupOldCrashes: async () => {},
102
+ crashedLastRun: async () => false,
103
+ updateCrashContext: () => {},
104
+ triggerTestCrash: () => {},
105
+ };
106
+
107
+ export default nativeModule ?? fallback;