@arms/rum-reactnative 0.1.3-beta.4 → 0.1.4-beta.1

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.
Files changed (84) hide show
  1. package/README.md +7 -1
  2. package/RumSdkReactnative.podspec +3 -1
  3. package/android/src/main/java/com/rumsdkreactnative/RumSdkReactnativeModule.kt +31 -0
  4. package/docs/ReactNative SDK/351/205/215/347/275/256/345/217/202/350/200/203.md" +25 -0
  5. package/docs/SDK /347/211/210/346/234/254/350/257/264/346/230/216.md" +14 -0
  6. package/ios/RumAppStateRecorder.h +69 -0
  7. package/ios/RumAppStateRecorder.mm +547 -0
  8. package/ios/RumCrashReporter.h +32 -0
  9. package/ios/RumCrashReporter.mm +156 -33
  10. package/ios/RumSdkReactnative.mm +81 -1
  11. package/lib/commonjs/NativeRumSdkReactnative.js +1 -54
  12. package/lib/commonjs/collector/action/index.js +1 -108
  13. package/lib/commonjs/collector/exception/crash.js +1 -148
  14. package/lib/commonjs/collector/exception/index.js +1 -238
  15. package/lib/commonjs/collector/exception/oom.js +1 -0
  16. package/lib/commonjs/collector/index.js +1 -42
  17. package/lib/commonjs/collector/resource/api.js +1 -292
  18. package/lib/commonjs/collector/view/perf.js +1 -79
  19. package/lib/commonjs/collector/view/pv.js +1 -88
  20. package/lib/commonjs/expoConfigPlugin.js +1 -37
  21. package/lib/commonjs/index.js +1 -29
  22. package/lib/commonjs/plugin.js +1 -47
  23. package/lib/commonjs/processor/default-processor.js +1 -27
  24. package/lib/commonjs/processor/session-processor.js +1 -47
  25. package/lib/commonjs/reporter/index.js +1 -148
  26. package/lib/commonjs/reporter/monitor-reporter.js +1 -43
  27. package/lib/commonjs/shell.js +1 -91
  28. package/lib/commonjs/types/client.js +1 -6
  29. package/lib/commonjs/types/crash.js +1 -37
  30. package/lib/commonjs/types/index.js +1 -2
  31. package/lib/commonjs/types/oom.js +1 -0
  32. package/lib/commonjs/utils/action.js +1 -305
  33. package/lib/commonjs/utils/api.js +1 -84
  34. package/lib/commonjs/utils/base.js +1 -95
  35. package/lib/commonjs/utils/crash.js +1 -149
  36. package/lib/commonjs/utils/platform.js +1 -162
  37. package/lib/commonjs/utils/session.js +1 -158
  38. package/lib/commonjs/utils/url.js +1 -59
  39. package/lib/commonjs/utils/view.js +1 -93
  40. package/lib/module/NativeRumSdkReactnative.js +1 -51
  41. package/lib/module/collector/action/index.js +1 -103
  42. package/lib/module/collector/exception/crash.js +1 -143
  43. package/lib/module/collector/exception/index.js +1 -233
  44. package/lib/module/collector/exception/oom.js +1 -0
  45. package/lib/module/collector/index.js +1 -8
  46. package/lib/module/collector/resource/api.js +1 -287
  47. package/lib/module/collector/view/perf.js +1 -79
  48. package/lib/module/collector/view/pv.js +1 -83
  49. package/lib/module/expoConfigPlugin.js +1 -33
  50. package/lib/module/index.js +1 -7
  51. package/lib/module/plugin.js +1 -42
  52. package/lib/module/processor/default-processor.js +1 -22
  53. package/lib/module/processor/session-processor.js +1 -42
  54. package/lib/module/reporter/index.js +1 -144
  55. package/lib/module/reporter/monitor-reporter.js +1 -38
  56. package/lib/module/shell.js +1 -84
  57. package/lib/module/types/client.js +1 -4
  58. package/lib/module/types/crash.js +1 -33
  59. package/lib/module/types/index.js +1 -2
  60. package/lib/module/types/oom.js +1 -0
  61. package/lib/module/utils/action.js +1 -299
  62. package/lib/module/utils/api.js +1 -76
  63. package/lib/module/utils/base.js +1 -90
  64. package/lib/module/utils/crash.js +1 -144
  65. package/lib/module/utils/platform.js +1 -150
  66. package/lib/module/utils/session.js +1 -154
  67. package/lib/module/utils/url.js +1 -54
  68. package/lib/module/utils/view.js +1 -88
  69. package/lib/typescript/commonjs/src/NativeRumSdkReactnative.d.ts +10 -0
  70. package/lib/typescript/commonjs/src/collector/exception/oom.d.ts +34 -0
  71. package/lib/typescript/commonjs/src/collector/index.d.ts +1 -0
  72. package/lib/typescript/commonjs/src/shell.d.ts +1 -1
  73. package/lib/typescript/commonjs/src/types/client.d.ts +27 -0
  74. package/lib/typescript/commonjs/src/types/oom.d.ts +58 -0
  75. package/lib/typescript/commonjs/src/utils/crash.d.ts +13 -0
  76. package/lib/typescript/module/src/NativeRumSdkReactnative.d.ts +10 -0
  77. package/lib/typescript/module/src/collector/exception/oom.d.ts +34 -0
  78. package/lib/typescript/module/src/collector/index.d.ts +1 -0
  79. package/lib/typescript/module/src/shell.d.ts +1 -1
  80. package/lib/typescript/module/src/types/client.d.ts +27 -0
  81. package/lib/typescript/module/src/types/oom.d.ts +58 -0
  82. package/lib/typescript/module/src/utils/crash.d.ts +13 -0
  83. package/package.json +1 -2
  84. package/src/NativeRumSdkReactnative.ts +18 -0
@@ -1,144 +1 @@
1
- "use strict";
2
-
3
- import { Reporter, Telemetry, logger } from '@arms/rum-core';
4
- import { fixAttrs } from "../utils/platform.js";
5
- import { CRASH_ID_FIELD, CRASH_REPORT_ACK_EVENT } from "../types/crash.js";
6
- // @ts-ignore
7
- import { AppState } from 'react-native';
8
-
9
- /**
10
- * 从 bundle.events 的 snapshots 中提取携带的 crash_id(瞬态字段)。
11
- * crash_id 由 CrashCollector 注入到 snapshots JSON 字符串中,
12
- * reporter 提取后用于 ack,并在序列化前从 snapshots 中剥离,不会发送到后端。
13
- * 兼容旧版降级路径:crash_id 可能残留在事件根级。
14
- */
15
- function extractCrashIds(bundle) {
16
- const ids = [];
17
- const events = bundle?.events;
18
- if (Array.isArray(events)) {
19
- for (const ev of events) {
20
- if (!ev) continue;
21
- // 优先从 snapshots 提取
22
- let cid;
23
- try {
24
- const snapshotsObj = JSON.parse(ev.snapshots);
25
- cid = snapshotsObj?.[CRASH_ID_FIELD];
26
- } catch {
27
- // snapshots 解析失败
28
- }
29
- // 兼容旧版降级路径:检查根级
30
- if (!cid) {
31
- cid = ev[CRASH_ID_FIELD];
32
- }
33
- if (typeof cid === 'string' && cid) ids.push(cid);
34
- }
35
- }
36
- return ids;
37
- }
38
-
39
- /**
40
- * 序列化前剥离事件上的 crash_id 瞬态字段:
41
- * - 从 snapshots JSON 字符串中移除;
42
- * - 兼容旧版降级路径:从事件根级移除。
43
- */
44
- function stripCrashIds(bundle) {
45
- const events = bundle?.events;
46
- if (!Array.isArray(events)) return;
47
- for (const ev of events) {
48
- if (!ev) continue;
49
- // 从 snapshots 中移除
50
- if (typeof ev.snapshots === 'string') {
51
- try {
52
- const snapshotsObj = JSON.parse(ev.snapshots);
53
- if (snapshotsObj && CRASH_ID_FIELD in snapshotsObj) {
54
- delete snapshotsObj[CRASH_ID_FIELD];
55
- ev.snapshots = JSON.stringify(snapshotsObj);
56
- }
57
- } catch {
58
- // snapshots 解析失败,跳过
59
- }
60
- }
61
- // 兼容旧版降级路径:从根级移除
62
- if (CRASH_ID_FIELD in ev) {
63
- delete ev[CRASH_ID_FIELD];
64
- }
65
- }
66
- }
67
-
68
- /**
69
- * rn-app reporter
70
- */
71
- class RNReporter extends Reporter {
72
- name = 'rn-reporter';
73
- constructor() {
74
- super();
75
- // 离开之前 flushEventQueue
76
- try {
77
- this.appStateListener = AppState.addEventListener('change', this.onAppStateChanged);
78
- } catch (error) {
79
- logger.warn('reporter', 'AppState listener binding failed', error);
80
- }
81
- }
82
- init(ctx) {
83
- this.ctx = ctx;
84
- }
85
- onAppStateChanged = nextAppState => {
86
- if (nextAppState === 'background') {
87
- // 执行后台操作:暂停动画、保存数据、停止网络请求等
88
- if (typeof this.flushEventQueue === 'function') {
89
- this.flushEventQueue();
90
- }
91
- // 后台前 flush 自监控数据
92
- Telemetry.get()?.flush();
93
- }
94
- // else if (nextAppState === 'active') {
95
- // console.warn('RUM:应用回到前台');
96
- // }
97
- };
98
- async request(ctx, bundle) {
99
- // 先从事件中提取崩溃 crash_id(瞬态字段),用于上报后 ack 删文件
100
- const crashIds = extractCrashIds(bundle);
101
- try {
102
- const batchedEvents = await fixAttrs(bundle);
103
- // 序列化前剥离瞬态字段,避免发送到后端
104
- stripCrashIds(batchedEvents);
105
- const config = ctx.getConfig();
106
- fetch(config.endpoint, {
107
- method: 'POST',
108
- headers: {
109
- 'Content-Type': 'text/plain'
110
- },
111
- body: JSON.stringify(batchedEvents)
112
- }).then(res => {
113
- this.ackCrashes(ctx, crashIds, !!(res && res.ok));
114
- }).catch(err => {
115
- this.catchHandle(err);
116
- this.ackCrashes(ctx, crashIds, false);
117
- });
118
- } catch (err) {
119
- this.catchHandle(err);
120
- this.ackCrashes(ctx, crashIds, false);
121
- }
122
- }
123
-
124
- /** 通过共享 emitter 通知崩溃采集器上报结果(成功则删文件,失败保留重试) */
125
- ackCrashes(ctx, crashIds, success) {
126
- if (!crashIds.length) return;
127
- try {
128
- ctx.emitter?.emit(CRASH_REPORT_ACK_EVENT, {
129
- crashIds,
130
- success
131
- });
132
- } catch (e) {
133
- logger.warn('reporter', 'emit crash ack failed', e);
134
- }
135
- }
136
- catchHandle = err => {
137
- logger.warn('reporter', 'sendRequest failed', err);
138
- };
139
- destroy() {
140
- this.appStateListener?.remove();
141
- }
142
- }
143
- export default RNReporter;
144
- //# sourceMappingURL=index.js.map
1
+ "use strict";import{Reporter,Telemetry,logger}from"@arms/rum-core";import{fixAttrs}from"../utils/platform.js";import{CRASH_ID_FIELD,CRASH_REPORT_ACK_EVENT}from"../types/crash.js";import{AppState}from"react-native";function extractCrashIds(t){const e=[],r=t?.events;if(Array.isArray(r))for(const t of r){if(!t)continue;let r;try{const e=JSON.parse(t.snapshots);r=e?.[CRASH_ID_FIELD]}catch{}r||(r=t[CRASH_ID_FIELD]),"string"==typeof r&&r&&e.push(r)}return e}function stripCrashIds(t){const e=t?.events;if(Array.isArray(e))for(const t of e)if(t){if("string"==typeof t.snapshots)try{const e=JSON.parse(t.snapshots);e&&CRASH_ID_FIELD in e&&(delete e[CRASH_ID_FIELD],t.snapshots=JSON.stringify(e))}catch{}CRASH_ID_FIELD in t&&delete t[CRASH_ID_FIELD]}}class RNReporter extends Reporter{name="rn-reporter";constructor(){super();try{this.appStateListener=AppState.addEventListener("change",this.onAppStateChanged)}catch(t){logger.warn("reporter","AppState listener binding failed",t)}}init(t){this.ctx=t}onAppStateChanged=t=>{"background"===t&&("function"==typeof this.flushEventQueue&&this.flushEventQueue(),Telemetry.get()?.flush())};async request(t,e){const r=extractCrashIds(e);try{const s=await fixAttrs(e);stripCrashIds(s);const a=t.getConfig();fetch(a.endpoint,{method:"POST",headers:{"Content-Type":"text/plain"},body:JSON.stringify(s)}).then(e=>{this.ackCrashes(t,r,!(!e||!e.ok))}).catch(e=>{this.catchHandle(e),this.ackCrashes(t,r,!1)})}catch(e){this.catchHandle(e),this.ackCrashes(t,r,!1)}}ackCrashes(t,e,r){if(e.length)try{t.emitter?.emit(CRASH_REPORT_ACK_EVENT,{crashIds:e,success:r})}catch(t){logger.warn("reporter","emit crash ack failed",t)}}catchHandle=t=>{logger.warn("reporter","sendRequest failed",t)};destroy(){this.appStateListener?.remove()}}export default RNReporter;
@@ -1,38 +1 @@
1
- "use strict";
2
-
3
- import { convertBundlesToLogGroup } from '@arms/rum-core';
4
- export class RNMonitorReporter {
5
- constructor(endpoint) {
6
- this.endpoint = this.buildMonitorEndpoint(endpoint);
7
- }
8
- send(data) {
9
- if (!data || data.length === 0) return;
10
- try {
11
- // 使用 rum-core 的 Protobuf 编码器将数据编码为 SLS LogGroup 二进制格式
12
- const protobufBody = convertBundlesToLogGroup(data);
13
- fetch(this.endpoint, {
14
- method: 'POST',
15
- headers: {
16
- 'Content-Type': 'application/x-protobuf',
17
- 'x-log-apiversion': '0.6.0',
18
- 'x-log-compresstype': '',
19
- 'x-log-date': new Date().toUTCString()
20
- },
21
- body: protobufBody
22
- }).catch(() => {
23
- // 静默处理,避免反馈循环(不调用 logger)
24
- });
25
- } catch (_) {
26
- // 吞掉异常,防止自监控影响宿主应用
27
- }
28
- }
29
- buildMonitorEndpoint(endpoint) {
30
- try {
31
- const url = new URL(endpoint);
32
- return `${url.origin}/rum/monitor`;
33
- } catch (_) {
34
- return endpoint;
35
- }
36
- }
37
- }
38
- //# sourceMappingURL=monitor-reporter.js.map
1
+ "use strict";import{convertBundlesToLogGroup}from"@arms/rum-core";export class RNMonitorReporter{constructor(o){this.endpoint=this.buildMonitorEndpoint(o)}send(o){if(o&&0!==o.length)try{const t=convertBundlesToLogGroup(o);fetch(this.endpoint,{method:"POST",headers:{"Content-Type":"application/x-protobuf","x-log-apiversion":"0.6.0","x-log-compresstype":"","x-log-date":(new Date).toUTCString()},body:t}).catch(()=>{})}catch(o){}}buildMonitorEndpoint(o){try{return`${new URL(o).origin}/rum/monitor`}catch(t){return o}}}
@@ -1,84 +1 @@
1
- "use strict";
2
-
3
- import { isFunction, Shell, callMonitored } from '@arms/rum-core';
4
- import * as collectors from "./collector/index.js";
5
- import DefaultProcessor from "./processor/default-processor.js";
6
- import SessionProcessor from "./processor/session-processor.js";
7
- import Reporter from "./reporter/index.js";
8
- import { RNMonitorReporter } from "./reporter/monitor-reporter.js";
9
- import { VERSION } from "./utils/platform.js";
10
- import { RumSession } from "./utils/session.js";
11
- import { startTrackingViews, stopTrackingViews } from "./utils/view.js";
12
- const builtInCollectors = [...Object.values(collectors).filter(Collector => isFunction(Collector))];
13
- /**
14
- * 对外导出 shell 层, 所有 shell 层模型的 API 设计约定:
15
- * 1. API 命名空间按照 variables / functions / events 来组织
16
- * 2. 事件(events)的命名格式为:on[Will|Did]VerbNoun?,参考 https://code.visualstudio.com/api/references/vscode-api#events
17
- * 3. 基于 Disposable 模式,对于事件的绑定、快捷键的绑定函数,返回值则是解绑函数
18
- */
19
- export class ArmsRum extends Shell {
20
- version = VERSION;
21
-
22
- /**
23
- * 初始化
24
- */
25
- init(config) {
26
- this.client.useCollectors(builtInCollectors.map(Collector => callMonitored(() => new Collector(), undefined, [], 'shell', `init ${Collector.name}`)).filter(Boolean));
27
- this.client.useProcessors([new DefaultProcessor(), new SessionProcessor()]);
28
- this.client.useReporter(new Reporter());
29
- // 传入 MonitorReporter 激活自监控
30
- const monitorReporter = config.endpoint ? new RNMonitorReporter(config.endpoint) : undefined;
31
- this.client.init(config, new RumSession(), undefined, monitorReporter);
32
- return this;
33
- }
34
-
35
- /**
36
- * set config
37
- */
38
-
39
- setConfig(...params) {
40
- const ctx = this.client.getContext();
41
- if (params.length === 2) {
42
- const oldConfig = ctx.getConfig();
43
- ctx.setConfig({
44
- ...oldConfig,
45
- [params[0]]: params[1]
46
- });
47
- } else {
48
- ctx.setConfig(params[0]);
49
- }
50
- }
51
-
52
- /**
53
- * 开启路由变化监听,自动采集 PV 数据
54
- *
55
- * 必须在 NavigationContainer 的 `onReady` 回调中调用,以确保
56
- * navigationRef 已正确初始化。
57
- *
58
- * @example
59
- * ```tsx
60
- * <NavigationContainer
61
- * ref={navigationRef}
62
- * onReady={() => {
63
- * ArmsRum.startTrackingViews(navigationRef);
64
- * }}
65
- * >
66
- * ...
67
- * </NavigationContainer>
68
- * ```
69
- *
70
- * @param navigationRef - NavigationContainer 的 ref 引用
71
- */
72
- startTrackingViews(navigationRef) {
73
- if (!navigationRef) return;
74
- startTrackingViews(navigationRef);
75
- }
76
- stopTrackingViews() {
77
- stopTrackingViews();
78
- }
79
- getCollector(name) {
80
- return this.client.getCollectors().find(collector => collector.name === name);
81
- }
82
- }
83
- export default new ArmsRum();
84
- //# sourceMappingURL=shell.js.map
1
+ "use strict";import{isFunction,Shell,callMonitored}from"@arms/rum-core";import*as collectors from"./collector/index.js";import DefaultProcessor from"./processor/default-processor.js";import SessionProcessor from"./processor/session-processor.js";import Reporter from"./reporter/index.js";import{RNMonitorReporter}from"./reporter/monitor-reporter.js";import{VERSION}from"./utils/platform.js";import{RumSession}from"./utils/session.js";import{startTrackingViews,stopTrackingViews}from"./utils/view.js";const builtInCollectors=[...Object.values(collectors).filter(e=>isFunction(e))];export class ArmsRum extends Shell{version=VERSION;init(e){this.client.useCollectors(builtInCollectors.map(e=>callMonitored(()=>new e,void 0,[],"shell",`init ${e.name}`)).filter(Boolean)),this.client.useProcessors([new DefaultProcessor,new SessionProcessor]),this.client.useReporter(new Reporter);const o=e.endpoint?new RNMonitorReporter(e.endpoint):void 0;return this.client.init(e,new RumSession,void 0,o),this}setConfig(...e){const o=this.client.getContext();if(2===e.length){const s=o.getConfig();o.setConfig({...s,[e[0]]:e[1]})}else o.setConfig(e[0])}startTrackingViews(e){e&&startTrackingViews(e)}stopTrackingViews(){stopTrackingViews()}getCollector(e){return this.client.getCollectors().find(o=>o.name===e)}}export default new ArmsRum;
@@ -1,4 +1 @@
1
- "use strict";
2
-
3
- export {};
4
- //# sourceMappingURL=client.js.map
1
+ "use strict";export{};
@@ -1,33 +1 @@
1
- "use strict";
2
-
3
- /**
4
- * 崩溃相关类型定义
5
- */
6
-
7
- /** 崩溃上下文同步事件名 */
8
- export const CRASH_CONTEXT_EVENT = 'crash_context_change';
9
-
10
- /** 崩溃上报确认事件名(reporter -> crash collector,用于 ack 后删除文件) */
11
- export const CRASH_REPORT_ACK_EVENT = 'crash_report_ack';
12
-
13
- /**
14
- * 挂在崩溃 RumEvent 上的瞬态字段名,携带 crash_id。
15
- * reporter 上报前提取用于 ack、并在序列化前剥离,不会发送到后端。
16
- */
17
- export const CRASH_ID_FIELD = '__rum_crash_id';
18
-
19
- /** 崩溃上报确认事件负载 */
20
-
21
- /**
22
- * 崩溃上下文快照(session / view 变更时同步到原生侧)。
23
- */
24
-
25
- /**
26
- * 原生崩溃栈帧数据结构。
27
- */
28
-
29
- /**
30
- * 原生崩溃数据结构(与原生侧 RumCrashReporter / CrashContextManager 写入的 JSON 对齐)。
31
- * 仅声明 JS 层会用到的字段,未列出的字段会通过 snapshots 透传给后端。
32
- */
33
- //# sourceMappingURL=crash.js.map
1
+ "use strict";export const CRASH_CONTEXT_EVENT="crash_context_change";export const CRASH_REPORT_ACK_EVENT="crash_report_ack";export const CRASH_ID_FIELD="__rum_crash_id";
@@ -1,2 +1 @@
1
- "use strict";
2
- //# sourceMappingURL=index.js.map
1
+ "use strict";
@@ -0,0 +1 @@
1
+ "use strict";
@@ -1,299 +1 @@
1
- "use strict";
2
-
3
- import { isFunction } from '@arms/rum-core';
4
-
5
- /**
6
- * 行为事件监听 & React Native 触摸自动采集
7
- *
8
- * 通过 patch React 的元素创建函数(createElement 及自动 JSX 运行时
9
- * jsx/jsxs/jsxDEV),自动拦截 Pressable / TouchableOpacity 等可触摸组件的
10
- * onPress,并从触摸事件(GestureResponderEvent)、onLayout、props/children 中
11
- * 提取丰富的行为数据(位置、尺寸、持续时间、语义化名称、快照等),发射标准事件
12
- * 给 ActionCollector。
13
- *
14
- * 说明:现代 RN/Expo(babel-preset-expo)默认使用「automatic」JSX 运行时,
15
- * JSX 会被编译为 react/jsx-runtime 的 jsx()/jsxs()(生产)或
16
- * react/jsx-dev-runtime 的 jsxDEV()(开发),而非 React.createElement。
17
- * 因此必须同时 patch 这三者才能覆盖所有场景。
18
- */
19
-
20
- const actionListeners = [];
21
- let patched = false;
22
-
23
- /** 语义化名称中截取的子元素文本最大长度 */
24
- const MAX_TEXT_LENGTH = 20;
25
-
26
- /** 已 patch 的目标:{ module, key, original } 用于还原 */
27
-
28
- let patchTargets = [];
29
- export function addActionListener(listener) {
30
- if (isFunction(listener)) {
31
- actionListeners.push(listener);
32
- }
33
- }
34
- export function removeActionListener(listener) {
35
- const idx = actionListeners.indexOf(listener);
36
- if (idx > -1) {
37
- actionListeners.splice(idx, 1);
38
- }
39
- }
40
-
41
- /** 采集到的原始触摸数据,交由 emitAction 组装为标准 ActionEvent */
42
-
43
- function emitAction(raw) {
44
- const {
45
- id,
46
- componentName,
47
- props,
48
- children,
49
- nativeEvent,
50
- pressInTs,
51
- layout
52
- } = raw;
53
-
54
- // 关键属性快照
55
- const dataset = {};
56
- if (props.id) dataset.id = props.id;
57
- if (props.testID) dataset.testID = props.testID;
58
- if (props.accessibilityLabel) {
59
- dataset.accessibilityLabel = props.accessibilityLabel;
60
- }
61
- const text = getElementText(children);
62
- const selector = componentName ? `${componentName}#${id}` : `#${id}`;
63
- const label = `tap ${selector}`;
64
- const name = text ? `${label}: ${text}` : label;
65
- const position = getPosition(nativeEvent);
66
- const duration = pressInTs && nativeEvent && typeof nativeEvent.timestamp === 'number' ? Math.max(0, Math.round(nativeEvent.timestamp - pressInTs)) : undefined;
67
- const event = {
68
- type: 'tap',
69
- name,
70
- targetName: selector,
71
- duration,
72
- position,
73
- target: {
74
- id,
75
- dataset,
76
- selector,
77
- width: layout?.width,
78
- height: layout?.height,
79
- // 带 onPress 的 touchable 本身即为可交互元素
80
- reaction: 1,
81
- // 存在原生触摸事件视为可信交互,纯代码调用视为不可信
82
- trust: nativeEvent ? 1 : 0
83
- }
84
- };
85
- actionListeners.forEach(listener => {
86
- listener(event);
87
- });
88
- }
89
-
90
- /** 获取组件的显示名称,如 Pressable / TouchableOpacity */
91
- function getComponentName(type) {
92
- if (!type) return undefined;
93
- if (typeof type === 'string') return type;
94
- return type.displayName || type.name || undefined;
95
- }
96
-
97
- /** 递归提取 children 中的文本内容,用于生成语义化名称 */
98
- function getElementText(children) {
99
- const acc = {
100
- text: ''
101
- };
102
- collectText(children, acc);
103
- const t = acc.text.trim();
104
- if (!t) return '';
105
- return t.length > MAX_TEXT_LENGTH ? t.slice(0, MAX_TEXT_LENGTH) + '...' : t;
106
- }
107
- function collectText(node, acc) {
108
- if (node == null || typeof node === 'boolean') return;
109
- if (acc.text.length > MAX_TEXT_LENGTH) return;
110
- if (typeof node === 'string' || typeof node === 'number') {
111
- acc.text += String(node);
112
- return;
113
- }
114
- if (Array.isArray(node)) {
115
- node.forEach(c => collectText(c, acc));
116
- return;
117
- }
118
- if (node.props && node.props.children != null) {
119
- collectText(node.props.children, acc);
120
- }
121
- }
122
-
123
- /** 从原生触摸事件中提取点击位置(屏幕坐标 + 元素内相对坐标) */
124
- function getPosition(nativeEvent) {
125
- if (!nativeEvent) return undefined;
126
- const {
127
- pageX,
128
- pageY,
129
- locationX,
130
- locationY
131
- } = nativeEvent;
132
- if (pageX == null && pageY == null && locationX == null && locationY == null) {
133
- return undefined;
134
- }
135
- return {
136
- x: round(pageX),
137
- y: round(pageY),
138
- ex: round(locationX),
139
- ey: round(locationY)
140
- };
141
- }
142
- function round(n) {
143
- return typeof n === 'number' ? Math.round(n) : undefined;
144
- }
145
-
146
- /**
147
- * 标记位:用于在已包装的 onPress 函数上做去重判断,避免重复包装
148
- */
149
- const RUM_WRAPPED = '__rumWrapped';
150
-
151
- /**
152
- * 生成一个「元素创建函数」的包装器。
153
- *
154
- * createElement / jsx / jsxs / jsxDEV 的前两个参数都是 (type, props),
155
- * 其余参数(children / key / source 等)原样透传即可,因此可用统一逻辑处理。
156
- *
157
- * 除包装 onPress 采集点击外,还会顺带包装 onPressIn(记录按下时间以计算
158
- * duration)与 onLayout(记录元素尺寸 width/height),以补全行为数据。
159
- *
160
- * childrenInRest:经典 createElement 的 children 以可变参数传入(在 rest 中),
161
- * 而 automatic 运行时 jsx/jsxs/jsxDEV 的 children 在 props.children 中。
162
- */
163
- function makeWrapper(original, touchableTypes, childrenInRest) {
164
- return function patchedElementFactory(type, props, ...rest) {
165
- if (props && touchableTypes.has(type) && isFunction(props.onPress)) {
166
- const id = props.id || props.testID || props.accessibilityLabel;
167
- if (id && !props.onPress[RUM_WRAPPED]) {
168
- const componentName = getComponentName(type);
169
- const children = childrenInRest ? rest.length <= 1 ? rest[0] : rest : props.children;
170
- const originalOnPress = props.onPress;
171
- const originalOnPressIn = props.onPressIn;
172
- const originalOnLayout = props.onLayout;
173
-
174
- // 闭包内保存按下时间与布局尺寸,供 onPress 采集时读取
175
- let pressInTs = 0;
176
- let layout;
177
- const wrappedOnPressIn = (...args) => {
178
- const ne = args[0] && args[0].nativeEvent;
179
- pressInTs = ne && typeof ne.timestamp === 'number' ? ne.timestamp : 0;
180
- if (isFunction(originalOnPressIn)) return originalOnPressIn(...args);
181
- };
182
- const wrappedOnLayout = (...args) => {
183
- const ne = args[0] && args[0].nativeEvent;
184
- const l = ne && ne.layout;
185
- if (l) {
186
- layout = {
187
- width: round(l.width),
188
- height: round(l.height)
189
- };
190
- }
191
- if (isFunction(originalOnLayout)) return originalOnLayout(...args);
192
- };
193
- const wrappedOnPress = (...args) => {
194
- emitAction({
195
- id,
196
- componentName,
197
- props,
198
- children,
199
- nativeEvent: args[0] && args[0].nativeEvent,
200
- pressInTs,
201
- layout
202
- });
203
- return originalOnPress(...args);
204
- };
205
- wrappedOnPress[RUM_WRAPPED] = true;
206
- return original(type, {
207
- ...props,
208
- onPress: wrappedOnPress,
209
- onPressIn: wrappedOnPressIn,
210
- onLayout: wrappedOnLayout
211
- }, ...rest);
212
- }
213
- }
214
- return original(type, props, ...rest);
215
- };
216
- }
217
-
218
- /**
219
- * 安全地按字面量 require 一个模块,失败返回 undefined。
220
- *
221
- * 注意:Metro 的 babel 插件禁止动态 `require(变量)`,因此这里必须用
222
- * 字面量字符串分别 require,不能抽成 `safeRequire(name)` 之类的通用函数。
223
- */
224
- function safeRequireJsxRuntime() {
225
- try {
226
- return require('react/jsx-runtime');
227
- } catch {
228
- return undefined;
229
- }
230
- }
231
- function safeRequireJsxDevRuntime() {
232
- try {
233
- return require('react/jsx-dev-runtime');
234
- } catch {
235
- return undefined;
236
- }
237
- }
238
- function patchModuleFn(mod, key, touchableTypes) {
239
- if (!mod || !isFunction(mod[key])) return;
240
- const original = mod[key];
241
- // 经典 createElement 的 children 以可变参数传入;jsx 运行时则在 props.children
242
- const childrenInRest = key === 'createElement';
243
- try {
244
- mod[key] = makeWrapper(original, touchableTypes, childrenInRest);
245
- patchTargets.push({
246
- mod,
247
- key,
248
- original
249
- });
250
- } catch {
251
- // 某些环境下属性只读,忽略
252
- }
253
- }
254
-
255
- /**
256
- * 开始自动采集用户点击行为
257
- *
258
- * 原理:patch React.createElement 以及 automatic JSX 运行时的 jsx/jsxs/jsxDEV,
259
- * 当组件为可触摸类型且含 onPress + id/testID/accessibilityLabel 时,
260
- * 包装 onPress 使其在调用时自动 emit action 事件。
261
- */
262
- export function startTrackingActions() {
263
- if (patched) return;
264
- const React = require('react');
265
- const RN = require('react-native');
266
- const touchableTypes = new Set([RN.Pressable, RN.TouchableOpacity, RN.TouchableHighlight, RN.TouchableWithoutFeedback, RN.TouchableNativeFeedback, RN.Button].filter(Boolean));
267
-
268
- // 经典运行时
269
- patchModuleFn(React, 'createElement', touchableTypes);
270
- // automatic 运行时(生产)
271
- const jsxRuntime = safeRequireJsxRuntime();
272
- patchModuleFn(jsxRuntime, 'jsx', touchableTypes);
273
- patchModuleFn(jsxRuntime, 'jsxs', touchableTypes);
274
- // automatic 运行时(开发)
275
- const jsxDevRuntime = safeRequireJsxDevRuntime();
276
- patchModuleFn(jsxDevRuntime, 'jsxDEV', touchableTypes);
277
- patched = patchTargets.length > 0;
278
- }
279
-
280
- /**
281
- * 停止自动采集
282
- */
283
- export function stopTrackingActions() {
284
- if (!patched) return;
285
- patchTargets.forEach(({
286
- mod,
287
- key,
288
- original
289
- }) => {
290
- try {
291
- mod[key] = original;
292
- } catch {
293
- // 忽略还原失败
294
- }
295
- });
296
- patchTargets = [];
297
- patched = false;
298
- }
299
- //# sourceMappingURL=action.js.map
1
+ "use strict";import{isFunction}from"@arms/rum-core";const actionListeners=[];let patched=!1;const MAX_TEXT_LENGTH=20;let patchTargets=[];export function addActionListener(t){isFunction(t)&&actionListeners.push(t)}export function removeActionListener(t){const e=actionListeners.indexOf(t);e>-1&&actionListeners.splice(e,1)}function emitAction(t){const{id:e,componentName:n,props:i,children:o,nativeEvent:r,pressInTs:c,layout:s}=t,a={};i.id&&(a.id=i.id),i.testID&&(a.testID=i.testID),i.accessibilityLabel&&(a.accessibilityLabel=i.accessibilityLabel);const u=getElementText(o),l=n?`${n}#${e}`:`#${e}`,p=`tap ${l}`,h=u?`${p}: ${u}`:p,d=getPosition(r),m={type:"tap",name:h,targetName:l,duration:c&&r&&"number"==typeof r.timestamp?Math.max(0,Math.round(r.timestamp-c)):void 0,position:d,target:{id:e,dataset:a,selector:l,width:s?.width,height:s?.height,reaction:1,trust:r?1:0}};actionListeners.forEach(t=>{t(m)})}function getComponentName(t){if(t)return"string"==typeof t?t:t.displayName||t.name||void 0}function getElementText(t){const e={text:""};collectText(t,e);const n=e.text.trim();return n?n.length>20?n.slice(0,20)+"...":n:""}function collectText(t,e){null!=t&&"boolean"!=typeof t&&(e.text.length>20||("string"!=typeof t&&"number"!=typeof t?Array.isArray(t)?t.forEach(t=>collectText(t,e)):t.props&&null!=t.props.children&&collectText(t.props.children,e):e.text+=String(t)))}function getPosition(t){if(!t)return;const{pageX:e,pageY:n,locationX:i,locationY:o}=t;return null!=e||null!=n||null!=i||null!=o?{x:round(e),y:round(n),ex:round(i),ey:round(o)}:void 0}function round(t){return"number"==typeof t?Math.round(t):void 0}const RUM_WRAPPED="__rumWrapped";function makeWrapper(t,e,n){return function(i,o,...r){if(o&&e.has(i)&&isFunction(o.onPress)){const e=o.id||o.testID||o.accessibilityLabel;if(e&&!o.onPress[RUM_WRAPPED]){const c=getComponentName(i),s=n?r.length<=1?r[0]:r:o.children,a=o.onPress,u=o.onPressIn,l=o.onLayout;let p,h=0;const d=(...t)=>{const e=t[0]&&t[0].nativeEvent;if(h=e&&"number"==typeof e.timestamp?e.timestamp:0,isFunction(u))return u(...t)},m=(...t)=>{const e=t[0]&&t[0].nativeEvent,n=e&&e.layout;if(n&&(p={width:round(n.width),height:round(n.height)}),isFunction(l))return l(...t)},f=(...t)=>(emitAction({id:e,componentName:c,props:o,children:s,nativeEvent:t[0]&&t[0].nativeEvent,pressInTs:h,layout:p}),a(...t));return f[RUM_WRAPPED]=!0,t(i,{...o,onPress:f,onPressIn:d,onLayout:m},...r)}}return t(i,o,...r)}}function safeRequireJsxRuntime(){try{return require("react/jsx-runtime")}catch{return}}function safeRequireJsxDevRuntime(){try{return require("react/jsx-dev-runtime")}catch{return}}function patchModuleFn(t,e,n){if(!t||!isFunction(t[e]))return;const i=t[e],o="createElement"===e;try{t[e]=makeWrapper(i,n,o),patchTargets.push({mod:t,key:e,original:i})}catch{}}export function startTrackingActions(){if(patched)return;const t=require("react"),e=require("react-native"),n=new Set([e.Pressable,e.TouchableOpacity,e.TouchableHighlight,e.TouchableWithoutFeedback,e.TouchableNativeFeedback,e.Button].filter(Boolean));patchModuleFn(t,"createElement",n);const i=safeRequireJsxRuntime();patchModuleFn(i,"jsx",n),patchModuleFn(i,"jsxs",n);patchModuleFn(safeRequireJsxDevRuntime(),"jsxDEV",n),patched=patchTargets.length>0}export function stopTrackingActions(){patched&&(patchTargets.forEach(({mod:t,key:e,original:n})=>{try{t[e]=n}catch{}}),patchTargets=[],patched=!1)}